What are some tips for leveraging Angular input signals in Storybook?

I am currently working on setting up Storybook 8.0.8 with Angular 17.3. I have been using the Angular input() signal in my components, but I've encountered an interesting issue where the args for the storybook stories also need the argument type to be a signal. Trying to access the injection context in the story doesn't seem like the ideal solution to me.

I am looking for a cleaner way to provide my args as literal values, as it used to be. Has anyone faced this problem before and discovered a good workaround?

Below is a basic angular component and storybook story that demonstrates the 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);
}

Just a reminder, when using any signal() creation function, make sure there is an injection context available. Using signal() without an injection context will result in

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

Answer №1

I have come up with a solution to the issue I was facing. It seems that Storybook only relies on the component's class definition as the expected type for the story args, without directly binding the values to the component instance. By finding a workaround within TypeScript's type system, I was able to make it compile and render the component in Storybook correctly.

const meta: Meta<NoteComponent> = {
  title: 'Shared/Note',
  component: NoteComponent,
  tags: ['autodocs'],
  args: {
    maxChars: 200 as unknown as InputSignal<number>
  }
};

Although it may not be ideal, this solution does get the job done.

Answer №2

In version 8.0.8 of Storybook, there is an issue where non-signal properties can be used as arguments, but Storybook expects signal properties to be provided. Internally, Storybook utilizes TransformEventType to convert any EventEmitter into a callback. This means that we can provide a callback for an EventEmitter property. However, Storybook does not internally transform the type of signal inputs into the actual value of the signal.

To address this, I created a 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 like this:

const meta: Meta<NoteComponent> = {
  title: 'Shared/Note',
  component: NoteComponent,
  tags: ['autodocs'],
  args: toArgs<NoteComponent>({
    maxChars: 200
  })
};

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

There was an issue with the program: "ERROR TypeError: When trying to call a method from the

Hey everyone, I'm attempting to retrieve data from an API using a specific service and then log the data in the console. However, I'm encountering an error with the component and service I'm using that reads: ERROR TypeError: Cannot read pro ...

Choose the appropriate NPM module platform for Angular development

My Angular application runs smoothly in Chrome, but encounters errors on Internet Explorer. The issue arises from the different distributions of NPM modules I install. For instance, within the kendo-angular-charts folder, we have: - dist |- cdn |- e ...

Differences Between Angular Event Emitter and Click Listener

I've been delving into Angular by exploring the "Your First App" section on their official website. One aspect that has caught my attention is the event emitter functionality. In the Parent Component (product-list): <app-product-alerts [produc ...

Utilizing a powerful combination of Angular 5, PrimeNG charts, Spring Boot, and JHipster

I am facing an issue with creating charts using PrimeNG. The main challenge I'm encountering is the conversion of data from a REST API in Angular 5 (TypeScript) and retrieving the list of measurements from the API. I have an endpoint that returns my m ...

When the application initializes, the Child Component is activated

There's a scenario where I need to trigger a component named 'cancellation' when the user clicks a button in another component called 'names'. To achieve this, I set a flag called loadCancellation to true when the Search button is ...

I want my Angular 2 application to redirect to the appropriate page when a user who is logged out attempts to access a page that requires them to be logged in

When a user is logged out in Angular 2 router and they try to navigate to a page that requires them to be logged in, I need the app.ts file to redirect them. I am utilizing typescript along with angular 2. Oddly enough, the redirection works for certain ...

Unable to generate or compose a text document within my Ionic application

After attempting to create a file and write in it, I'm encountering an issue where the file appears to be created but is not visible when navigating to the folder. Can someone please point out what might be going wrong? Below is my code snippet: th ...

Is it possible to modify the host header within an Angular app?

I'm experiencing a vulnerability issue and to resolve it, I need to utilize SERVER_NAME instead of the Host header. Is it possible to accomplish this using Angular? ...

Angular mat-table experiencing issues with matToolTip functionality

My Angular project is using Angular Material 16x, but for some reason, the matToolTip is not displaying at all. I have experimented with various versions, including a basic matTooltip="hello world", but I just can't seem to get it to work. I have come ...

Scrolling horizontally in Ionic framework

In regards to the response found on Ionic - Horizontal scroll tab for Categories, I have a question. I am curious about what needs to be included in the category.model. Can anyone provide some guidance? ...

"Dealing with Angular 9's mat-table and the pesky ExpressionChangedAfterItHasBeenChecked

I am facing an issue with my Angular 9 component that has multiple mat-tables. One of the tables contains rows with input fields bound to a reactive form array. While binding the table to the form array works well, I encounter an error when loading the for ...

Troubleshooting Appium error management is ineffective

As a newcomer to appium, I might have made some mistakes. I'm encountering a problem with appium while using wdio and jasmine. it('wtf', (done) => { client.init().element('someName').getText() // ^ here ...

Utilizing the <slot> feature in Angular 5 for increased functionality

Currently, I am working on a single page application (SPA) where Vue framework has been utilized for development purposes. Front-End: Vue Back-End: NodeJs Within my application, there are other sub-modules built in Angular 4. I am looking to replicate th ...

Best practices for implementing dual ngFor directives within a single tr element?

Click here to view the page The image attached shows the view I want to iterate through two ngFor loops inside the tr tag. When using a div tag inside the tr, it's looping the button next to the tag instead of where I want it in the file table header ...

Detecting changes in an ondrop event triggered by a modification in object property

As I work on creating a drag and drop interface, I realize the importance of implementing change detection for smooth functionality. Below is a snippet of my component: import { Component, OnInit, ChangeDetectorRef } from '@angular/core'; impor ...

Angular Boilerplate is experiencing difficulties in properly reading ABP

Working on my boilerplate project, I am now diving into consuming backend services (using asp .net) in Angular through http requests. However, I encountered an issue when trying to implement the delete method in mycomponent.ts, as TypeScript was not recogn ...

Strategies for effectively choosing this specific entity from the repository

Is it possible to choose the right entity when crafting a repository method using typeorm? I'm facing an issue where I need to select the password property specifically from the Admin entity, however, the "this" keyword selects the Repository instead ...

What is the reason behind the ineffectiveness of injection in abstraction?

I am working with an interface export interface Tree {} The base class implements this interface: export class TreeBase implements Tree {} There are several concrete classes that extend the TreeBase: export class TreeLayers extends TreeBase {} export cl ...

Utilizing Directives to Embed Attributes

My current challenge involves changing the fill color of attributes in an in-line SVG using Angular and TypeScript. The goal is to have the SVG elements with a "TA" attribute change their fill color based on a user-selected color from a dropdown menu. Howe ...

What exactly does the use of type assertion as any in Typescript entail?

I'm attempting to dissect a portion of code and figure out its functionality. While I've encountered type assertion before, this particular example is proving to be quite baffling for me. (this.whatever as any).something([]); Here's the la ...