Skip to content

Commit 3b6e631

Browse files
authored
fix(angular): inputs on standalone form controls are reactive (#28434)
Issue number: resolves #28431 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> My previous attempt at fixing #28358 caused inputs to no longer be correctly proxied to the underlying components. This was an attempt to work around an underlying ng-packagr bug (see linked thread for more info). ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> I decided it would be best to continue using `ProxyCmp` (since we know that works) and find an alternative to working around the ng-packagr bug. I spoke with the Angular team, and they recommended pulling the provider into its own object. `forwardRef` is now required since we are referencing the component before it is declared. - Revert 82d6309 - Moves provider to an object to avoid ng-packagr issue - I reverted the proxy e2e tests. These are no longer needed since we are not ejecting from the typical `ProxyCmp` usage anymore. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Dev build: `7.5.3-dev.11698699090.1151d73f` Verified that the issue is fixed with the repro provided in #28431 Also verified that this does not regress the issue described in #28358.
1 parent 89698b3 commit 3b6e631

File tree

21 files changed

+252
-420
lines changed

21 files changed

+252
-420
lines changed

packages/angular/standalone/src/directives/checkbox.ts

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,14 @@ import {
77
HostListener,
88
Injector,
99
NgZone,
10+
forwardRef,
1011
} from '@angular/core';
11-
import type { OnInit } from '@angular/core';
1212
import { NG_VALUE_ACCESSOR } from '@angular/forms';
1313
import { ValueAccessor, setIonicClasses } from '@ionic/angular/common';
1414
import type { CheckboxChangeEventDetail, Components } from '@ionic/core/components';
1515
import { defineCustomElement } from '@ionic/core/components/ion-checkbox.js';
1616

17-
/**
18-
* Value accessor components should not use ProxyCmp
19-
* and should call defineCustomElement and proxyInputs
20-
* manually instead. Using both the @ProxyCmp and @Component
21-
* decorators and useExisting (where useExisting refers to the
22-
* class) causes ng-packagr to output multiple component variables
23-
* which breaks treeshaking.
24-
* For example, the following would be generated:
25-
* let IonCheckbox = IonCheckbox_1 = class IonCheckbox extends ValueAccessor {
26-
* Instead, we want only want the class generated:
27-
* class IonCheckbox extends ValueAccessor {
28-
*/
29-
import { proxyInputs, proxyOutputs } from './angular-component-lib/utils';
17+
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
3018

3119
const CHECKBOX_INPUTS = [
3220
'checked',
@@ -41,40 +29,42 @@ const CHECKBOX_INPUTS = [
4129
'value',
4230
];
4331

32+
/**
33+
* Pulling the provider into an object and using PURE works
34+
* around an ng-packagr issue that causes
35+
* components with multiple decorators and
36+
* a provider to be re-assigned. This re-assignment
37+
* is not supported by Webpack and causes treeshaking
38+
* to not work on these kinds of components.
39+
*/
40+
const accessorProvider = {
41+
provide: NG_VALUE_ACCESSOR,
42+
useExisting: /*@__PURE__*/ forwardRef(() => IonCheckbox),
43+
multi: true,
44+
};
45+
46+
@ProxyCmp({
47+
defineCustomElementFn: defineCustomElement,
48+
inputs: CHECKBOX_INPUTS,
49+
})
4450
@Component({
4551
selector: 'ion-checkbox',
4652
changeDetection: ChangeDetectionStrategy.OnPush,
4753
template: '<ng-content></ng-content>',
4854
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
4955
inputs: CHECKBOX_INPUTS,
50-
providers: [
51-
{
52-
provide: NG_VALUE_ACCESSOR,
53-
useExisting: IonCheckbox,
54-
multi: true,
55-
},
56-
],
56+
providers: [accessorProvider],
5757
standalone: true,
5858
})
59-
export class IonCheckbox extends ValueAccessor implements OnInit {
59+
export class IonCheckbox extends ValueAccessor {
6060
protected el: HTMLElement;
6161
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) {
6262
super(injector, r);
63-
defineCustomElement();
6463
c.detach();
6564
this.el = r.nativeElement;
6665
proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']);
6766
}
6867

69-
ngOnInit(): void {
70-
/**
71-
* Data-bound input properties are set
72-
* by Angular after the constructor, so
73-
* we need to run the proxy in ngOnInit.
74-
*/
75-
proxyInputs(IonCheckbox, CHECKBOX_INPUTS);
76-
}
77-
7868
writeValue(value: boolean): void {
7969
this.elementRef.nativeElement.checked = this.lastValue = value;
8070
setIonicClasses(this.elementRef);

packages/angular/standalone/src/directives/datetime.ts

Lines changed: 23 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,14 @@ import {
77
HostListener,
88
Injector,
99
NgZone,
10+
forwardRef,
1011
} from '@angular/core';
11-
import type { OnInit } from '@angular/core';
1212
import { NG_VALUE_ACCESSOR } from '@angular/forms';
1313
import { ValueAccessor } from '@ionic/angular/common';
1414
import type { DatetimeChangeEventDetail, Components } from '@ionic/core/components';
1515
import { defineCustomElement } from '@ionic/core/components/ion-datetime.js';
1616

17-
/**
18-
* Value accessor components should not use ProxyCmp
19-
* and should call defineCustomElement and proxyInputs
20-
* manually instead. Using both the @ProxyCmp and @Component
21-
* decorators and useExisting (where useExisting refers to the
22-
* class) causes ng-packagr to output multiple component variables
23-
* which breaks treeshaking.
24-
* For example, the following would be generated:
25-
* let IonDatetime = IonDatetime_1 = class IonDatetime extends ValueAccessor {
26-
* Instead, we want only want the class generated:
27-
* class IonDatetime extends ValueAccessor {
28-
*/
29-
import { proxyInputs, proxyMethods, proxyOutputs } from './angular-component-lib/utils';
17+
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
3018

3119
const DATETIME_INPUTS = [
3220
'cancelText',
@@ -61,43 +49,44 @@ const DATETIME_INPUTS = [
6149
'yearValues',
6250
];
6351

64-
const DATETIME_METHODS = ['confirm', 'reset', 'cancel'];
52+
/**
53+
* Pulling the provider into an object and using PURE works
54+
* around an ng-packagr issue that causes
55+
* components with multiple decorators and
56+
* a provider to be re-assigned. This re-assignment
57+
* is not supported by Webpack and causes treeshaking
58+
* to not work on these kinds of components.
59+
60+
*/
61+
const accessorProvider = {
62+
provide: NG_VALUE_ACCESSOR,
63+
useExisting: /*@__PURE__*/ forwardRef(() => IonDatetime),
64+
multi: true,
65+
};
6566

67+
@ProxyCmp({
68+
defineCustomElementFn: defineCustomElement,
69+
inputs: DATETIME_INPUTS,
70+
methods: ['confirm', 'reset', 'cancel'],
71+
})
6672
@Component({
6773
selector: 'ion-datetime',
6874
changeDetection: ChangeDetectionStrategy.OnPush,
6975
template: '<ng-content></ng-content>',
7076
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
7177
inputs: DATETIME_INPUTS,
72-
providers: [
73-
{
74-
provide: NG_VALUE_ACCESSOR,
75-
useExisting: IonDatetime,
76-
multi: true,
77-
},
78-
],
78+
providers: [accessorProvider],
7979
standalone: true,
8080
})
81-
export class IonDatetime extends ValueAccessor implements OnInit {
81+
export class IonDatetime extends ValueAccessor {
8282
protected el: HTMLElement;
8383
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) {
8484
super(injector, r);
85-
defineCustomElement();
8685
c.detach();
8786
this.el = r.nativeElement;
8887
proxyOutputs(this, this.el, ['ionCancel', 'ionChange', 'ionFocus', 'ionBlur']);
8988
}
9089

91-
ngOnInit(): void {
92-
/**
93-
* Data-bound input properties are set
94-
* by Angular after the constructor, so
95-
* we need to run the proxy in ngOnInit.
96-
*/
97-
proxyInputs(IonDatetime, DATETIME_INPUTS);
98-
proxyMethods(IonDatetime, DATETIME_METHODS);
99-
}
100-
10190
@HostListener('ionChange', ['$event.target'])
10291
handleIonChange(el: HTMLIonDatetimeElement): void {
10392
this.handleValueChange(el, el.value);

packages/angular/standalone/src/directives/input.ts

Lines changed: 22 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {
77
HostListener,
88
Injector,
99
NgZone,
10+
forwardRef,
1011
} from '@angular/core';
11-
import type { OnInit } from '@angular/core';
1212
import { NG_VALUE_ACCESSOR } from '@angular/forms';
1313
import { ValueAccessor } from '@ionic/angular/common';
1414
import type {
@@ -18,19 +18,7 @@ import type {
1818
} from '@ionic/core/components';
1919
import { defineCustomElement } from '@ionic/core/components/ion-input.js';
2020

21-
/**
22-
* Value accessor components should not use ProxyCmp
23-
* and should call defineCustomElement and proxyInputs
24-
* manually instead. Using both the @ProxyCmp and @Component
25-
* decorators and useExisting (where useExisting refers to the
26-
* class) causes ng-packagr to output multiple component variables
27-
* which breaks treeshaking.
28-
* For example, the following would be generated:
29-
* let IonInput = IonInput_1 = class IonInput extends ValueAccessor {
30-
* Instead, we want only want the class generated:
31-
* class IonInput extends ValueAccessor {
32-
*/
33-
import { proxyInputs, proxyMethods, proxyOutputs } from './angular-component-lib/utils';
21+
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
3422

3523
const INPUT_INPUTS = [
3624
'accept',
@@ -72,43 +60,43 @@ const INPUT_INPUTS = [
7260
'value',
7361
];
7462

75-
const INPUT_METHODS = ['setFocus', 'getInputElement'];
63+
/**
64+
* Pulling the provider into an object and using PURE works
65+
* around an ng-packagr issue that causes
66+
* components with multiple decorators and
67+
* a provider to be re-assigned. This re-assignment
68+
* is not supported by Webpack and causes treeshaking
69+
* to not work on these kinds of components.
70+
*/
71+
const accessorProvider = {
72+
provide: NG_VALUE_ACCESSOR,
73+
useExisting: /*@__PURE__*/ forwardRef(() => IonInput),
74+
multi: true,
75+
};
7676

77+
@ProxyCmp({
78+
defineCustomElementFn: defineCustomElement,
79+
inputs: INPUT_INPUTS,
80+
methods: ['setFocus', 'getInputElement'],
81+
})
7782
@Component({
7883
selector: 'ion-input',
7984
changeDetection: ChangeDetectionStrategy.OnPush,
8085
template: '<ng-content></ng-content>',
8186
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
8287
inputs: INPUT_INPUTS,
83-
providers: [
84-
{
85-
provide: NG_VALUE_ACCESSOR,
86-
useExisting: IonInput,
87-
multi: true,
88-
},
89-
],
88+
providers: [accessorProvider],
9089
standalone: true,
9190
})
92-
export class IonInput extends ValueAccessor implements OnInit {
91+
export class IonInput extends ValueAccessor {
9392
protected el: HTMLElement;
9493
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) {
9594
super(injector, r);
96-
defineCustomElement();
9795
c.detach();
9896
this.el = r.nativeElement;
9997
proxyOutputs(this, this.el, ['ionInput', 'ionChange', 'ionBlur', 'ionFocus']);
10098
}
10199

102-
ngOnInit(): void {
103-
/**
104-
* Data-bound input properties are set
105-
* by Angular after the constructor, so
106-
* we need to run the proxy in ngOnInit.
107-
*/
108-
proxyInputs(IonInput, INPUT_INPUTS);
109-
proxyMethods(IonInput, INPUT_METHODS);
110-
}
111-
112100
@HostListener('ionInput', ['$event.target'])
113101
handleIonInput(el: HTMLIonInputElement): void {
114102
this.handleValueChange(el, el.value);

packages/angular/standalone/src/directives/radio-group.ts

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,63 +7,53 @@ import {
77
HostListener,
88
Injector,
99
NgZone,
10+
forwardRef,
1011
} from '@angular/core';
11-
import type { OnInit } from '@angular/core';
1212
import { NG_VALUE_ACCESSOR } from '@angular/forms';
1313
import { ValueAccessor } from '@ionic/angular/common';
1414
import type { RadioGroupChangeEventDetail, Components } from '@ionic/core/components';
1515
import { defineCustomElement } from '@ionic/core/components/ion-radio-group.js';
1616

17-
/**
18-
* Value accessor components should not use ProxyCmp
19-
* and should call defineCustomElement and proxyInputs
20-
* manually instead. Using both the @ProxyCmp and @Component
21-
* decorators and useExisting (where useExisting refers to the
22-
* class) causes ng-packagr to output multiple component variables
23-
* which breaks treeshaking.
24-
* For example, the following would be generated:
25-
* let IonRadioGroup = IonRadioGroup_1 = class IonRadioGroup extends ValueAccessor {
26-
* Instead, we want only want the class generated:
27-
* class IonRadioGroup extends ValueAccessor {
28-
*/
29-
import { proxyInputs, proxyOutputs } from './angular-component-lib/utils';
17+
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
3018

3119
const RADIO_GROUP_INPUTS = ['allowEmptySelection', 'name', 'value'];
3220

21+
/**
22+
* Pulling the provider into an object and using PURE works
23+
* around an ng-packagr issue that causes
24+
* components with multiple decorators and
25+
* a provider to be re-assigned. This re-assignment
26+
* is not supported by Webpack and causes treeshaking
27+
* to not work on these kinds of components.
28+
*/
29+
const accessorProvider = {
30+
provide: NG_VALUE_ACCESSOR,
31+
useExisting: /*@__PURE__*/ forwardRef(() => IonRadioGroup),
32+
multi: true,
33+
};
34+
35+
@ProxyCmp({
36+
defineCustomElementFn: defineCustomElement,
37+
inputs: RADIO_GROUP_INPUTS,
38+
})
3339
@Component({
3440
selector: 'ion-radio-group',
3541
changeDetection: ChangeDetectionStrategy.OnPush,
3642
template: '<ng-content></ng-content>',
3743
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
3844
inputs: RADIO_GROUP_INPUTS,
39-
providers: [
40-
{
41-
provide: NG_VALUE_ACCESSOR,
42-
useExisting: IonRadioGroup,
43-
multi: true,
44-
},
45-
],
45+
providers: [accessorProvider],
4646
standalone: true,
4747
})
48-
export class IonRadioGroup extends ValueAccessor implements OnInit {
48+
export class IonRadioGroup extends ValueAccessor {
4949
protected el: HTMLElement;
5050
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) {
5151
super(injector, r);
52-
defineCustomElement();
5352
c.detach();
5453
this.el = r.nativeElement;
5554
proxyOutputs(this, this.el, ['ionChange']);
5655
}
5756

58-
ngOnInit(): void {
59-
/**
60-
* Data-bound input properties are set
61-
* by Angular after the constructor, so
62-
* we need to run the proxy in ngOnInit.
63-
*/
64-
proxyInputs(IonRadioGroup, RADIO_GROUP_INPUTS);
65-
}
66-
6757
@HostListener('ionChange', ['$event.target'])
6858
handleIonChange(el: HTMLIonRadioGroupElement): void {
6959
this.handleValueChange(el, el.value);

0 commit comments

Comments
 (0)