0
# Template Utilities
1
2
Utilities for converting story arguments to Angular template bindings and handling dynamic template generation.
3
4
## Capabilities
5
6
### argsToTemplate Function
7
8
Converts an object of arguments to a string of Angular property and event bindings, automatically excluding undefined values to preserve component default values.
9
10
```typescript { .api }
11
/**
12
* Converts an object of arguments to a string of property and event bindings and excludes undefined
13
* values. Angular treats undefined values in property bindings as an actual value and does not apply
14
* the default value of the property as soon as the binding is set.
15
*
16
* @param args - Object containing story arguments
17
* @param options - Options for controlling which properties to include/exclude
18
* @returns String of Angular template bindings
19
*/
20
declare function argsToTemplate<A extends Record<string, any>>(
21
args: A,
22
options?: ArgsToTemplateOptions<keyof A>
23
): string;
24
25
interface ArgsToTemplateOptions<T> {
26
/**
27
* An array of keys to specifically include in the output. If provided, only the keys from this
28
* array will be included in the output, irrespective of the `exclude` option. Undefined values
29
* will still be excluded from the output.
30
*/
31
include?: Array<T>;
32
/**
33
* An array of keys to specifically exclude from the output. If provided, these keys will be
34
* omitted from the output. This option is ignored if the `include` option is also provided
35
*/
36
exclude?: Array<T>;
37
}
38
```
39
40
**Why argsToTemplate is Important:**
41
42
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.
43
44
**Basic Usage Example:**
45
46
```typescript
47
import { argsToTemplate } from "@storybook/angular";
48
49
// Component with default values
50
@Component({
51
selector: 'example-button',
52
template: `<button>{{ label }}</button>`
53
})
54
export class ExampleComponent {
55
@Input() label: string = 'Default Label';
56
@Input() size: string = 'medium';
57
@Output() click = new EventEmitter<void>();
58
}
59
60
// Story using argsToTemplate
61
export const Dynamic: Story = {
62
render: (args) => ({
63
props: args,
64
template: `<example-button ${argsToTemplate(args)}></example-button>`,
65
}),
66
args: {
67
label: 'Custom Label',
68
// size is undefined, so component default 'medium' will be used
69
click: { action: 'clicked' },
70
},
71
};
72
73
// Generated template will be:
74
// <example-button [label]="label" (click)="click($event)"></example-button>
75
// Note: size is excluded because it's undefined
76
```
77
78
**Comparison Without argsToTemplate:**
79
80
```typescript
81
// Without argsToTemplate (problematic)
82
export const Problematic: Story = {
83
render: (args) => ({
84
props: args,
85
template: `<example-button [label]="label" [size]="size" (click)="click($event)"></example-button>`,
86
}),
87
args: {
88
label: 'Custom Label',
89
// size is undefined, but binding will set it to undefined, overriding default
90
click: { action: 'clicked' },
91
},
92
};
93
// Result: size becomes undefined instead of using component default 'medium'
94
```
95
96
**Include Option Example:**
97
98
```typescript
99
import { argsToTemplate } from "@storybook/angular";
100
101
export const SpecificProps: Story = {
102
render: (args) => ({
103
props: args,
104
template: `<example-button ${argsToTemplate(args, { include: ['label', 'size'] })}></example-button>`,
105
}),
106
args: {
107
label: 'Button Text',
108
size: 'large',
109
disabled: true,
110
click: { action: 'clicked' },
111
},
112
};
113
114
// Generated template will only include label and size:
115
// <example-button [label]="label" [size]="size"></example-button>
116
```
117
118
**Exclude Option Example:**
119
120
```typescript
121
import { argsToTemplate } from "@storybook/angular";
122
123
export const ExcludeInternal: Story = {
124
render: (args) => ({
125
props: args,
126
template: `<example-button ${argsToTemplate(args, { exclude: ['internalProp'] })}></example-button>`,
127
}),
128
args: {
129
label: 'Button Text',
130
size: 'large',
131
internalProp: 'not-for-template',
132
click: { action: 'clicked' },
133
},
134
};
135
136
// Generated template excludes internalProp:
137
// <example-button [label]="label" [size]="size" (click)="click($event)"></example-button>
138
```
139
140
**Complex Component Example:**
141
142
```typescript
143
import { argsToTemplate } from "@storybook/angular";
144
145
@Component({
146
selector: 'user-profile',
147
template: `
148
<div class="profile">
149
<h2>{{ name || 'Anonymous User' }}</h2>
150
<p>Age: {{ age || 'Not specified' }}</p>
151
<button (click)="onEdit()">Edit</button>
152
</div>
153
`
154
})
155
export class UserProfileComponent {
156
@Input() name?: string;
157
@Input() age?: number;
158
@Input() email?: string;
159
@Input() showEdit: boolean = true;
160
@Output() edit = new EventEmitter<void>();
161
@Output() delete = new EventEmitter<string>();
162
163
onEdit() {
164
this.edit.emit();
165
}
166
}
167
168
export const UserProfile: Story = {
169
render: (args) => ({
170
props: args,
171
template: `<user-profile ${argsToTemplate(args)}></user-profile>`,
172
}),
173
args: {
174
name: 'John Doe',
175
// age is undefined - component will show 'Not specified'
176
email: 'john@example.com',
177
// showEdit is undefined - component default 'true' will be used
178
edit: { action: 'edit-clicked' },
179
delete: { action: 'delete-clicked' },
180
},
181
};
182
183
// Generated template:
184
// <user-profile [name]="name" [email]="email" (edit)="edit($event)" (delete)="delete($event)"></user-profile>
185
```
186
187
**Event Binding Handling:**
188
189
The function automatically detects function properties and creates event bindings:
190
191
```typescript
192
export const WithEvents: Story = {
193
render: (args) => ({
194
props: args,
195
template: `<button ${argsToTemplate(args)}></button>`,
196
}),
197
args: {
198
text: 'Click me',
199
disabled: false,
200
// Functions become event bindings
201
click: (event) => console.log('Clicked!', event),
202
mouseOver: { action: 'hovered' },
203
focus: () => alert('Focused'),
204
},
205
};
206
207
// Generated template:
208
// <button [text]="text" [disabled]="disabled" (click)="click($event)" (mouseOver)="mouseOver($event)" (focus)="focus($event)"></button>
209
```
210
211
**Working with Angular Signals:**
212
213
For components using Angular signals (v17+), argsToTemplate works seamlessly:
214
215
```typescript
216
@Component({
217
selector: 'signal-component',
218
template: `<p>{{ message() }} - Count: {{ count() }}</p>`
219
})
220
export class SignalComponent {
221
message = input<string>('Default message');
222
count = input<number>(0);
223
increment = output<number>();
224
}
225
226
export const WithSignals: Story = {
227
render: (args) => ({
228
props: args,
229
template: `<signal-component ${argsToTemplate(args)}></signal-component>`,
230
}),
231
args: {
232
message: 'Hello Signals!',
233
// count is undefined, component default 0 will be used
234
increment: { action: 'incremented' },
235
},
236
};
237
238
// Generated template:
239
// <signal-component [message]="message" (increment)="increment($event)"></signal-component>
240
```
241
242
## Best Practices
243
244
### When to Use argsToTemplate
245
246
- **Always recommended** for dynamic templates where args determine which properties to bind
247
- Essential when components have meaningful default values that should be preserved
248
- Useful for generic story templates that work across different component configurations
249
250
### Performance Considerations
251
252
- `argsToTemplate` performs filtering on each render, which is generally fast but consider caching for complex objects
253
- For static templates with known properties, explicit template strings may be more performant
254
255
### Type Safety
256
257
- Use with TypeScript for better IntelliSense and type checking
258
- Consider creating typed wrappers for frequently used components:
259
260
```typescript
261
function createButtonTemplate<T extends ButtonComponent>(args: Partial<T>) {
262
return `<app-button ${argsToTemplate(args)}></app-button>`;
263
}
264
```
265
266
### Common Patterns
267
268
Combine with component default values:
269
270
```typescript
271
export const ConfigurableButton: Story = {
272
render: (args) => ({
273
props: { ...DEFAULT_BUTTON_PROPS, ...args },
274
template: `<button ${argsToTemplate(args)}></button>`,
275
}),
276
};
277
```
278
279
Use with conditional rendering:
280
281
```typescript
282
export const ConditionalContent: Story = {
283
render: (args) => ({
284
props: args,
285
template: `
286
<div>
287
<header ${argsToTemplate(args, { include: ['title', 'subtitle'] })}></header>
288
${args.showContent ? `<main ${argsToTemplate(args, { exclude: ['title', 'subtitle'] })}></main>` : ''}
289
</div>
290
`,
291
}),
292
};
293
```