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 e4542e26510c..cc12902b5c05 100644
--- a/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.test.ts
+++ b/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.test.ts
@@ -36,10 +36,279 @@ describe('angular template decorator', () => {
``
);
});
+
+ it('with no props should generate simple tag', () => {
+ const component = InputComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual('');
+ });
+
+ describe('with component without selector', () => {
+ @Component({
+ template: `The content`,
+ })
+ class WithoutSelectorComponent {}
+
+ it('should add component ng-container', async () => {
+ const component = WithoutSelectorComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(``);
+ });
+ });
+
+ describe('with component with attribute selector', () => {
+ @Component({
+ selector: 'doc-button[foo]',
+ template: '',
+ })
+ class WithAttributeComponent {}
+
+ it('should add attribute to template', async () => {
+ const component = WithAttributeComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(``);
+ });
+ });
+
+ describe('with component with attribute and value selector', () => {
+ @Component({
+ selector: 'doc-button[foo="bar"]',
+ template: '',
+ })
+ class WithAttributeValueComponent {}
+
+ it('should add attribute to template', async () => {
+ const component = WithAttributeValueComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(``);
+ });
+ });
+
+ describe('with component with attribute only selector', () => {
+ @Component({
+ selector: '[foo]',
+ template: '',
+ })
+ class WithAttributeOnlyComponent {}
+
+ it('should create a div and add attribute to template', async () => {
+ const component = WithAttributeOnlyComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(`
`);
+ });
+ });
+
+ describe('with component with void element and attribute selector', () => {
+ @Component({
+ selector: 'input[foo]',
+ template: '',
+ })
+ class VoidElementWithAttributeComponent {}
+
+ it('should create without separate closing tag', async () => {
+ const component = VoidElementWithAttributeComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(``);
+ });
+ });
+
+ describe('with component with attribute and value only selector', () => {
+ @Component({
+ selector: '[foo="bar"]',
+ template: '',
+ })
+ class WithAttributeOnlyComponent {}
+
+ it('should create a div and add attribute to template', async () => {
+ const component = WithAttributeOnlyComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(``);
+ });
+ });
+
+ describe('with component with void element, attribute and value only selector', () => {
+ @Component({
+ selector: 'input[foo="bar"]',
+ template: '',
+ })
+ class VoidElementWithAttributeComponent {}
+
+ it('should create and add attribute to template without separate closing tag', async () => {
+ const component = VoidElementWithAttributeComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(``);
+ });
+ });
+
+ describe('with component with class selector', () => {
+ @Component({
+ selector: 'doc-button.foo',
+ template: '',
+ })
+ class WithClassComponent {}
+
+ it('should add class to template', async () => {
+ const component = WithClassComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(``);
+ });
+ });
+
+ describe('with component with class only selector', () => {
+ @Component({
+ selector: '.foo',
+ template: '',
+ })
+ class WithClassComponent {}
+
+ it('should create a div and add attribute to template', async () => {
+ const component = WithClassComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(``);
+ });
+ });
+
+ describe('with component with multiple selectors', () => {
+ @Component({
+ selector: 'doc-button, doc-button2',
+ template: '',
+ })
+ class WithMultipleSelectorsComponent {}
+
+ it('should use the first selector', async () => {
+ const component = WithMultipleSelectorsComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(``);
+ });
+ });
+
+ describe('with component with multiple selectors starting with attribute', () => {
+ @Component({
+ selector: 'doc-button[foo], doc-button2',
+ template: '',
+ })
+ class WithMultipleSelectorsComponent {}
+
+ it('should use the first selector', async () => {
+ const component = WithMultipleSelectorsComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(``);
+ });
+ });
+
+ describe('with component with multiple selectors starting with attribute and value', () => {
+ @Component({
+ selector: 'doc-button[foo="bar"], doc-button2',
+ template: '',
+ })
+ class WithMultipleSelectorsComponent {}
+
+ it('should use the first selector', async () => {
+ const component = WithMultipleSelectorsComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(``);
+ });
+ });
+
+ describe('with component with multiple selectors including 2 attributes and a class', () => {
+ @Component({
+ selector: 'doc-button, button[foo], .button[foo], button[baz]',
+ template: '',
+ })
+ class WithMultipleSelectorsComponent {}
+
+ it('should use the first selector', async () => {
+ const component = WithMultipleSelectorsComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(``);
+ });
+ });
+
+ describe('with component with multiple selectors with line breaks', () => {
+ @Component({
+ selector: `doc-button,
+ doc-button2`,
+ template: '',
+ })
+ class WithMultipleSelectorsComponent {}
+
+ it('should use the first selector', async () => {
+ const component = WithMultipleSelectorsComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(``);
+ });
+ });
+
+ describe('with component with multiple selectors starting with attribute only with line breaks', () => {
+ @Component({
+ selector: `[foo],
+ doc-button2`,
+ template: '',
+ })
+ class WithMultipleSelectorsComponent {}
+
+ it('should use the first selector', async () => {
+ const component = WithMultipleSelectorsComponent;
+ const props = {};
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(``);
+ });
+ });
+
+ it('with props should generate tag with properties', () => {
+ const component = InputComponent;
+ const props = {
+ isDisabled: true,
+ label: 'Hello world',
+ accent: ButtonAccent.High,
+ counter: 4,
+ };
+ 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) => {},
+ };
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(
+ ``
+ );
+ });
+
+ it('should generate correct property for overridden name for Input', () => {
+ const component = InputComponent;
+ const props = {
+ color: '#ffffff',
+ };
+ const source = computesTemplateFromComponent(component, props);
+ expect(source).toEqual(``);
+ });
});
describe('angular source decorator', () => {
- it('With no props should generate simple tag', () => {
+ it('with no props should generate simple tag', () => {
const component = InputComponent;
const props = {};
const argTypes: ArgTypes = {};
@@ -298,8 +567,7 @@ describe('angular source decorator', () => {
const source = computesTemplateSourceFromComponent(component, props, argTypes);
expect(source).toEqual(``);
});
-
- it('With props should generate tag with properties', () => {
+ it('with props should generate tag with properties', () => {
const component = InputComponent;
const props = {
isDisabled: true,
@@ -315,7 +583,7 @@ describe('angular source decorator', () => {
);
});
- it('With props should generate tag with outputs', () => {
+ it('with props should generate tag with outputs', () => {
const component = InputComponent;
const props = {
isDisabled: true,
@@ -342,7 +610,7 @@ describe('angular source decorator', () => {
});
describe('with argTypes (from compodoc)', () => {
- it('Should handle enum as strongly typed enum', () => {
+ it('should handle enum as strongly typed enum', () => {
const component = InputComponent;
const props = {
isDisabled: false,
@@ -372,7 +640,7 @@ describe('angular source decorator', () => {
);
});
- it('Should handle enum without values as string', () => {
+ it('should handle enum without values as string', () => {
const component = InputComponent;
const props = {
isDisabled: false,
@@ -402,7 +670,7 @@ describe('angular source decorator', () => {
);
});
- it('Should handle objects correctly', () => {
+ it('should handle simple object as stringified', () => {
const component = InputComponent;
const someDataObject: ISomeInterface = {
@@ -437,5 +705,42 @@ describe('angular source decorator', () => {
``
);
});
+
+ it('should handle circular object as stringified', () => {
+ const component = InputComponent;
+
+ const someDataObject: ISomeInterface = {
+ one: 'Hello world',
+ two: true,
+ three: [
+ `a string literal with "double quotes"`,
+ `a string literal with 'single quotes'`,
+ 'a single quoted string with "double quotes"',
+ "a double quoted string with 'single quotes'",
+ // eslint-disable-next-line prettier/prettier
+ 'a single quoted string with escaped \'single quotes\'',
+ // eslint-disable-next-line prettier/prettier
+ "a double quoted string with escaped \"double quotes\"",
+
+ `a string literal with \'escaped single quotes\'`,
+
+ `a string literal with \"escaped double quotes\"`,
+ ],
+ };
+ someDataObject.ref = someDataObject;
+
+ const props = {
+ isDisabled: false,
+ label: 'Hello world',
+ someDataObject,
+ };
+
+ const source = computesTemplateSourceFromComponent(component, props, null);
+ // Ideally we should stringify the object, but that could cause the story to break because of unescaped values in the JSON object.
+ // This will have to do for now
+ 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 716b2cf9cb78..9e0d38f17986 100644
--- a/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts
+++ b/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts
@@ -80,6 +80,22 @@ export const computesTemplateFromComponent = (
);
};
+/**
+ * Stringify an object with a placholder in the circular references.
+ */
+function stringifyCircular(obj: any) {
+ const seen = new Set();
+ return JSON.stringify(obj, (key, value) => {
+ if (typeof value === 'object' && value !== null) {
+ if (seen.has(value)) {
+ return '[Circular]';
+ }
+ seen.add(value);
+ }
+ return value;
+ });
+}
+
const createAngularInputProperty = ({
propertyName,
value,
@@ -95,7 +111,7 @@ const createAngularInputProperty = ({
templateValue = `'${value}'`;
break;
case 'object':
- templateValue = JSON.stringify(value)
+ templateValue = stringifyCircular(value)
.replace(/'/g, '\u2019')
.replace(/\\"/g, '\u201D')
.replace(/"([^-"]+)":/g, '$1: ')
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 7cc025a3ae87..b0ae93c94b99 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
@@ -11,6 +11,7 @@ export interface ISomeInterface {
one: string;
two: boolean;
three: any[];
+ ref?: ISomeInterface;
}
@Component({