CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-storybook--angular

Storybook for Angular: Develop, document, and test UI components in isolation

Pending
Overview
Eval results
Files

template-utilities.mddocs/

Template Utilities

Utilities for converting story arguments to Angular template bindings and handling dynamic template generation.

Capabilities

argsToTemplate Function

Converts an object of arguments to a string of Angular property and event bindings, automatically excluding undefined values to preserve component default values.

/**
 * Converts an object of arguments to a string of property and event bindings and excludes undefined
 * values. Angular treats undefined values in property bindings as an actual value and does not apply
 * the default value of the property as soon as the binding is set.
 * 
 * @param args - Object containing story arguments
 * @param options - Options for controlling which properties to include/exclude
 * @returns String of Angular template bindings
 */
declare function argsToTemplate<A extends Record<string, any>>(
  args: A,
  options?: ArgsToTemplateOptions<keyof A>
): string;

interface ArgsToTemplateOptions<T> {
  /**
   * An array of keys to specifically include in the output. If provided, only the keys from this
   * array will be included in the output, irrespective of the `exclude` option. Undefined values
   * will still be excluded from the output.
   */
  include?: Array<T>;
  /**
   * An array of keys to specifically exclude from the output. If provided, these keys will be
   * omitted from the output. This option is ignored if the `include` option is also provided
   */
  exclude?: Array<T>;
}

Why argsToTemplate is Important:

Angular treats undefined values in property bindings as actual values, preventing the component's default values from being used. This utility excludes undefined values, allowing default values to work while still making all properties controllable via Storybook controls.

Basic Usage Example:

import { argsToTemplate } from "@storybook/angular";

// Component with default values
@Component({
  selector: 'example-button',
  template: `<button>{{ label }}</button>`
})
export class ExampleComponent {
  @Input() label: string = 'Default Label';
  @Input() size: string = 'medium';
  @Output() click = new EventEmitter<void>();
}

// Story using argsToTemplate
export const Dynamic: Story = {
  render: (args) => ({
    props: args,
    template: `<example-button ${argsToTemplate(args)}></example-button>`,
  }),
  args: {
    label: 'Custom Label',
    // size is undefined, so component default 'medium' will be used
    click: { action: 'clicked' },
  },
};

// Generated template will be:
// <example-button [label]="label" (click)="click($event)"></example-button>
// Note: size is excluded because it's undefined

Comparison Without argsToTemplate:

// Without argsToTemplate (problematic)
export const Problematic: Story = {
  render: (args) => ({
    props: args,
    template: `<example-button [label]="label" [size]="size" (click)="click($event)"></example-button>`,
  }),
  args: {
    label: 'Custom Label',
    // size is undefined, but binding will set it to undefined, overriding default
    click: { action: 'clicked' },
  },
};
// Result: size becomes undefined instead of using component default 'medium'

Include Option Example:

import { argsToTemplate } from "@storybook/angular";

export const SpecificProps: Story = {
  render: (args) => ({
    props: args,
    template: `<example-button ${argsToTemplate(args, { include: ['label', 'size'] })}></example-button>`,
  }),
  args: {
    label: 'Button Text',
    size: 'large',
    disabled: true,
    click: { action: 'clicked' },
  },
};

// Generated template will only include label and size:
// <example-button [label]="label" [size]="size"></example-button>

Exclude Option Example:

import { argsToTemplate } from "@storybook/angular";

export const ExcludeInternal: Story = {
  render: (args) => ({
    props: args,
    template: `<example-button ${argsToTemplate(args, { exclude: ['internalProp'] })}></example-button>`,
  }),
  args: {
    label: 'Button Text',
    size: 'large',
    internalProp: 'not-for-template',
    click: { action: 'clicked' },
  },
};

// Generated template excludes internalProp:
// <example-button [label]="label" [size]="size" (click)="click($event)"></example-button>

Complex Component Example:

import { argsToTemplate } from "@storybook/angular";

