-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #24434 from storybookjs/valentin/angular-args-to-t…
…emplate Angular: Introduce argsToTemplate for property and event Bindings
- Loading branch information
Showing
35 changed files
with
651 additions
and
381 deletions.
There are no files selected for viewing
102 changes: 102 additions & 0 deletions
102
code/frameworks/angular/src/client/argsToTemplate.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { argsToTemplate, ArgsToTemplateOptions } from './argsToTemplate'; // adjust path | ||
|
||
describe('argsToTemplate', () => { | ||
it('should correctly convert args to template string and exclude undefined values', () => { | ||
const args: Record<string, any> = { | ||
prop1: 'value1', | ||
prop2: undefined, | ||
prop3: 'value3', | ||
}; | ||
const options: ArgsToTemplateOptions<keyof typeof args> = {}; | ||
const result = argsToTemplate(args, options); | ||
expect(result).toBe('[prop1]="prop1" [prop3]="prop3"'); | ||
}); | ||
|
||
it('should include properties from include option', () => { | ||
const args = { | ||
prop1: 'value1', | ||
prop2: 'value2', | ||
prop3: 'value3', | ||
}; | ||
const options: ArgsToTemplateOptions<keyof typeof args> = { | ||
include: ['prop1', 'prop3'], | ||
}; | ||
const result = argsToTemplate(args, options); | ||
expect(result).toBe('[prop1]="prop1" [prop3]="prop3"'); | ||
}); | ||
|
||
it('should include non-undefined properties from include option', () => { | ||
const args: Record<string, any> = { | ||
prop1: 'value1', | ||
prop2: 'value2', | ||
prop3: undefined, | ||
}; | ||
const options: ArgsToTemplateOptions<keyof typeof args> = { | ||
include: ['prop1', 'prop3'], | ||
}; | ||
const result = argsToTemplate(args, options); | ||
expect(result).toBe('[prop1]="prop1"'); | ||
}); | ||
|
||
it('should exclude properties from exclude option', () => { | ||
const args = { | ||
prop1: 'value1', | ||
prop2: 'value2', | ||
prop3: 'value3', | ||
}; | ||
const options: ArgsToTemplateOptions<keyof typeof args> = { | ||
exclude: ['prop2'], | ||
}; | ||
const result = argsToTemplate(args, options); | ||
expect(result).toBe('[prop1]="prop1" [prop3]="prop3"'); | ||
}); | ||
|
||
it('should exclude properties from exclude option and undefined properties', () => { | ||
const args: Record<string, any> = { | ||
prop1: 'value1', | ||
prop2: 'value2', | ||
prop3: undefined, | ||
}; | ||
const options: ArgsToTemplateOptions<keyof typeof args> = { | ||
exclude: ['prop2'], | ||
}; | ||
const result = argsToTemplate(args, options); | ||
expect(result).toBe('[prop1]="prop1"'); | ||
}); | ||
|
||
it('should prioritize include over exclude when both options are given', () => { | ||
const args = { | ||
prop1: 'value1', | ||
prop2: 'value2', | ||
prop3: 'value3', | ||
}; | ||
const options: ArgsToTemplateOptions<keyof typeof args> = { | ||
include: ['prop1', 'prop2'], | ||
exclude: ['prop2', 'prop3'], | ||
}; | ||
const result = argsToTemplate(args, options); | ||
expect(result).toBe('[prop1]="prop1" [prop2]="prop2"'); | ||
}); | ||
|
||
it('should work when neither include nor exclude options are given', () => { | ||
const args = { | ||
prop1: 'value1', | ||
prop2: 'value2', | ||
}; | ||
const options: ArgsToTemplateOptions<keyof typeof args> = {}; | ||
const result = argsToTemplate(args, options); | ||
expect(result).toBe('[prop1]="prop1" [prop2]="prop2"'); | ||
}); | ||
|
||
it('should bind events correctly when value is a function', () => { | ||
const args = { event1: () => {}, event2: () => {} }; | ||
const result = argsToTemplate(args, {}); | ||
expect(result).toEqual('(event1)="event1($event)" (event2)="event2($event)"'); | ||
}); | ||
|
||
it('should mix properties and events correctly', () => { | ||
const args = { input: 'Value1', event1: () => {} }; | ||
const result = argsToTemplate(args, {}); | ||
expect(result).toEqual('[input]="input" (event1)="event1($event)"'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/** | ||
* Options for controlling the behavior of the argsToTemplate function. | ||
* | ||
* @template T The type of the keys in the target object. | ||
*/ | ||
export 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>; | ||
} | ||
|
||
/** | ||
* Converts an object of arguments to a string of property and event bindings and excludes undefined values. | ||
* Why? Because 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. | ||
* This feels counter-intuitive and is a common source of bugs in stories. | ||
* @example | ||
* ```ts | ||
* // component.ts | ||
*ㅤ@Component({ selector: 'example' }) | ||
* export class ExampleComponent { | ||
* ㅤ@Input() input1: string = 'Default Input1'; | ||
* ㅤ@Input() input2: string = 'Default Input2'; | ||
* ㅤ@Output() click = new EventEmitter(); | ||
* } | ||
* | ||
* // component.stories.ts | ||
* import { argsToTemplate } from '@storybook/angular'; | ||
* export const Input1: Story = { | ||
* render: (args) => ({ | ||
* props: args, | ||
* // Problem1: <example [input1]="input1" [input2]="input2" (click)="click($event)"></example> | ||
* // This will set input2 to undefined and the internal default value will not be used. | ||
* // Problem2: <example [input1]="input1" (click)="click($event)"></example> | ||
* // The default value of input2 will be used, but it is not overridable by the user via controls. | ||
* // Solution: Now the controls will be applicable to both input1 and input2, and the default values will be used if the user does not override them. | ||
* template: `<example ${argsToTemplate(args)}"></example>`, | ||
* }), | ||
* args: { | ||
* // In this Story, we want to set the input1 property, and the internal default property of input2 should be used. | ||
* input1: 'Input 1', | ||
* click: { action: 'clicked' }, | ||
* }, | ||
*}; | ||
* ``` | ||
*/ | ||
export function argsToTemplate<A extends Record<string, any>>( | ||
args: A, | ||
options: ArgsToTemplateOptions<keyof A> = {} | ||
) { | ||
const includeSet = options.include ? new Set(options.include) : null; | ||
const excludeSet = options.exclude ? new Set(options.exclude) : null; | ||
|
||
return Object.entries(args) | ||
.filter(([key]) => args[key] !== undefined) | ||
.filter(([key]) => { | ||
if (includeSet) return includeSet.has(key); | ||
if (excludeSet) return !excludeSet.has(key); | ||
return true; | ||
}) | ||
.map(([key, value]) => | ||
typeof value === 'function' ? `(${key})="${key}($event)"` : `[${key}]="${key}"` | ||
) | ||
.join(' '); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 20 additions & 15 deletions
35
code/frameworks/angular/template/stories/argTypes/doc-button/doc-button.stories.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,29 @@ | ||
import { Args } from '@storybook/angular'; | ||
import { Meta, StoryObj, argsToTemplate } from '@storybook/angular'; | ||
import { DocButtonComponent } from './doc-button.component'; | ||
|
||
export default { | ||
const meta: Meta<DocButtonComponent<any>> = { | ||
component: DocButtonComponent, | ||
}; | ||
|
||
export const Basic = (args: Args) => ({ | ||
props: args, | ||
}); | ||
Basic.args = { label: 'Args test', isDisabled: false }; | ||
Basic.ArgTypes = { | ||
theDefaultValue: { | ||
table: { | ||
defaultValue: { summary: 'Basic default value' }, | ||
export default meta; | ||
|
||
type Story = StoryObj<DocButtonComponent<any>>; | ||
|
||
export const Basic: Story = { | ||
args: { label: 'Args test', isDisabled: false }, | ||
argTypes: { | ||
theDefaultValue: { | ||
table: { | ||
defaultValue: { summary: 'Basic default value' }, | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
export const WithTemplate = (args: Args) => ({ | ||
props: args, | ||
template: '<my-button [label]="label" [appearance]="appearance"></my-button>', | ||
}); | ||
WithTemplate.args = { label: 'Template test', appearance: 'primary' }; | ||
export const WithTemplate: Story = { | ||
args: { label: 'Template test', appearance: 'primary' }, | ||
render: (args) => ({ | ||
props: args, | ||
template: `<my-button ${argsToTemplate(args)}></my-button>`, | ||
}), | ||
}; |
21 changes: 13 additions & 8 deletions
21
code/frameworks/angular/template/stories/argTypes/doc-directive/doc-directive.stories.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,19 @@ | ||
import { Meta, StoryObj } from '@storybook/angular'; | ||
import { DocDirective } from './doc-directive.directive'; | ||
|
||
export default { | ||
const meta: Meta<DocDirective> = { | ||
component: DocDirective, | ||
}; | ||
|
||
const modules = { | ||
declarations: [DocDirective], | ||
}; | ||
export default meta; | ||
|
||
type Story = StoryObj<DocDirective>; | ||
|
||
export const Basic = () => ({ | ||
moduleMetadata: modules, | ||
template: '<div docDirective [hasGrayBackground]="true"><h1>DocDirective</h1></div>', | ||
}); | ||
export const Basic: Story = { | ||
render: () => ({ | ||
moduleMetadata: { | ||
declarations: [DocDirective], | ||
}, | ||
template: '<div docDirective [hasGrayBackground]="true"><h1>DocDirective</h1></div>', | ||
}), | ||
}; |
21 changes: 13 additions & 8 deletions
21
code/frameworks/angular/template/stories/argTypes/doc-injectable/doc-injectable.stories.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,19 @@ | ||
import { Meta, StoryObj } from '@storybook/angular'; | ||
import { DocInjectableService } from './doc-injectable.service'; | ||
|
||
export default { | ||
const meta: Meta<DocInjectableService> = { | ||
component: DocInjectableService, | ||
}; | ||
|
||
const modules = { | ||
provider: [DocInjectableService], | ||
}; | ||
export default meta; | ||
|
||
type Story = StoryObj<DocInjectableService>; | ||
|
||
export const Basic = () => ({ | ||
moduleMetadata: modules, | ||
template: '<div><h1>DocInjectable</h1></div>', | ||
}); | ||
export const Basic: Story = { | ||
render: () => ({ | ||
moduleMetadata: { | ||
providers: [DocInjectableService], | ||
}, | ||
template: '<div><h1>DocInjectable</h1></div>', | ||
}), | ||
}; |
21 changes: 13 additions & 8 deletions
21
code/frameworks/angular/template/stories/argTypes/doc-pipe/doc-pipe.stories.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,19 @@ | ||
import { Meta, StoryObj } from '@storybook/angular'; | ||
import { DocPipe } from './doc-pipe.pipe'; | ||
|
||
export default { | ||
const meta: Meta<DocPipe> = { | ||
component: DocPipe, | ||
}; | ||
|
||
const modules = { | ||
declarations: [DocPipe], | ||
}; | ||
export default meta; | ||
|
||
type Story = StoryObj<DocPipe>; | ||
|
||
export const Basic = () => ({ | ||
moduleMetadata: modules, | ||
template: `<div><h1>{{ 'DocPipe' | docPipe }}</h1></div>`, | ||
}); | ||
export const Basic: Story = { | ||
render: () => ({ | ||
moduleMetadata: { | ||
declarations: [DocPipe], | ||
}, | ||
template: `<div><h1>{{ 'DocPipe' | docPipe }}</h1></div>`, | ||
}), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 7 additions & 2 deletions
9
.../stories/basics/component-with-complex-selectors/attribute-selectors.component.stories.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,13 @@ | ||
import { Meta, StoryObj } from '@storybook/angular'; | ||
import { AttributeSelectorComponent } from './attribute-selector.component'; | ||
|
||
export default { | ||
const meta: Meta<AttributeSelectorComponent> = { | ||
// title: 'Basics / Component / With Complex Selectors', | ||
component: AttributeSelectorComponent, | ||
}; | ||
|
||
export const AttributeSelectors = {}; | ||
export default meta; | ||
|
||
type Story = StoryObj<AttributeSelectorComponent>; | ||
|
||
export const AttributeSelectors: Story = {}; |
Oops, something went wrong.