Skip to content

Commit 11f028d

Browse files
authored
Feature/required inputs (#673)
* Update output-angular.ts Adapt the component arguments to pass information on required inputs. * Update generate-angular-component.ts Processing of required inputs. Keep the order, but instead of specify only name, define it as object with required property set to true. * Removed unused code and fixed the syntax * Add type for component inputs in Angular * Use ComponentInputProperty instead of two arrays with input property names. * Adapt the argument for the createAngularComponentDefinition * Use name field instead of object so correct text is put in proxy. + prettier * Fix ordering the properties, as .sort() do not provide any order for objects when used without compare callback. + prettier * Update proxies.ts after with build. * Move mapPropName function to utils to avoid code duplication. * Use mapPropName from utils * Do not use require in @ProxyCmp, it require string array. * Update generate-angular-component.test.ts with new parameter type and tests for required inputs. * Update proxies.ts - remove new syntax from @ProxyCmp. * Update types.ts - remove last new line
1 parent 3d43687 commit 11f028d

File tree

6 files changed

+97
-16
lines changed

6 files changed

+97
-16
lines changed

example-project/component-library-angular/projects/library/src/directives/proxies.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ export declare interface MyListScoped extends Components.MyListScoped {}
448448
changeDetection: ChangeDetectionStrategy.OnPush,
449449
template: '<ng-content></ng-content>',
450450
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
451-
inputs: ['animated', 'backdropDismiss', 'component', 'componentProps', 'cssClass', 'event', 'keyboardClose', 'mode', 'showBackdrop', 'translucent'],
451+
inputs: ['animated', 'backdropDismiss', { name: 'component', required: true }, 'componentProps', 'cssClass', 'event', 'keyboardClose', 'mode', 'showBackdrop', 'translucent'],
452452
})
453453
export class MyPopover {
454454
protected el: HTMLMyPopoverElement;

packages/angular/src/generate-angular-component.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { CompilerJsDoc, ComponentCompilerEvent, ComponentCompilerProperty } from '@stencil/core/internal';
22

3-
import { createComponentEventTypeImports, dashToPascalCase, formatToQuotedList } from './utils';
4-
import type { OutputType } from './types';
3+
import { createComponentEventTypeImports, dashToPascalCase, formatToQuotedList, mapPropName } from './utils';
4+
import type { ComponentInputProperty, OutputType } from './types';
55

66
/**
77
* Creates a property declaration.
@@ -33,11 +33,30 @@ function createPropertyDeclaration(
3333
}
3434
}
3535

36+
/**
37+
* Creates a formatted inputs text with required declaration.
38+
*
39+
* @param prop A ComponentCompilerEvent or ComponentCompilerProperty to turn into a property declaration.
40+
* @param inputs The inputs of the Stencil component (e.g. [{name: 'myInput', required: true]).
41+
* @returns The inputs list declaration as a string.
42+
*/
43+
function formatInputs(inputs: readonly ComponentInputProperty[]): string {
44+
return inputs
45+
.map((item) => {
46+
if (item.required) {
47+
return `{ name: '${item.name}', required: true }`;
48+
} else {
49+
return `'${item.name}'`;
50+
}
51+
})
52+
.join(', ');
53+
}
54+
3655
/**
3756
* Creates an Angular component declaration from formatted Stencil compiler metadata.
3857
*
3958
* @param tagName The tag name of the component.
40-
* @param inputs The inputs of the Stencil component (e.g. ['myInput']).
59+
* @param inputs The inputs of the Stencil component (e.g. [{name: 'myInput', required: true]).
4160
* @param outputs The outputs/events of the Stencil component. (e.g. ['myOutput']).
4261
* @param methods The methods of the Stencil component. (e.g. ['myMethod']).
4362
* @param includeImportCustomElements Whether to define the component as a custom element.
@@ -47,7 +66,7 @@ function createPropertyDeclaration(
4766
*/
4867
export const createAngularComponentDefinition = (
4968
tagName: string,
50-
inputs: readonly string[],
69+
inputs: readonly ComponentInputProperty[],
5170
outputs: readonly string[],
5271
methods: readonly string[],
5372
includeImportCustomElements = false,
@@ -61,7 +80,10 @@ export const createAngularComponentDefinition = (
6180
const hasMethods = methods.length > 0;
6281

6382
// Formats the input strings into comma separated, single quoted values.
64-
const formattedInputs = formatToQuotedList(inputs);
83+
const proxyCmpFormattedInputs = formatToQuotedList(inputs.map(mapPropName));
84+
// Formats the input strings into comma separated, single quoted values if optional.
85+
// Formats the required input strings into comma separated {name, required} objects.
86+
const formattedInputs = formatInputs(inputs);
6587
// Formats the output strings into comma separated, single quoted values.
6688
const formattedOutputs = formatToQuotedList(outputs);
6789
// Formats the method strings into comma separated, single quoted values.
@@ -76,7 +98,7 @@ export const createAngularComponentDefinition = (
7698
}
7799

78100
if (hasInputs) {
79-
proxyCmpOptions.push(`\n inputs: [${formattedInputs}]`);
101+
proxyCmpOptions.push(`\n inputs: [${proxyCmpFormattedInputs}]`);
80102
}
81103

82104
if (hasMethods) {

packages/angular/src/output-angular.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import path from 'path';
22
import type { CompilerCtx, ComponentCompilerMeta, ComponentCompilerProperty, Config } from '@stencil/core/internal';
3-
import type { OutputTargetAngular, PackageJSON } from './types';
3+
import type { ComponentInputProperty, OutputTargetAngular, PackageJSON } from './types';
44
import {
55
relativeImport,
66
normalizePath,
@@ -10,6 +10,7 @@ import {
1010
createImportStatement,
1111
isOutputTypeCustomElementsBuild,
1212
OutputTypes,
13+
mapPropName,
1314
} from './utils';
1415
import { createAngularComponentDefinition, createComponentTypeDefinition } from './generate-angular-component';
1516
import { generateAngularDirectivesFile } from './generate-angular-directives-file';
@@ -144,7 +145,12 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
144145
const proxyFileOutput = [];
145146

146147
const filterInternalProps = (prop: { name: string; internal: boolean }) => !prop.internal;
147-
const mapPropName = (prop: { name: string }) => prop.name;
148+
149+
// Ensure that virtual properties has required as false.
150+
const mapInputProp = (prop: { name: string; required?: boolean }) => ({
151+
name: prop.name,
152+
required: prop.required ?? false,
153+
});
148154

149155
const { componentCorePackage, customElementsDir } = outputTarget;
150156

@@ -157,13 +163,13 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
157163
internalProps.push(...cmpMeta.properties.filter(filterInternalProps));
158164
}
159165

160-
const inputs = internalProps.map(mapPropName);
166+
const inputs = internalProps.map(mapInputProp);
161167

162168
if (cmpMeta.virtualProperties) {
163-
inputs.push(...cmpMeta.virtualProperties.map(mapPropName));
169+
inputs.push(...cmpMeta.virtualProperties.map(mapInputProp));
164170
}
165171

166-
inputs.sort();
172+
const orderedInputs = sortBy(inputs, (cip: ComponentInputProperty) => cip.name);
167173

168174
const outputs: string[] = [];
169175

@@ -187,7 +193,7 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
187193
*/
188194
const componentDefinition = createAngularComponentDefinition(
189195
cmpMeta.tagName,
190-
inputs,
196+
orderedInputs,
191197
outputs,
192198
methods,
193199
isCustomElementsBuild,

packages/angular/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,8 @@ export interface ValueAccessorConfig {
4848
export interface PackageJSON {
4949
types: string;
5050
}
51+
52+
export interface ComponentInputProperty {
53+
name: string;
54+
required: boolean;
55+
}

packages/angular/src/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export const OutputTypes: { [key: string]: OutputType } = {
1010

1111
export const toLowerCase = (str: string) => str.toLowerCase();
1212

13+
export const mapPropName = (prop: { name: string }) => prop.name;
14+
1315
export const dashToPascalCase = (str: string) =>
1416
toLowerCase(str)
1517
.split('-')

packages/angular/tests/generate-angular-component.test.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class MyComponent {
2727
});
2828

2929
it('generates a component with inputs', () => {
30-
const component = createAngularComponentDefinition('my-component', ['my-input', 'my-other-input'], [], [], false);
30+
const component = createAngularComponentDefinition('my-component', [{name: 'my-input', required: false}, {name: 'my-other-input', required: false}], [], [], false);
3131
expect(component).toMatch(`@ProxyCmp({
3232
inputs: ['my-input', 'my-other-input']
3333
})
@@ -48,6 +48,28 @@ export class MyComponent {
4848
}`);
4949
});
5050

51+
it('generates a component with inputs (required)', () => {
52+
const component = createAngularComponentDefinition('my-component', [{name: 'my-input', required: false}, {name: 'my-other-input', required: true}], [], [], false);
53+
expect(component).toMatch(`@ProxyCmp({
54+
inputs: ['my-input', 'my-other-input']
55+
})
56+
@Component({
57+
selector: 'my-component',
58+
changeDetection: ChangeDetectionStrategy.OnPush,
59+
template: '<ng-content></ng-content>',
60+
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
61+
inputs: ['my-input', { name: 'my-other-input', required: true }],
62+
standalone: false
63+
})
64+
export class MyComponent {
65+
protected el: HTMLMyComponentElement;
66+
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
67+
c.detach();
68+
this.el = r.nativeElement;
69+
}
70+
}`);
71+
});
72+
5173
it('generates a component with outputs', () => {
5274
const component = createAngularComponentDefinition(
5375
'my-component',
@@ -126,7 +148,7 @@ export class MyComponent {
126148
});
127149

128150
it('generates a component with inputs', () => {
129-
const component = createAngularComponentDefinition('my-component', ['my-input', 'my-other-input'], [], [], true);
151+
const component = createAngularComponentDefinition('my-component', [{name: 'my-input', required: false}, {name: 'my-other-input', required: false}], [], [], true);
130152

131153
expect(component).toEqual(`@ProxyCmp({
132154
defineCustomElementFn: defineMyComponent,
@@ -149,6 +171,30 @@ export class MyComponent {
149171
}`);
150172
});
151173

174+
it('generates a component with inputs (required)', () => {
175+
const component = createAngularComponentDefinition('my-component', [{name: 'my-input', required: true}, {name: 'my-other-input', required: false}], [], [], true);
176+
177+
expect(component).toEqual(`@ProxyCmp({
178+
defineCustomElementFn: defineMyComponent,
179+
inputs: ['my-input', 'my-other-input']
180+
})
181+
@Component({
182+
selector: 'my-component',
183+
changeDetection: ChangeDetectionStrategy.OnPush,
184+
template: '<ng-content></ng-content>',
185+
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
186+
inputs: [{ name: 'my-input', required: true }, 'my-other-input'],
187+
standalone: false
188+
})
189+
export class MyComponent {
190+
protected el: HTMLMyComponentElement;
191+
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
192+
c.detach();
193+
this.el = r.nativeElement;
194+
}
195+
}`);
196+
});
197+
152198
it('generates a component with outputs', () => {
153199
const component = createAngularComponentDefinition(
154200
'my-component',
@@ -227,7 +273,7 @@ export class MyComponent {
227273

228274
describe('inline members', () => {
229275
it('generates component with inlined member with jsDoc', () => {
230-
const component = createAngularComponentDefinition('my-component', ['myMember'], [], [], false, false, [
276+
const component = createAngularComponentDefinition('my-component', [{name: 'myMember', required: false}], [], [], false, false, [
231277
{
232278
docs: {
233279
tags: [{ name: 'deprecated', text: 'use v2 of this API' }],

0 commit comments

Comments
 (0)