Synchronize Story Controls with Component state #32441
-
SummaryI have a Angular ButtonComponent which can be made toggleable. For this, the I'm using Storybook to test the component and check the styling. I've configured my Story so that the My problem is that whenever I click on the button to change the How can I synchronize the Story controls with the state of the component whenever its state changes? Additional informationMy Angular library resides inside a Nx managed mono-repository. I'm using the following package versions:
Below is my Component's story: import {
ButtonShapes,
ButtonSizes,
ButtonTypes,
DEFAULT_BUTTON_SHAPE,
DEFAULT_BUTTON_SIZE,
DEFAULT_BUTTON_TYPE,
} from '@dnd-mapp/shared-ui';
import { Meta, StoryObj } from '@storybook/angular';
import { ButtonStoryComponent } from './button-story.component';
const meta = {
title: 'Components/Buttons/Button',
tags: ['!dev'],
component: ButtonStoryComponent,
args: {
type: DEFAULT_BUTTON_TYPE,
size: DEFAULT_BUTTON_SIZE,
shape: DEFAULT_BUTTON_SHAPE,
toggle: false,
selected: false,
label: 'My Button label',
},
argTypes: {
type: {
options: Object.values(ButtonTypes),
control: 'select',
},
size: {
options: Object.values(ButtonSizes),
control: 'select',
},
shape: {
options: Object.values(ButtonShapes),
control: 'select',
},
},
} satisfies Meta<ButtonStoryComponent>;
export default meta;
type Story = StoryObj<ButtonStoryComponent>;
export const Default: Story = {
tags: ['dev'],
name: 'Interactive',
}; The accompanying story component: import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
DestroyRef,
inject,
input,
output,
signal,
} from '@angular/core';
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
import {
ButtonComponent,
buttonShapeAttribute,
buttonSizeAttribute,
buttonTypeAttribute,
ThemeDirective,
} from '@dnd-mapp/shared-ui';
@Component({
selector: 'dma-story',
templateUrl: './button-story.component.html',
styleUrl: './button-story.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [ThemeDirective],
imports: [ButtonComponent],
})
export class ButtonStoryComponent {
private readonly destroyRef = inject(DestroyRef);
public readonly type = input.required({ transform: buttonTypeAttribute });
public readonly size = input.required({ transform: buttonSizeAttribute });
public readonly shape = input.required({ transform: buttonShapeAttribute });
public readonly toggle = input(false, { transform: booleanAttribute });
public readonly selected = input(false, { transform: booleanAttribute });
public readonly selectedChange = output<boolean>();
protected readonly isSelected = signal(false);
constructor() {
toObservable(this.selected)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (selected) => this.isSelected.set(selected),
});
}
public readonly label = input('My Button label');
protected onSelectedChange(selected: boolean) {
this.isSelected.set(selected);
this.selectedChange.emit(selected);
}
} And the ButtonComponent itself: import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
inject,
input,
output,
signal,
} from '@angular/core';
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
import {
buttonShapeAttribute,
buttonSizeAttribute,
buttonTypeAttribute,
DEFAULT_BUTTON_SHAPE,
DEFAULT_BUTTON_SIZE,
DEFAULT_BUTTON_TYPE,
} from './models';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'button[dmaButton]',
templateUrl: './button.component.html',
styleUrl: './button.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[attr.dma-button]': 'type()',
'[attr.dma-button-size]': 'size()',
'[attr.dma-button-shape]': 'shape()',
'[attr.dma-toggle-button]': 'isToggleButton()',
'[class.selected]': 'isSelected() && toggle()',
'(click)': 'onClick()',
},
imports: [],
})
export class ButtonComponent {
private readonly destroyRef = inject(DestroyRef);
public readonly type = input(DEFAULT_BUTTON_TYPE, { transform: buttonTypeAttribute, alias: 'dmaButton' });
// eslint-disable-next-line @angular-eslint/no-input-rename
public readonly size = input(DEFAULT_BUTTON_SIZE, { transform: buttonSizeAttribute, alias: 'dmaButtonSize' });
// eslint-disable-next-line @angular-eslint/no-input-rename
public readonly shape = input(DEFAULT_BUTTON_SHAPE, { transform: buttonShapeAttribute, alias: 'dmaButtonShape' });
public readonly toggle = input(false, { transform: booleanAttribute });
public readonly selected = input(false, { transform: booleanAttribute });
public readonly selectedChange = output<boolean>();
protected readonly isToggleButton = computed(() => (this.toggle() ? '' : undefined));
protected readonly isSelected = signal(false);
constructor() {
toObservable(this.selected)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (selected) => this.isSelected.set(selected),
});
}
protected onClick() {
if (!this.toggle()) return;
this.isSelected.update((selected) => !selected);
this.selectedChange.emit(this.isSelected());
}
} Create a reproductionNo response |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 2 replies
-
Storybook controls (args) don't automatically sync with internal component state changes in Angular—when your component updates its state (like toggling For Angular, you can do this by using a custom render function in your story that binds the component's output to a handler which calls import { Meta, StoryObj } from '@storybook/angular';
import { ButtonComponent } from './button.component';
const meta: Meta<ButtonComponent> = {
component: ButtonComponent,
render: (args, { updateArgs }) => ({
props: {
...args,
selectedChange: (selected: boolean) => {
updateArgs({ selected });
},
},
}),
};
export default meta;
type Story = StoryObj<ButtonComponent>;
export const Interactive: Story = {
args: {
toggle: true,
selected: false,
label: 'My Button label',
},
}; This way, whenever the button emits Keep in mind, the Docs tab's code panel may not always reflect live state changes—this is a known limitation in Storybook's current implementation. To reply, just mention @dosu. How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other |
Beta Was this translation helpful? Give feedback.
This error is expected—Storybook hooks like
useArgs
(and functions likeupdateArgs
/setArgs
) can only be used inside story functions or decorators, not inside Angular component code or event handlers. In Angular stories,updateArgs
is not reliably available in the context object by default, which is why you see it asundefined
or missing in your render function context. This is a known limitation in Storybook 9.x for Angular and is being worked on for future releases [source].To work around this and enable syncing component state with Storybook controls, you can add a decorator in your
.storybook/preview.ts
that injectsupdateArgs
into the story context. Here’s how you can do it:// .sto…