diff --git a/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.test.ts b/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.test.ts index fca3ee9e37b4..e4542e26510c 100644 --- a/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.test.ts +++ b/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.test.ts @@ -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( + `` + ); + }); + + 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( + `` + ); + }); +}); + describe('angular source decorator', () => { it('With no props should generate simple tag', () => { const component = InputComponent; @@ -264,6 +298,7 @@ describe('angular source decorator', () => { const source = computesTemplateSourceFromComponent(component, props, argTypes); expect(source).toEqual(``); }); + it('With props should generate tag with properties', () => { const component = InputComponent; const props = { @@ -271,11 +306,12 @@ describe('angular source decorator', () => { label: 'Hello world', accent: ButtonAccent.High, counter: 4, + 'aria-label': 'Hello world', }; const argTypes: ArgTypes = {}; const source = computesTemplateSourceFromComponent(component, props, argTypes); expect(source).toEqual( - `` + `` ); }); @@ -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( - `` + `` ); }); diff --git a/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts b/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts index 7c585b8ff489..716b2cf9cb78 100644 --- a/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts +++ b/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts @@ -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 = {} @@ -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( @@ -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); diff --git a/code/frameworks/angular/src/client/angular-beta/__testfixtures__/input.component.ts b/code/frameworks/angular/src/client/angular-beta/__testfixtures__/input.component.ts index abf4205eeaf5..7cc025a3ae87 100644 --- a/code/frameworks/angular/src/client/angular-beta/__testfixtures__/input.component.ts +++ b/code/frameworks/angular/src/client/angular-beta/__testfixtures__/input.component.ts @@ -39,9 +39,13 @@ export class InputComponent { @Input() public label: string; + @Input('aria-label') public ariaLabel: string; + /** Specifies some arbitrary object */ @Input() public someDataObject: ISomeInterface; @Output() public onClick = new EventEmitter(); + + @Output('dash-out') public dashOut = new EventEmitter(); } diff --git a/code/frameworks/angular/src/client/argsToTemplate.test.ts b/code/frameworks/angular/src/client/argsToTemplate.test.ts index b7405aba645a..29a51acb1b9d 100644 --- a/code/frameworks/angular/src/client/argsToTemplate.test.ts +++ b/code/frameworks/angular/src/client/argsToTemplate.test.ts @@ -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)"'); + }); }); diff --git a/code/frameworks/angular/src/client/argsToTemplate.ts b/code/frameworks/angular/src/client/argsToTemplate.ts index 0072aa84743d..5b29b627029f 100644 --- a/code/frameworks/angular/src/client/argsToTemplate.ts +++ b/code/frameworks/angular/src/client/argsToTemplate.ts @@ -1,3 +1,5 @@ +import { formatPropInTemplate } from './angular-beta/ComputesTemplateFromComponent'; + /** * Options for controlling the behavior of the argsToTemplate function. * @@ -68,7 +70,9 @@ export function argsToTemplate>( return true; }) .map(([key, value]) => - typeof value === 'function' ? `(${key})="${key}($event)"` : `[${key}]="${key}"` + typeof value === 'function' + ? `(${key})="${formatPropInTemplate(key)}($event)"` + : `[${key}]="${formatPropInTemplate(key)}"` ) .join(' '); }