Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Angular: Fix template props not able to use dot notation #28588

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,43 @@
import { Component } from '@angular/core';
import { ArgTypes } from 'storybook/internal/types';
import { describe, it, expect } from 'vitest';
import { computesTemplateSourceFromComponent } from './ComputesTemplateFromComponent';
import {
computesTemplateFromComponent,
computesTemplateSourceFromComponent,
} from './ComputesTemplateFromComponent';
import { ISomeInterface, ButtonAccent, InputComponent } from './__testfixtures__/input.component';

describe('angular template decorator', () => {
it('with props should generate tag with properties', () => {
const component = InputComponent;
const props = {
isDisabled: true,
label: 'Hello world',
accent: ButtonAccent.High,
counter: 4,
'aria-label': 'Hello world',
};
const source = computesTemplateFromComponent(component, props);
expect(source).toEqual(
`<doc-button [counter]="counter" [accent]="accent" [isDisabled]="isDisabled" [label]="label" [aria-label]="this['aria-label']"></doc-button>`
);
});

it('with props should generate tag with outputs', () => {
const component = InputComponent;
const props = {
isDisabled: true,
label: 'Hello world',
onClick: ($event: any) => {},
'dash-out': ($event: any) => {},
};
const source = computesTemplateFromComponent(component, props);
expect(source).toEqual(
`<doc-button [isDisabled]="isDisabled" [label]="label" (onClick)="onClick($event)" (dash-out)="this['dash-out']($event)"></doc-button>`
);
});
});

describe('angular source decorator', () => {
it('With no props should generate simple tag', () => {
const component = InputComponent;
Expand Down Expand Up @@ -264,18 +298,20 @@ describe('angular source decorator', () => {
const source = computesTemplateSourceFromComponent(component, props, argTypes);
expect(source).toEqual(`<doc-button></doc-button>`);
});

it('With props should generate tag with properties', () => {
const component = InputComponent;
const props = {
isDisabled: true,
label: 'Hello world',
accent: ButtonAccent.High,
counter: 4,
'aria-label': 'Hello world',
};
const argTypes: ArgTypes = {};
const source = computesTemplateSourceFromComponent(component, props, argTypes);
expect(source).toEqual(
`<doc-button [counter]="4" [accent]="'High'" [isDisabled]="true" [label]="'Hello world'"></doc-button>`
`<doc-button [counter]="4" [accent]="'High'" [isDisabled]="true" [label]="'Hello world'" [aria-label]="'Hello world'"></doc-button>`
);
});

Expand All @@ -285,11 +321,12 @@ describe('angular source decorator', () => {
isDisabled: true,
label: 'Hello world',
onClick: ($event: any) => {},
'dash-out': ($event: any) => {},
};
const argTypes: ArgTypes = {};
const source = computesTemplateSourceFromComponent(component, props, argTypes);
expect(source).toEqual(
`<doc-button [isDisabled]="true" [label]="'Hello world'" (onClick)="onClick($event)"></doc-button>`
`<doc-button [isDisabled]="true" [label]="'Hello world'" (onClick)="onClick($event)" (dash-out)="this['dash-out']($event)"></doc-button>`
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ import {
getComponentInputsOutputs,
} from './utils/NgComponentAnalyzer';

/**
* Check if the name matches the criteria for a valid identifier.
* A valid identifier can only contain letters, digits, underscores, or dollar signs.
* It cannot start with a digit.
*/
const isValidIdentifier = (name: string): boolean => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);

/**
* Returns the property name, if it can be accessed with dot notation. If not,
* it returns `this['propertyName']`.
*/
export const formatPropInTemplate = (propertyName: string) =>
isValidIdentifier(propertyName) ? propertyName : `this['${propertyName}']`;

const separateInputsOutputsAttributes = (
ngComponentInputsOutputs: ComponentInputsOutputs,
props: ICollection = {}
Expand Down Expand Up @@ -50,10 +64,12 @@ export const computesTemplateFromComponent = (
);

const templateInputs =
initialInputs.length > 0 ? ` ${initialInputs.map((i) => `[${i}]="${i}"`).join(' ')}` : '';
initialInputs.length > 0
? ` ${initialInputs.map((i) => `[${i}]="${formatPropInTemplate(i)}"`).join(' ')}`
: '';
const templateOutputs =
initialOutputs.length > 0
? ` ${initialOutputs.map((i) => `(${i})="${i}($event)"`).join(' ')}`
? ` ${initialOutputs.map((i) => `(${i})="${formatPropInTemplate(i)}($event)"`).join(' ')}`
: '';

return buildTemplate(
Expand Down Expand Up @@ -137,7 +153,7 @@ export const computesTemplateSourceFromComponent = (
: '';
const templateOutputs =
initialOutputs.length > 0
? ` ${initialOutputs.map((i) => `(${i})="${i}($event)"`).join(' ')}`
? ` ${initialOutputs.map((i) => `(${i})="${formatPropInTemplate(i)}($event)"`).join(' ')}`
: '';

return buildTemplate(ngComponentMetadata.selector, '', templateInputs, templateOutputs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,13 @@ export class InputComponent<T> {
@Input()
public label: string;

@Input('aria-label') public ariaLabel: string;

/** Specifies some arbitrary object */
@Input() public someDataObject: ISomeInterface;

@Output()
public onClick = new EventEmitter<Event>();

@Output('dash-out') public dashOut = new EventEmitter<any>();
}
6 changes: 6 additions & 0 deletions code/frameworks/angular/src/client/argsToTemplate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,10 @@ describe('argsToTemplate', () => {
const result = argsToTemplate(args, {});
expect(result).toEqual('[input]="input" (event1)="event1($event)"');
});

it('should format for non dot notation', () => {
const args = { 'non-dot': 'Value1', 'dash-out': () => {} };
const result = argsToTemplate(args, {});
expect(result).toEqual('[non-dot]="this[\'non-dot\']" (dash-out)="this[\'dash-out\']($event)"');
});
});
6 changes: 5 additions & 1 deletion code/frameworks/angular/src/client/argsToTemplate.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { formatPropInTemplate } from './angular-beta/ComputesTemplateFromComponent';

/**
* Options for controlling the behavior of the argsToTemplate function.
*
Expand Down Expand Up @@ -68,7 +70,9 @@ export function argsToTemplate<A extends Record<string, any>>(
return true;
})
.map(([key, value]) =>
typeof value === 'function' ? `(${key})="${key}($event)"` : `[${key}]="${key}"`
typeof value === 'function'
? `(${key})="${formatPropInTemplate(key)}($event)"`
: `[${key}]="${formatPropInTemplate(key)}"`
)
.join(' ');
}