@Component({
  selector: 'user-profile',
  template: `
    <div class="profile">
      <h2>{{ name || 'Anonymous User' }}</h2>
      <p>Age: {{ age || 'Not specified' }}</p>
      <button (click)="onEdit()">Edit</button>
    </div>
  `
})
export class UserProfileComponent {
  @Input() name?: string;
  @Input() age?: number;
  @Input() email?: string;
  @Input() showEdit: boolean = true;
  @Output() edit = new EventEmitter<void>();
  @Output() delete = new EventEmitter<string>();
  
  onEdit() {
    this.edit.emit();
  }
}

export const UserProfile: Story = {
  render: (args) => ({
    props: args,
    template: `<user-profile ${argsToTemplate(args)}></user-profile>`,
  }),
  args: {
    name: 'John Doe',
    // age is undefined - component will show 'Not specified'
    email: 'john@example.com',
    // showEdit is undefined - component default 'true' will be used
    edit: { action: 'edit-clicked' },
    delete: { action: 'delete-clicked' },
  },
};

// Generated template:
// <user-profile [name]="name" [email]="email" (edit)="edit($event)" (delete)="delete($event)"></user-profile>

Event Binding Handling:

The function automatically detects function properties and creates event bindings:

export const WithEvents: Story = {
  render: (args) => ({
    props: args,
    template: `<button ${argsToTemplate(args)}></button>`,
  }),
  args: {
    text: 'Click me',
    disabled: false,
    // Functions become event bindings
    click: (event) => console.log('Clicked!', event),
    mouseOver: { action: 'hovered' },
    focus: () => alert('Focused'),
  },
};

// Generated template:
// <button [text]="text" [disabled]="disabled" (click)="click($event)" (mouseOver)="mouseOver($event)" (focus)="focus($event)"></button>

Working with Angular Signals:

For components using Angular signals (v17+), argsToTemplate works seamlessly:

@Component({
  selector: 'signal-component',
  template: `<p>{{ message() }} - Count: {{ count() }}</p>`
})
export class SignalComponent {
  message = input<string>('Default message');
  count = input<number>(0);
  increment = output<number>();
}

export const WithSignals: Story = {
  render: (args) => ({
    props: args,
    template: `<signal-component ${argsToTemplate(args)}></signal-component>`,
  }),
  args: {
    message: 'Hello Signals!',
    // count is undefined, component default 0 will be used
    increment: { action: 'incremented' },
  },
};

// Generated template:
// <signal-component [message]="message" (increment)="increment($event)"></signal-component>

Best Practices

When to Use argsToTemplate

  • Always recommended for dynamic templates where args determine which properties to bind
  • Essential when components have meaningful default values that should be preserved
  • Useful for generic story templates that work across different component configurations

Performance Considerations

  • argsToTemplate performs filtering on each render, which is generally fast but consider caching for complex objects
  • For static templates with known properties, explicit template strings may be more performant

Type Safety

  • Use with TypeScript for better IntelliSense and type checking
  • Consider creating typed wrappers for frequently used components:
function createButtonTemplate<T extends ButtonComponent>(args: Partial<T>) {
  return `<app-button ${argsToTemplate(args)}></app-button>`;
}

Common Patterns

Combine with component default values:

export const ConfigurableButton: Story = {
  render: (args) => ({
    props: { ...DEFAULT_BUTTON_PROPS, ...args },
    template: `<button ${argsToTemplate(args)}></button>`,
  }),
};

Use with conditional rendering:

export const ConditionalContent: Story = {
  render: (args) => ({
    props: args,
    template: `
      <div>
        <header ${argsToTemplate(args, { include: ['title', 'subtitle'] })}></header>
        ${args.showContent ? `<main ${argsToTemplate(args, { exclude: ['title', 'subtitle'] })}></main>` : ''}
      </div>
    `,
  }),
};

Install with Tessl CLI

npx tessl i tessl/npm-storybook--angular

docs

cli-builders.md

decorators.md

framework-config.md

index.md

portable-stories.md

story-types.md

template-utilities.md

tile.json