I am trying to setup Storybook 8.0.8 to work with angular 17.3. I am using Angular input()
signal in components, and I am running into an interesting gotcha where the storybook stories args also want the argument type to be a signal. I could try and get the injection context available in the story, but this doesn't come off to me as "a good way to do things".
I'm hoping for a clean, inline way to provide my args as literal values as they used to be. Anyone out there ran into this same problem and found a decent solution?
Here's a simple angular component and storybook story that replicates my issue
const meta: Meta<NoteComponent> = {
title: 'Shared/Note',
component: NoteComponent,
tags: ['autodocs'],
args: {
maxChars: 200 // Error: Type 'number' is not assignable to type 'InputSignal<number>'.
}
};
export default meta;
type Story = StoryObj<NoteComponent>;
export const Primary: Story = {
};
@Component({
selector: 'app-note',
template: 'A note with {{maxChars()}} maximum characters',
standalone: true,
})
export class NoteComponent {
maxChars = input<number>(300);
}
Reminder that using any signal()
creation functions expects an injection context to be in scope. Calling signal()
without an injection context produces
NG0203: inputFunction() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with runInInjectionContext
Storybook 8.0.8, the problem is that storybook is able to use non signal properties as args but expects us to provide signal one. Internally, Storybook uses TransformEventType which will convert any EventEmitter to a callback. I.e. we can provide a callback for an EventEmitter property. Unfortunately, internally, SB does not transform the type of signal inputs into the held value of the signal.
I therefore wrote this temporary helper:
storybook.helper.ts
export function toArgs<Component>(
args: Partial<TransformSignalInputType<TransformEventType<Component>>>
): TransformEventType<Component> {
return args as unknown as TransformEventType<Component>;
}
/** Convert event emitter to callback for storybook */
type TransformEventType<T> = {
[K in keyof T]: T[K] extends EventEmitter<infer E> ? (e: E) => void : T[K];
};
/** Convert any input signal into the held type of the signal */
type TransformSignalInputType<T> = {
[K in keyof T]: TransformInputType<T[K]>;
};
import { InputSignalWithTransform, InputSignal } from '@angular/core';
// Type to extract the type from InputSignal or InputSignalWithTransform
type TransformInputType<T> =
T extends InputSignalWithTransform<infer U, any>
? U
: T extends InputSignal<infer U>
? U
: T;
You can then use it as follows:
const meta: Meta<NoteComponent> = {
title: 'Shared/Note',
component: NoteComponent,
tags: ['autodocs'],
args: toArgs<NoteComponent>({
maxChars: 200
})
};