@stencil/angular-output-target 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,3 +49,4 @@ export const config: Config = {
49
49
  | `excludeComponents` | An array of tag names to exclude from generating component wrappers for. This is helpful when have a custom framework implementation of a specific component or need to extend the base component wrapper behavior. |
50
50
  | `outputType` | Specifies the type of output to be generated. It can take one of the following values: <br />1. `component`: Generates all the component wrappers to be declared on an Angular module. This option is required for Stencil projects using the `dist` hydrated output.<br /> 2. `scam`: Generates a separate Angular module for each component.<br /> 3. `standalone`: Generates standalone component wrappers.<br /> Both `scam` and `standalone` options are compatible with the `dist-custom-elements` output. <br />Note: Please choose the appropriate `outputType` based on your project's requirements and the desired output structure. Defaults to `component`. |
51
51
  | `customElementsDir` | This is the directory where the custom elements are imported from when using the [Custom Elements Bundle](https://stenciljs.com/docs/custom-elements). Defaults to the `components` directory. Only applies for `outputType: "scam"` or `outputType: "standalone"`. |
52
+ | `inlineProperties` | Experimental. When true, tries to inline the properties of components. This is required to enable Angular Language Service to type-check and show jsdocs when using the components in html-templates. |
@@ -1,4 +1,4 @@
1
- import type { ComponentCompilerEvent } from '@stencil/core/internal';
1
+ import type { ComponentCompilerEvent, ComponentCompilerProperty } from '@stencil/core/internal';
2
2
  import type { OutputType } from './types';
3
3
  /**
4
4
  * Creates an Angular component declaration from formatted Stencil compiler metadata.
@@ -9,9 +9,10 @@ import type { OutputType } from './types';
9
9
  * @param methods The methods of the Stencil component. (e.g. ['myMethod']).
10
10
  * @param includeImportCustomElements Whether to define the component as a custom element.
11
11
  * @param standalone Whether to define the component as a standalone component.
12
+ * @param inlineComponentProps List of properties that should be inlined into the component definition.
12
13
  * @returns The component declaration as a string.
13
14
  */
14
- export declare const createAngularComponentDefinition: (tagName: string, inputs: readonly string[], outputs: readonly string[], methods: readonly string[], includeImportCustomElements?: boolean, standalone?: boolean) => string;
15
+ export declare const createAngularComponentDefinition: (tagName: string, inputs: readonly string[], outputs: readonly string[], methods: readonly string[], includeImportCustomElements?: boolean, standalone?: boolean, inlineComponentProps?: readonly ComponentCompilerProperty[]) => string;
15
16
  /**
16
17
  * Creates the component interface type definition.
17
18
  * @param outputType The output type.
@@ -1,4 +1,22 @@
1
1
  import { createComponentEventTypeImports, dashToPascalCase, formatToQuotedList } from './utils';
2
+ /**
3
+ * Creates a property declaration.
4
+ *
5
+ * @param prop A ComponentCompilerEvent or ComponentCompilerProperty to turn into a property declaration.
6
+ * @param type The name of the type (e.g. 'string')
7
+ * @returns The property declaration as a string.
8
+ */
9
+ function createPropertyDeclaration(prop, type) {
10
+ const comment = createDocComment(prop.docs);
11
+ let eventName = prop.name;
12
+ if (/[-/]/.test(prop.name)) {
13
+ // If a member name includes a dash or a forward slash, we need to wrap it in quotes.
14
+ // https://github.com/ionic-team/stencil-ds-output-targets/issues/212
15
+ eventName = `'${prop.name}'`;
16
+ }
17
+ return `${comment.length > 0 ? ` ${comment}` : ''}
18
+ ${eventName}: ${type};`;
19
+ }
2
20
  /**
3
21
  * Creates an Angular component declaration from formatted Stencil compiler metadata.
4
22
  *
@@ -8,9 +26,10 @@ import { createComponentEventTypeImports, dashToPascalCase, formatToQuotedList }
8
26
  * @param methods The methods of the Stencil component. (e.g. ['myMethod']).
9
27
  * @param includeImportCustomElements Whether to define the component as a custom element.
10
28
  * @param standalone Whether to define the component as a standalone component.
29
+ * @param inlineComponentProps List of properties that should be inlined into the component definition.
11
30
  * @returns The component declaration as a string.
12
31
  */
13
- export const createAngularComponentDefinition = (tagName, inputs, outputs, methods, includeImportCustomElements = false, standalone = false) => {
32
+ export const createAngularComponentDefinition = (tagName, inputs, outputs, methods, includeImportCustomElements = false, standalone = false, inlineComponentProps = []) => {
14
33
  const tagNameAsPascal = dashToPascalCase(tagName);
15
34
  const hasInputs = inputs.length > 0;
16
35
  const hasOutputs = outputs.length > 0;
@@ -36,6 +55,8 @@ export const createAngularComponentDefinition = (tagName, inputs, outputs, metho
36
55
  if (standalone && includeImportCustomElements) {
37
56
  standaloneOption = `\n standalone: true`;
38
57
  }
58
+ const propertyDeclarations = inlineComponentProps.map((m) => createPropertyDeclaration(m, `Components.${tagNameAsPascal}['${m.name}']`));
59
+ const propertiesDeclarationText = ['protected el: HTMLElement;', ...propertyDeclarations].join('\n ');
39
60
  /**
40
61
  * Notes on the generated output:
41
62
  * - We disable @angular-eslint/no-inputs-metadata-property, so that
@@ -52,7 +73,7 @@ export const createAngularComponentDefinition = (tagName, inputs, outputs, metho
52
73
  inputs: [${formattedInputs}],${standaloneOption}
53
74
  })
54
75
  export class ${tagNameAsPascal} {
55
- protected el: HTMLElement;
76
+ ${propertiesDeclarationText}
56
77
  constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
57
78
  c.detach();
58
79
  this.el = r.nativeElement;${hasOutputs
@@ -146,17 +167,7 @@ export const createComponentTypeDefinition = (outputType, tagNameAsPascal, event
146
167
  customElementsDir,
147
168
  outputType,
148
169
  });
149
- const eventTypes = publicEvents.map((event) => {
150
- const comment = createDocComment(event.docs);
151
- let eventName = event.name;
152
- if (/[-/]/.test(event.name)) {
153
- // If an event name includes a dash or a forward slash, we need to wrap it in quotes.
154
- // https://github.com/ionic-team/stencil-ds-output-targets/issues/212
155
- eventName = `'${event.name}'`;
156
- }
157
- return `${comment.length > 0 ? ` ${comment}` : ''}
158
- ${eventName}: EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>;`;
159
- });
170
+ const eventTypes = publicEvents.map((event) => createPropertyDeclaration(event, `EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>`));
160
171
  const interfaceDeclaration = `export declare interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} {`;
161
172
  const typeDefinition = (eventTypeImports.length > 0 ? `${eventTypeImports + '\n\n'}` : '') +
162
173
  `${interfaceDeclaration}${eventTypes.length === 0
package/dist/index.cjs.js CHANGED
@@ -144,6 +144,24 @@ const EXTENDED_PATH_REGEX = /^\\\\\?\\/;
144
144
  const NON_ASCII_REGEX = /[^\x00-\x80]+/;
145
145
  const SLASH_REGEX = /\\/g;
146
146
 
147
+ /**
148
+ * Creates a property declaration.
149
+ *
150
+ * @param prop A ComponentCompilerEvent or ComponentCompilerProperty to turn into a property declaration.
151
+ * @param type The name of the type (e.g. 'string')
152
+ * @returns The property declaration as a string.
153
+ */
154
+ function createPropertyDeclaration(prop, type) {
155
+ const comment = createDocComment(prop.docs);
156
+ let eventName = prop.name;
157
+ if (/[-/]/.test(prop.name)) {
158
+ // If a member name includes a dash or a forward slash, we need to wrap it in quotes.
159
+ // https://github.com/ionic-team/stencil-ds-output-targets/issues/212
160
+ eventName = `'${prop.name}'`;
161
+ }
162
+ return `${comment.length > 0 ? ` ${comment}` : ''}
163
+ ${eventName}: ${type};`;
164
+ }
147
165
  /**
148
166
  * Creates an Angular component declaration from formatted Stencil compiler metadata.
149
167
  *
@@ -153,9 +171,10 @@ const SLASH_REGEX = /\\/g;
153
171
  * @param methods The methods of the Stencil component. (e.g. ['myMethod']).
154
172
  * @param includeImportCustomElements Whether to define the component as a custom element.
155
173
  * @param standalone Whether to define the component as a standalone component.
174
+ * @param inlineComponentProps List of properties that should be inlined into the component definition.
156
175
  * @returns The component declaration as a string.
157
176
  */
158
- const createAngularComponentDefinition = (tagName, inputs, outputs, methods, includeImportCustomElements = false, standalone = false) => {
177
+ const createAngularComponentDefinition = (tagName, inputs, outputs, methods, includeImportCustomElements = false, standalone = false, inlineComponentProps = []) => {
159
178
  const tagNameAsPascal = dashToPascalCase(tagName);
160
179
  const hasInputs = inputs.length > 0;
161
180
  const hasOutputs = outputs.length > 0;
@@ -181,6 +200,8 @@ const createAngularComponentDefinition = (tagName, inputs, outputs, methods, inc
181
200
  if (standalone && includeImportCustomElements) {
182
201
  standaloneOption = `\n standalone: true`;
183
202
  }
203
+ const propertyDeclarations = inlineComponentProps.map((m) => createPropertyDeclaration(m, `Components.${tagNameAsPascal}['${m.name}']`));
204
+ const propertiesDeclarationText = ['protected el: HTMLElement;', ...propertyDeclarations].join('\n ');
184
205
  /**
185
206
  * Notes on the generated output:
186
207
  * - We disable @angular-eslint/no-inputs-metadata-property, so that
@@ -197,7 +218,7 @@ const createAngularComponentDefinition = (tagName, inputs, outputs, methods, inc
197
218
  inputs: [${formattedInputs}],${standaloneOption}
198
219
  })
199
220
  export class ${tagNameAsPascal} {
200
- protected el: HTMLElement;
221
+ ${propertiesDeclarationText}
201
222
  constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
202
223
  c.detach();
203
224
  this.el = r.nativeElement;${hasOutputs
@@ -291,17 +312,7 @@ const createComponentTypeDefinition = (outputType, tagNameAsPascal, events, comp
291
312
  customElementsDir,
292
313
  outputType,
293
314
  });
294
- const eventTypes = publicEvents.map((event) => {
295
- const comment = createDocComment(event.docs);
296
- let eventName = event.name;
297
- if (/[-/]/.test(event.name)) {
298
- // If an event name includes a dash or a forward slash, we need to wrap it in quotes.
299
- // https://github.com/ionic-team/stencil-ds-output-targets/issues/212
300
- eventName = `'${event.name}'`;
301
- }
302
- return `${comment.length > 0 ? ` ${comment}` : ''}
303
- ${eventName}: EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>;`;
304
- });
315
+ const eventTypes = publicEvents.map((event) => createPropertyDeclaration(event, `EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>`));
305
316
  const interfaceDeclaration = `export declare interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} {`;
306
317
  const typeDefinition = (eventTypeImports.length > 0 ? `${eventTypeImports + '\n\n'}` : '') +
307
318
  `${interfaceDeclaration}${eventTypes.length === 0
@@ -504,10 +515,11 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
504
515
  const { componentCorePackage, customElementsDir } = outputTarget;
505
516
  for (let cmpMeta of components) {
506
517
  const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName);
507
- const inputs = [];
518
+ const internalProps = [];
508
519
  if (cmpMeta.properties) {
509
- inputs.push(...cmpMeta.properties.filter(filterInternalProps).map(mapPropName));
520
+ internalProps.push(...cmpMeta.properties.filter(filterInternalProps));
510
521
  }
522
+ const inputs = internalProps.map(mapPropName);
511
523
  if (cmpMeta.virtualProperties) {
512
524
  inputs.push(...cmpMeta.virtualProperties.map(mapPropName));
513
525
  }
@@ -520,13 +532,14 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
520
532
  if (cmpMeta.methods) {
521
533
  methods.push(...cmpMeta.methods.filter(filterInternalProps).map(mapPropName));
522
534
  }
535
+ const inlineComponentProps = outputTarget.inlineProperties ? internalProps : [];
523
536
  /**
524
537
  * For each component, we need to generate:
525
538
  * 1. The @Component decorated class
526
539
  * 2. Optionally the @NgModule decorated class (if includeSingleComponentAngularModules is true)
527
540
  * 3. The component interface (using declaration merging for types).
528
541
  */
529
- const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, inputs, outputs, methods, isCustomElementsBuild, isStandaloneBuild);
542
+ const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, inputs, outputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps);
530
543
  const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName);
531
544
  const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir);
532
545
  proxyFileOutput.push(componentDefinition, '\n');
package/dist/index.js CHANGED
@@ -136,6 +136,24 @@ const EXTENDED_PATH_REGEX = /^\\\\\?\\/;
136
136
  const NON_ASCII_REGEX = /[^\x00-\x80]+/;
137
137
  const SLASH_REGEX = /\\/g;
138
138
 
139
+ /**
140
+ * Creates a property declaration.
141
+ *
142
+ * @param prop A ComponentCompilerEvent or ComponentCompilerProperty to turn into a property declaration.
143
+ * @param type The name of the type (e.g. 'string')
144
+ * @returns The property declaration as a string.
145
+ */
146
+ function createPropertyDeclaration(prop, type) {
147
+ const comment = createDocComment(prop.docs);
148
+ let eventName = prop.name;
149
+ if (/[-/]/.test(prop.name)) {
150
+ // If a member name includes a dash or a forward slash, we need to wrap it in quotes.
151
+ // https://github.com/ionic-team/stencil-ds-output-targets/issues/212
152
+ eventName = `'${prop.name}'`;
153
+ }
154
+ return `${comment.length > 0 ? ` ${comment}` : ''}
155
+ ${eventName}: ${type};`;
156
+ }
139
157
  /**
140
158
  * Creates an Angular component declaration from formatted Stencil compiler metadata.
141
159
  *
@@ -145,9 +163,10 @@ const SLASH_REGEX = /\\/g;
145
163
  * @param methods The methods of the Stencil component. (e.g. ['myMethod']).
146
164
  * @param includeImportCustomElements Whether to define the component as a custom element.
147
165
  * @param standalone Whether to define the component as a standalone component.
166
+ * @param inlineComponentProps List of properties that should be inlined into the component definition.
148
167
  * @returns The component declaration as a string.
149
168
  */
150
- const createAngularComponentDefinition = (tagName, inputs, outputs, methods, includeImportCustomElements = false, standalone = false) => {
169
+ const createAngularComponentDefinition = (tagName, inputs, outputs, methods, includeImportCustomElements = false, standalone = false, inlineComponentProps = []) => {
151
170
  const tagNameAsPascal = dashToPascalCase(tagName);
152
171
  const hasInputs = inputs.length > 0;
153
172
  const hasOutputs = outputs.length > 0;
@@ -173,6 +192,8 @@ const createAngularComponentDefinition = (tagName, inputs, outputs, methods, inc
173
192
  if (standalone && includeImportCustomElements) {
174
193
  standaloneOption = `\n standalone: true`;
175
194
  }
195
+ const propertyDeclarations = inlineComponentProps.map((m) => createPropertyDeclaration(m, `Components.${tagNameAsPascal}['${m.name}']`));
196
+ const propertiesDeclarationText = ['protected el: HTMLElement;', ...propertyDeclarations].join('\n ');
176
197
  /**
177
198
  * Notes on the generated output:
178
199
  * - We disable @angular-eslint/no-inputs-metadata-property, so that
@@ -189,7 +210,7 @@ const createAngularComponentDefinition = (tagName, inputs, outputs, methods, inc
189
210
  inputs: [${formattedInputs}],${standaloneOption}
190
211
  })
191
212
  export class ${tagNameAsPascal} {
192
- protected el: HTMLElement;
213
+ ${propertiesDeclarationText}
193
214
  constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
194
215
  c.detach();
195
216
  this.el = r.nativeElement;${hasOutputs
@@ -283,17 +304,7 @@ const createComponentTypeDefinition = (outputType, tagNameAsPascal, events, comp
283
304
  customElementsDir,
284
305
  outputType,
285
306
  });
286
- const eventTypes = publicEvents.map((event) => {
287
- const comment = createDocComment(event.docs);
288
- let eventName = event.name;
289
- if (/[-/]/.test(event.name)) {
290
- // If an event name includes a dash or a forward slash, we need to wrap it in quotes.
291
- // https://github.com/ionic-team/stencil-ds-output-targets/issues/212
292
- eventName = `'${event.name}'`;
293
- }
294
- return `${comment.length > 0 ? ` ${comment}` : ''}
295
- ${eventName}: EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>;`;
296
- });
307
+ const eventTypes = publicEvents.map((event) => createPropertyDeclaration(event, `EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>`));
297
308
  const interfaceDeclaration = `export declare interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} {`;
298
309
  const typeDefinition = (eventTypeImports.length > 0 ? `${eventTypeImports + '\n\n'}` : '') +
299
310
  `${interfaceDeclaration}${eventTypes.length === 0
@@ -496,10 +507,11 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
496
507
  const { componentCorePackage, customElementsDir } = outputTarget;
497
508
  for (let cmpMeta of components) {
498
509
  const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName);
499
- const inputs = [];
510
+ const internalProps = [];
500
511
  if (cmpMeta.properties) {
501
- inputs.push(...cmpMeta.properties.filter(filterInternalProps).map(mapPropName));
512
+ internalProps.push(...cmpMeta.properties.filter(filterInternalProps));
502
513
  }
514
+ const inputs = internalProps.map(mapPropName);
503
515
  if (cmpMeta.virtualProperties) {
504
516
  inputs.push(...cmpMeta.virtualProperties.map(mapPropName));
505
517
  }
@@ -512,13 +524,14 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
512
524
  if (cmpMeta.methods) {
513
525
  methods.push(...cmpMeta.methods.filter(filterInternalProps).map(mapPropName));
514
526
  }
527
+ const inlineComponentProps = outputTarget.inlineProperties ? internalProps : [];
515
528
  /**
516
529
  * For each component, we need to generate:
517
530
  * 1. The @Component decorated class
518
531
  * 2. Optionally the @NgModule decorated class (if includeSingleComponentAngularModules is true)
519
532
  * 3. The component interface (using declaration merging for types).
520
533
  */
521
- const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, inputs, outputs, methods, isCustomElementsBuild, isStandaloneBuild);
534
+ const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, inputs, outputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps);
522
535
  const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName);
523
536
  const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir);
524
537
  proxyFileOutput.push(componentDefinition, '\n');
@@ -101,10 +101,11 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
101
101
  const { componentCorePackage, customElementsDir } = outputTarget;
102
102
  for (let cmpMeta of components) {
103
103
  const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName);
104
- const inputs = [];
104
+ const internalProps = [];
105
105
  if (cmpMeta.properties) {
106
- inputs.push(...cmpMeta.properties.filter(filterInternalProps).map(mapPropName));
106
+ internalProps.push(...cmpMeta.properties.filter(filterInternalProps));
107
107
  }
108
+ const inputs = internalProps.map(mapPropName);
108
109
  if (cmpMeta.virtualProperties) {
109
110
  inputs.push(...cmpMeta.virtualProperties.map(mapPropName));
110
111
  }
@@ -117,13 +118,14 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
117
118
  if (cmpMeta.methods) {
118
119
  methods.push(...cmpMeta.methods.filter(filterInternalProps).map(mapPropName));
119
120
  }
121
+ const inlineComponentProps = outputTarget.inlineProperties ? internalProps : [];
120
122
  /**
121
123
  * For each component, we need to generate:
122
124
  * 1. The @Component decorated class
123
125
  * 2. Optionally the @NgModule decorated class (if includeSingleComponentAngularModules is true)
124
126
  * 3. The component interface (using declaration merging for types).
125
127
  */
126
- const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, inputs, outputs, methods, isCustomElementsBuild, isStandaloneBuild);
128
+ const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, inputs, outputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps);
127
129
  const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName);
