or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

cli-builders.mddecorators.mdframework-config.mdindex.mdportable-stories.mdstory-types.mdtemplate-utilities.md

template-utilities.mddocs/

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

```