128
130
  const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir);
129
131
  proxyFileOutput.push(componentDefinition, '\n');
package/dist/types.d.ts CHANGED
@@ -27,6 +27,12 @@ export interface OutputTargetAngular {
27
27
  * - `standalone` - Generate a component with the `standalone` flag set to `true`.
28
28
  */
29
29
  outputType?: OutputType;
30
+ /**
31
+ * Experimental (!)
32
+ * When true, tries to inline the properties of components. This is required to enable Angular Language Service
33
+ * to type-check and show jsdocs when using the components in html-templates.
34
+ */
35
+ inlineProperties?: boolean;
30
36
  }
31
37
  export type ValueAccessorTypes = 'text' | 'radio' | 'select' | 'number' | 'boolean';
32
38
  export interface ValueAccessorConfig {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stencil/angular-output-target",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Angular output target for @stencil/core components.",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.js",
@@ -21,7 +21,7 @@ export class BooleanValueAccessor extends ValueAccessor {
21
21
  constructor(el: ElementRef) {
22
22
  super(el);
23
23
  }
24
- writeValue(value: any) {
24
+ override writeValue(value: any) {
25
25
  this.el.nativeElement.checked = this.lastValue = value == null ? false : value;
26
26
  }
27
27
  }
@@ -21,7 +21,7 @@ export class NumericValueAccessor extends ValueAccessor {
21
21
  constructor(el: ElementRef) {
22
22
  super(el);
23
23
  }
24
- registerOnChange(fn: (_: number | null) => void) {
24
+ override registerOnChange(fn: (_: number | null) => void) {
25
25
  super.registerOnChange(value => {
26
26
  fn(value === '' ? null : parseFloat(value));
27
27
  });