diff --git a/packages/angular/standalone/src/directives/checkbox.ts b/packages/angular/standalone/src/directives/checkbox.ts index 26f139f379b..218c0483ee1 100644 --- a/packages/angular/standalone/src/directives/checkbox.ts +++ b/packages/angular/standalone/src/directives/checkbox.ts @@ -8,12 +8,25 @@ import { Injector, NgZone, } from '@angular/core'; +import type { OnInit } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor, setIonicClasses } from '@ionic/angular/common'; import type { CheckboxChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-checkbox.js'; -import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; +/** + * Value accessor components should not use ProxyCmp + * and should call defineCustomElement and proxyInputs + * manually instead. Using both the @ProxyCmp and @Component + * decorators and useExisting (where useExisting refers to the + * class) causes ng-packagr to output multiple component variables + * which breaks treeshaking. + * For example, the following would be generated: + * let IonCheckbox = IonCheckbox_1 = class IonCheckbox extends ValueAccessor { + * Instead, we want only want the class generated: + * class IonCheckbox extends ValueAccessor { + */ +import { proxyInputs, proxyOutputs } from './angular-component-lib/utils'; const CHECKBOX_INPUTS = [ 'checked', @@ -28,10 +41,6 @@ const CHECKBOX_INPUTS = [ 'value', ]; -@ProxyCmp({ - defineCustomElementFn: defineCustomElement, - inputs: CHECKBOX_INPUTS, -}) @Component({ selector: 'ion-checkbox', changeDetection: ChangeDetectionStrategy.OnPush, @@ -47,15 +56,25 @@ const CHECKBOX_INPUTS = [ ], standalone: true, }) -export class IonCheckbox extends ValueAccessor { +export class IonCheckbox extends ValueAccessor implements OnInit { protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { super(injector, r); + defineCustomElement(); c.detach(); this.el = r.nativeElement; proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']); } + ngOnInit(): void { + /** + * Data-bound input properties are set + * by Angular after the constructor, so + * we need to run the proxy in ngOnInit. + */ + proxyInputs(IonCheckbox, CHECKBOX_INPUTS); + } + writeValue(value: boolean): void { this.elementRef.nativeElement.checked = this.lastValue = value; setIonicClasses(this.elementRef); diff --git a/packages/angular/standalone/src/directives/datetime.ts b/packages/angular/standalone/src/directives/datetime.ts index 57068f71b97..4f476d7b4e2 100644 --- a/packages/angular/standalone/src/directives/datetime.ts +++ b/packages/angular/standalone/src/directives/datetime.ts @@ -8,12 +8,25 @@ import { Injector, NgZone, } from '@angular/core'; +import type { OnInit } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor } from '@ionic/angular/common'; import type { DatetimeChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-datetime.js'; -import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; +/** + * Value accessor components should not use ProxyCmp + * and should call defineCustomElement and proxyInputs + * manually instead. Using both the @ProxyCmp and @Component + * decorators and useExisting (where useExisting refers to the + * class) causes ng-packagr to output multiple component variables + * which breaks treeshaking. + * For example, the following would be generated: + * let IonDatetime = IonDatetime_1 = class IonDatetime extends ValueAccessor { + * Instead, we want only want the class generated: + * class IonDatetime extends ValueAccessor { + */ +import { proxyInputs, proxyMethods, proxyOutputs } from './angular-component-lib/utils'; const DATETIME_INPUTS = [ 'cancelText', @@ -48,11 +61,8 @@ const DATETIME_INPUTS = [ 'yearValues', ]; -@ProxyCmp({ - defineCustomElementFn: defineCustomElement, - inputs: DATETIME_INPUTS, - methods: ['confirm', 'reset', 'cancel'], -}) +const DATETIME_METHODS = ['confirm', 'reset', 'cancel']; + @Component({ selector: 'ion-datetime', changeDetection: ChangeDetectionStrategy.OnPush, @@ -68,15 +78,26 @@ const DATETIME_INPUTS = [ ], standalone: true, }) -export class IonDatetime extends ValueAccessor { +export class IonDatetime extends ValueAccessor implements OnInit { protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { super(injector, r); + defineCustomElement(); c.detach(); this.el = r.nativeElement; proxyOutputs(this, this.el, ['ionCancel', 'ionChange', 'ionFocus', 'ionBlur']); } + ngOnInit(): void { + /** + * Data-bound input properties are set + * by Angular after the constructor, so + * we need to run the proxy in ngOnInit. + */ + proxyInputs(IonDatetime, DATETIME_INPUTS); + proxyMethods(IonDatetime, DATETIME_METHODS); + } + @HostListener('ionChange', ['$event.target']) handleIonChange(el: HTMLIonDatetimeElement): void { this.handleValueChange(el, el.value); diff --git a/packages/angular/standalone/src/directives/input.ts b/packages/angular/standalone/src/directives/input.ts index 1fb55006f7e..8fb920eddbb 100644 --- a/packages/angular/standalone/src/directives/input.ts +++ b/packages/angular/standalone/src/directives/input.ts @@ -8,6 +8,7 @@ import { Injector, NgZone, } from '@angular/core'; +import type { OnInit } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor } from '@ionic/angular/common'; import type { @@ -17,7 +18,19 @@ import type { } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-input.js'; -import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; +/** + * Value accessor components should not use ProxyCmp + * and should call defineCustomElement and proxyInputs + * manually instead. Using both the @ProxyCmp and @Component + * decorators and useExisting (where useExisting refers to the + * class) causes ng-packagr to output multiple component variables + * which breaks treeshaking. + * For example, the following would be generated: + * let IonInput = IonInput_1 = class IonInput extends ValueAccessor { + * Instead, we want only want the class generated: + * class IonInput extends ValueAccessor { + */ +import { proxyInputs, proxyMethods, proxyOutputs } from './angular-component-lib/utils'; const INPUT_INPUTS = [ 'accept', @@ -59,11 +72,8 @@ const INPUT_INPUTS = [ 'value', ]; -@ProxyCmp({ - defineCustomElementFn: defineCustomElement, - inputs: INPUT_INPUTS, - methods: ['setFocus', 'getInputElement'], -}) +const INPUT_METHODS = ['setFocus', 'getInputElement']; + @Component({ selector: 'ion-input', changeDetection: ChangeDetectionStrategy.OnPush, @@ -79,15 +89,26 @@ const INPUT_INPUTS = [ ], standalone: true, }) -export class IonInput extends ValueAccessor { +export class IonInput extends ValueAccessor implements OnInit { protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { super(injector, r); + defineCustomElement(); c.detach(); this.el = r.nativeElement; proxyOutputs(this, this.el, ['ionInput', 'ionChange', 'ionBlur', 'ionFocus']); } + ngOnInit(): void { + /** + * Data-bound input properties are set + * by Angular after the constructor, so + * we need to run the proxy in ngOnInit. + */ + proxyInputs(IonInput, INPUT_INPUTS); + proxyMethods(IonInput, INPUT_METHODS); + } + @HostListener('ionInput', ['$event.target']) handleIonInput(el: HTMLIonInputElement): void { this.handleValueChange(el, el.value); diff --git a/packages/angular/standalone/src/directives/radio-group.ts b/packages/angular/standalone/src/directives/radio-group.ts index 998057a5e34..2c6870aa16e 100644 --- a/packages/angular/standalone/src/directives/radio-group.ts +++ b/packages/angular/standalone/src/directives/radio-group.ts @@ -8,19 +8,28 @@ import { Injector, NgZone, } from '@angular/core'; +import type { OnInit } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor } from '@ionic/angular/common'; import type { RadioGroupChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-radio-group.js'; -import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; +/** + * Value accessor components should not use ProxyCmp + * and should call defineCustomElement and proxyInputs + * manually instead. Using both the @ProxyCmp and @Component + * decorators and useExisting (where useExisting refers to the + * class) causes ng-packagr to output multiple component variables + * which breaks treeshaking. + * For example, the following would be generated: + * let IonRadioGroup = IonRadioGroup_1 = class IonRadioGroup extends ValueAccessor { + * Instead, we want only want the class generated: + * class IonRadioGroup extends ValueAccessor { + */ +import { proxyInputs, proxyOutputs } from './angular-component-lib/utils'; const RADIO_GROUP_INPUTS = ['allowEmptySelection', 'name', 'value']; -@ProxyCmp({ - defineCustomElementFn: defineCustomElement, - inputs: RADIO_GROUP_INPUTS, -}) @Component({ selector: 'ion-radio-group', changeDetection: ChangeDetectionStrategy.OnPush, @@ -36,15 +45,25 @@ const RADIO_GROUP_INPUTS = ['allowEmptySelection', 'name', 'value']; ], standalone: true, }) -export class IonRadioGroup extends ValueAccessor { +export class IonRadioGroup extends ValueAccessor implements OnInit { protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { super(injector, r); + defineCustomElement(); c.detach(); this.el = r.nativeElement; proxyOutputs(this, this.el, ['ionChange']); } + ngOnInit(): void { + /** + * Data-bound input properties are set + * by Angular after the constructor, so + * we need to run the proxy in ngOnInit. + */ + proxyInputs(IonRadioGroup, RADIO_GROUP_INPUTS); + } + @HostListener('ionChange', ['$event.target']) handleIonChange(el: HTMLIonRadioGroupElement): void { this.handleValueChange(el, el.value); diff --git a/packages/angular/standalone/src/directives/radio.ts b/packages/angular/standalone/src/directives/radio.ts index 9ad32fadf86..d70ea8d01df 100644 --- a/packages/angular/standalone/src/directives/radio.ts +++ b/packages/angular/standalone/src/directives/radio.ts @@ -8,19 +8,28 @@ import { Injector, NgZone, } from '@angular/core'; +import type { OnInit } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor } from '@ionic/angular/common'; import type { Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-radio.js'; -import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; +/** + * Value accessor components should not use ProxyCmp + * and should call defineCustomElement and proxyInputs + * manually instead. Using both the @ProxyCmp and @Component + * decorators and useExisting (where useExisting refers to the + * class) causes ng-packagr to output multiple component variables + * which breaks treeshaking. + * For example, the following would be generated: + * let IonRadio = IonRadio_1 = class IonRadio extends ValueAccessor { + * Instead, we want only want the class generated: + * class IonRadio extends ValueAccessor { + */ +import { proxyInputs, proxyOutputs } from './angular-component-lib/utils'; const RADIO_INPUTS = ['color', 'disabled', 'justify', 'labelPlacement', 'legacy', 'mode', 'name', 'value']; -@ProxyCmp({ - defineCustomElementFn: defineCustomElement, - inputs: RADIO_INPUTS, -}) @Component({ selector: 'ion-radio', changeDetection: ChangeDetectionStrategy.OnPush, @@ -36,15 +45,25 @@ const RADIO_INPUTS = ['color', 'disabled', 'justify', 'labelPlacement', 'legacy' ], standalone: true, }) -export class IonRadio extends ValueAccessor { +export class IonRadio extends ValueAccessor implements OnInit { protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { super(injector, r); + defineCustomElement(); c.detach(); this.el = r.nativeElement; proxyOutputs(this, this.el, ['ionFocus', 'ionBlur']); } + ngOnInit(): void { + /** + * Data-bound input properties are set + * by Angular after the constructor, so + * we need to run the proxy in ngOnInit. + */ + proxyInputs(IonRadio, RADIO_INPUTS); + } + @HostListener('ionSelect', ['$event.target']) handleIonSelect(el: any): void { /** diff --git a/packages/angular/standalone/src/directives/range.ts b/packages/angular/standalone/src/directives/range.ts index 36aa4c93fca..f77beecbe4b 100644 --- a/packages/angular/standalone/src/directives/range.ts +++ b/packages/angular/standalone/src/directives/range.ts @@ -8,6 +8,7 @@ import { Injector, NgZone, } from '@angular/core'; +import type { OnInit } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor } from '@ionic/angular/common'; import type { @@ -18,7 +19,19 @@ import type { } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-range.js'; -import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; +/** + * Value accessor components should not use ProxyCmp + * and should call defineCustomElement and proxyInputs + * manually instead. Using both the @ProxyCmp and @Component + * decorators and useExisting (where useExisting refers to the + * class) causes ng-packagr to output multiple component variables + * which breaks treeshaking. + * For example, the following would be generated: + * let IonRange = IonRange_1 = class IonRange extends ValueAccessor { + * Instead, we want only want the class generated: + * class IonRange extends ValueAccessor { + */ +import { proxyInputs, proxyOutputs } from './angular-component-lib/utils'; const RANGE_INPUTS = [ 'activeBarStart', @@ -41,10 +54,6 @@ const RANGE_INPUTS = [ 'value', ]; -@ProxyCmp({ - defineCustomElementFn: defineCustomElement, - inputs: RANGE_INPUTS, -}) @Component({ selector: 'ion-range', changeDetection: ChangeDetectionStrategy.OnPush, @@ -60,15 +69,25 @@ const RANGE_INPUTS = [ ], standalone: true, }) -export class IonRange extends ValueAccessor { +export class IonRange extends ValueAccessor implements OnInit { protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { super(injector, r); + defineCustomElement(); c.detach(); this.el = r.nativeElement; proxyOutputs(this, this.el, ['ionChange', 'ionInput', 'ionFocus', 'ionBlur', 'ionKnobMoveStart', 'ionKnobMoveEnd']); } + ngOnInit(): void { + /** + * Data-bound input properties are set + * by Angular after the constructor, so + * we need to run the proxy in ngOnInit. + */ + proxyInputs(IonRange, RANGE_INPUTS); + } + @HostListener('ionChange', ['$event.target']) handleIonChange(el: HTMLIonRangeElement): void { this.handleValueChange(el, el.value); diff --git a/packages/angular/standalone/src/directives/searchbar.ts b/packages/angular/standalone/src/directives/searchbar.ts index 5a4abd6d318..75f1d7da671 100644 --- a/packages/angular/standalone/src/directives/searchbar.ts +++ b/packages/angular/standalone/src/directives/searchbar.ts @@ -8,12 +8,25 @@ import { Injector, NgZone, } from '@angular/core'; +import type { OnInit } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor } from '@ionic/angular/common'; import type { SearchbarInputEventDetail, SearchbarChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-searchbar.js'; -import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; +/** + * Value accessor components should not use ProxyCmp + * and should call defineCustomElement and proxyInputs + * manually instead. Using both the @ProxyCmp and @Component + * decorators and useExisting (where useExisting refers to the + * class) causes ng-packagr to output multiple component variables + * which breaks treeshaking. + * For example, the following would be generated: + * let IonSearchbar = IonSearchbar_1 = class IonSearchbar extends ValueAccessor { + * Instead, we want only want the class generated: + * class IonSearchbar extends ValueAccessor { + */ +import { proxyInputs, proxyMethods, proxyOutputs } from './angular-component-lib/utils'; const SEARCHBAR_INPUTS = [ 'animated', @@ -38,11 +51,8 @@ const SEARCHBAR_INPUTS = [ 'value', ]; -@ProxyCmp({ - defineCustomElementFn: defineCustomElement, - inputs: SEARCHBAR_INPUTS, - methods: ['setFocus', 'getInputElement'], -}) +const SEARCHBAR_METHODS = ['setFocus', 'getInputElement']; + @Component({ selector: 'ion-searchbar', changeDetection: ChangeDetectionStrategy.OnPush, @@ -58,15 +68,26 @@ const SEARCHBAR_INPUTS = [ ], standalone: true, }) -export class IonSearchbar extends ValueAccessor { +export class IonSearchbar extends ValueAccessor implements OnInit { protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { super(injector, r); + defineCustomElement(); c.detach(); this.el = r.nativeElement; proxyOutputs(this, this.el, ['ionInput', 'ionChange', 'ionCancel', 'ionClear', 'ionBlur', 'ionFocus']); } + ngOnInit(): void { + /** + * Data-bound input properties are set + * by Angular after the constructor, so + * we need to run the proxy in ngOnInit. + */ + proxyInputs(IonSearchbar, SEARCHBAR_INPUTS); + proxyMethods(IonSearchbar, SEARCHBAR_METHODS); + } + @HostListener('ionInput', ['$event.target']) handleIonInput(el: HTMLIonSearchbarElement): void { this.handleValueChange(el, el.value); diff --git a/packages/angular/standalone/src/directives/segment.ts b/packages/angular/standalone/src/directives/segment.ts index 760b990d5e5..5d6a16f77c0 100644 --- a/packages/angular/standalone/src/directives/segment.ts +++ b/packages/angular/standalone/src/directives/segment.ts @@ -8,19 +8,28 @@ import { Injector, NgZone, } from '@angular/core'; +import type { OnInit } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor } from '@ionic/angular/common'; import type { SegmentChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-segment.js'; -import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; +/** + * Value accessor components should not use ProxyCmp + * and should call defineCustomElement and proxyInputs + * manually instead. Using both the @ProxyCmp and @Component + * decorators and useExisting (where useExisting refers to the + * class) causes ng-packagr to output multiple component variables + * which breaks treeshaking. + * For example, the following would be generated: + * let IonSegment = IonSegment_1 = class IonSegment extends ValueAccessor { + * Instead, we want only want the class generated: + * class IonSegment extends ValueAccessor { + */ +import { proxyInputs, proxyOutputs } from './angular-component-lib/utils'; const SEGMENT_INPUTS = ['color', 'disabled', 'mode', 'scrollable', 'selectOnFocus', 'swipeGesture', 'value']; -@ProxyCmp({ - defineCustomElementFn: defineCustomElement, - inputs: SEGMENT_INPUTS, -}) @Component({ selector: 'ion-segment', changeDetection: ChangeDetectionStrategy.OnPush, @@ -36,15 +45,25 @@ const SEGMENT_INPUTS = ['color', 'disabled', 'mode', 'scrollable', 'selectOnFocu ], standalone: true, }) -export class IonSegment extends ValueAccessor { +export class IonSegment extends ValueAccessor implements OnInit { protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { super(injector, r); + defineCustomElement(); c.detach(); this.el = r.nativeElement; proxyOutputs(this, this.el, ['ionChange']); } + ngOnInit(): void { + /** + * Data-bound input properties are set + * by Angular after the constructor, so + * we need to run the proxy in ngOnInit. + */ + proxyInputs(IonSegment, SEGMENT_INPUTS); + } + @HostListener('ionChange', ['$event.target']) handleIonChange(el: HTMLIonSegmentElement): void { this.handleValueChange(el, el.value); diff --git a/packages/angular/standalone/src/directives/select.ts b/packages/angular/standalone/src/directives/select.ts index f0d19ce078e..0041f91d863 100644 --- a/packages/angular/standalone/src/directives/select.ts +++ b/packages/angular/standalone/src/directives/select.ts @@ -8,12 +8,25 @@ import { Injector, NgZone, } from '@angular/core'; +import type { OnInit } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor } from '@ionic/angular/common'; import type { SelectChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-select.js'; -import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; +/** + * Value accessor components should not use ProxyCmp + * and should call defineCustomElement and proxyInputs + * manually instead. Using both the @ProxyCmp and @Component + * decorators and useExisting (where useExisting refers to the + * class) causes ng-packagr to output multiple component variables + * which breaks treeshaking. + * For example, the following would be generated: + * let IonSelect = IonSelect_1 = class IonSelect extends ValueAccessor { + * Instead, we want only want the class generated: + * class IonSelect extends ValueAccessor { + */ +import { proxyInputs, proxyMethods, proxyOutputs } from './angular-component-lib/utils'; const SELECT_INPUTS = [ 'cancelText', @@ -39,11 +52,8 @@ const SELECT_INPUTS = [ 'value', ]; -@ProxyCmp({ - defineCustomElementFn: defineCustomElement, - inputs: SELECT_INPUTS, - methods: ['open'], -}) +const SELECT_METHODS = ['open']; + @Component({ selector: 'ion-select', changeDetection: ChangeDetectionStrategy.OnPush, @@ -59,15 +69,26 @@ const SELECT_INPUTS = [ ], standalone: true, }) -export class IonSelect extends ValueAccessor { +export class IonSelect extends ValueAccessor implements OnInit { protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { super(injector, r); + defineCustomElement(); c.detach(); this.el = r.nativeElement; proxyOutputs(this, this.el, ['ionChange', 'ionCancel', 'ionDismiss', 'ionFocus', 'ionBlur']); } + ngOnInit(): void { + /** + * Data-bound input properties are set + * by Angular after the constructor, so + * we need to run the proxy in ngOnInit. + */ + proxyInputs(IonSelect, SELECT_INPUTS); + proxyMethods(IonSelect, SELECT_METHODS); + } + @HostListener('ionChange', ['$event.target']) handleIonChange(el: HTMLIonSelectElement): void { this.handleValueChange(el, el.value); diff --git a/packages/angular/standalone/src/directives/textarea.ts b/packages/angular/standalone/src/directives/textarea.ts index 75239632320..af5d8b49967 100644 --- a/packages/angular/standalone/src/directives/textarea.ts +++ b/packages/angular/standalone/src/directives/textarea.ts @@ -8,12 +8,25 @@ import { Injector, NgZone, } from '@angular/core'; +import type { OnInit } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor } from '@ionic/angular/common'; import type { TextareaChangeEventDetail, TextareaInputEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-textarea.js'; -import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; +/** + * Value accessor components should not use ProxyCmp + * and should call defineCustomElement and proxyInputs + * manually instead. Using both the @ProxyCmp and @Component + * decorators and useExisting (where useExisting refers to the + * class) causes ng-packagr to output multiple component variables + * which breaks treeshaking. + * For example, the following would be generated: + * let IonTextarea = IonTextarea_1 = class IonTextarea extends ValueAccessor { + * Instead, we want only want the class generated: + * class IonTextarea extends ValueAccessor { + */ +import { proxyInputs, proxyMethods, proxyOutputs } from './angular-component-lib/utils'; const TEXTAREA_INPUTS = [ 'autoGrow', @@ -48,11 +61,8 @@ const TEXTAREA_INPUTS = [ 'wrap', ]; -@ProxyCmp({ - defineCustomElementFn: defineCustomElement, - inputs: TEXTAREA_INPUTS, - methods: ['setFocus', 'getInputElement'], -}) +const TEXTAREA_METHODS = ['setFocus', 'getInputElement']; + @Component({ selector: 'ion-textarea', changeDetection: ChangeDetectionStrategy.OnPush, @@ -68,15 +78,26 @@ const TEXTAREA_INPUTS = [ ], standalone: true, }) -export class IonTextarea extends ValueAccessor { +export class IonTextarea extends ValueAccessor implements OnInit { protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { super(injector, r); + defineCustomElement(); c.detach(); this.el = r.nativeElement; proxyOutputs(this, this.el, ['ionChange', 'ionInput', 'ionBlur', 'ionFocus']); } + ngOnInit(): void { + /** + * Data-bound input properties are set + * by Angular after the constructor, so + * we need to run the proxy in ngOnInit. + */ + proxyInputs(IonTextarea, TEXTAREA_INPUTS); + proxyMethods(IonTextarea, TEXTAREA_METHODS); + } + @HostListener('ionInput', ['$event.target']) handleIonInput(el: HTMLIonTextareaElement): void { this.handleValueChange(el, el.value); diff --git a/packages/angular/standalone/src/directives/toggle.ts b/packages/angular/standalone/src/directives/toggle.ts index e1ece5f856e..1736200650f 100644 --- a/packages/angular/standalone/src/directives/toggle.ts +++ b/packages/angular/standalone/src/directives/toggle.ts @@ -8,12 +8,25 @@ import { Injector, NgZone, } from '@angular/core'; +import type { OnInit } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor, setIonicClasses } from '@ionic/angular/common'; import type { ToggleChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-toggle.js'; -import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; +/** + * Value accessor components should not use ProxyCmp + * and should call defineCustomElement and proxyInputs + * manually instead. Using both the @ProxyCmp and @Component + * decorators and useExisting (where useExisting refers to the + * class) causes ng-packagr to output multiple component variables + * which breaks treeshaking. + * For example, the following would be generated: + * let IonToggle = IonToggle_1 = class IonToggle extends ValueAccessor { + * Instead, we want only want the class generated: + * class IonToggle extends ValueAccessor { + */ +import { proxyInputs, proxyOutputs } from './angular-component-lib/utils'; const TOGGLE_INPUTS = [ 'checked', @@ -28,10 +41,6 @@ const TOGGLE_INPUTS = [ 'value', ]; -@ProxyCmp({ - defineCustomElementFn: defineCustomElement, - inputs: TOGGLE_INPUTS, -}) @Component({ selector: 'ion-toggle', changeDetection: ChangeDetectionStrategy.OnPush, @@ -47,15 +56,25 @@ const TOGGLE_INPUTS = [ ], standalone: true, }) -export class IonToggle extends ValueAccessor { +export class IonToggle extends ValueAccessor implements OnInit { protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { super(injector, r); + defineCustomElement(); c.detach(); this.el = r.nativeElement; proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']); } + ngOnInit(): void { + /** + * Data-bound input properties are set + * by Angular after the constructor, so + * we need to run the proxy in ngOnInit. + */ + proxyInputs(IonToggle, TOGGLE_INPUTS); + } + writeValue(value: boolean): void { this.elementRef.nativeElement.checked = this.lastValue = value; setIonicClasses(this.elementRef); diff --git a/packages/angular/test/base/e2e/src/standalone/value-accessors.spec.ts b/packages/angular/test/base/e2e/src/standalone/value-accessors.spec.ts index a8e1c36e90c..1e82274c801 100644 --- a/packages/angular/test/base/e2e/src/standalone/value-accessors.spec.ts +++ b/packages/angular/test/base/e2e/src/standalone/value-accessors.spec.ts @@ -14,6 +14,18 @@ describe('Value Accessors', () => { cy.get('ion-checkbox').should('have.class', 'ion-dirty'); cy.get('ion-checkbox').should('have.class', 'ion-valid'); }); + + it('should proxy inputs on load', () => { + cy.get('ion-checkbox').should('have.prop', 'color', 'danger'); + }); + }); + + describe('Datetime', () => { + beforeEach(() => cy.visit('/standalone/value-accessors/datetime')); + + it('should proxy inputs on load', () => { + cy.get('ion-datetime').should('have.prop', 'color', 'danger'); + }); }); describe('Input', () => { @@ -48,6 +60,10 @@ describe('Value Accessors', () => { cy.get('ion-input[formControlName="inputNumber"]').should('have.class', 'ion-valid'); }); + + it('should proxy inputs on load', () => { + cy.get('ion-input').first().should('have.prop', 'color', 'danger'); + }); }); describe('Radio Group', () => { @@ -63,8 +79,21 @@ describe('Value Accessors', () => { cy.get('ion-radio-group').should('have.class', 'ion-dirty'); cy.get('ion-radio-group').should('have.class', 'ion-valid'); }); + + it('should proxy inputs on load', () => { + cy.get('ion-radio').first().should('have.prop', 'color', 'danger'); + }); + }); + + describe('Range', () => { + beforeEach(() => cy.visit('/standalone/value-accessors/range')); + + it('should proxy inputs on load', () => { + cy.get('ion-range').should('have.prop', 'color', 'danger'); + }); }); + describe('Searchbar', () => { beforeEach(() => cy.visit('/standalone/value-accessors/searchbar')); @@ -80,6 +109,10 @@ describe('Value Accessors', () => { cy.get('ion-searchbar').should('have.class', 'ion-dirty'); cy.get('ion-searchbar').should('have.class', 'ion-valid'); }); + + it('should proxy inputs on load', () => { + cy.get('ion-searchbar').should('have.prop', 'color', 'danger'); + }); }); describe('Segment', () => { @@ -95,6 +128,32 @@ describe('Value Accessors', () => { cy.get('ion-segment').should('have.class', 'ion-dirty'); cy.get('ion-segment').should('have.class', 'ion-valid'); }); + + it('should proxy inputs on load', () => { + cy.get('ion-segment').should('have.prop', 'color', 'danger'); + }); + }); + + describe('Select', () => { + beforeEach(() => cy.visit('/standalone/value-accessors/select')); + + it('should update the form value', () => { + cy.get('#formValue').should('have.text', JSON.stringify({ select: 'bananas' }, null, 2)); + cy.get('ion-select').should('have.class', 'ion-pristine'); + + cy.get('ion-select').click(); + cy.get('ion-popover').should('be.visible'); + + cy.get('ion-popover ion-radio-group ion-radio').first().click(); + + cy.get('#formValue').should('have.text', JSON.stringify({ select: 'apples' }, null, 2)); + cy.get('ion-select').should('have.class', 'ion-dirty'); + cy.get('ion-select').should('have.class', 'ion-valid'); + }); + + it('should proxy inputs on load', () => { + cy.get('ion-select').should('have.prop', 'color', 'danger'); + }); }); describe('Textarea', () => { @@ -112,6 +171,10 @@ describe('Value Accessors', () => { cy.get('ion-textarea').should('have.class', 'ion-dirty'); cy.get('ion-textarea').should('have.class', 'ion-valid'); }); + + it('should proxy inputs on load', () => { + cy.get('ion-textarea').should('have.prop', 'color', 'danger'); + }); }); describe('Toggle', () => { @@ -127,6 +190,10 @@ describe('Value Accessors', () => { cy.get('ion-toggle').should('have.class', 'ion-dirty'); cy.get('ion-toggle').should('have.class', 'ion-valid'); }); + + it('should proxy inputs on load', () => { + cy.get('ion-toggle').should('have.prop', 'color', 'danger'); + }); }); }); diff --git a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts index d5618a3ce89..3e2499ca112 100644 --- a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts +++ b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts @@ -37,6 +37,7 @@ export const routes: Routes = [ { path: 'range', loadComponent: () => import('../value-accessors/range/range.component').then(c => c.RangeComponent) }, { path: 'searchbar', loadComponent: () => import('../value-accessors/searchbar/searchbar.component').then(c => c.SearchbarComponent) }, { path: 'segment', loadComponent: () => import('../value-accessors/segment/segment.component').then(c => c.SegmentComponent) }, + { path: 'select', loadComponent: () => import('../value-accessors/select/select.component').then(c => c.SelectComponent) }, { path: 'textarea', loadComponent: () => import('../value-accessors/textarea/textarea.component').then(c => c.TextareaComponent) }, { path: 'toggle', loadComponent: () => import('../value-accessors/toggle/toggle.component').then(c => c.ToggleComponent) }, { path: '**', redirectTo: 'checkbox' } diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/checkbox/checkbox.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/checkbox/checkbox.component.html index 2921e06b917..b3d621b0aeb 100644 --- a/packages/angular/test/base/src/app/standalone/value-accessors/checkbox/checkbox.component.html +++ b/packages/angular/test/base/src/app/standalone/value-accessors/checkbox/checkbox.component.html @@ -6,6 +6,6 @@

IonCheckbox Value Accessors

- + diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/datetime/datetime.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/datetime/datetime.component.html index 22c36339717..6d682257c88 100644 --- a/packages/angular/test/base/src/app/standalone/value-accessors/datetime/datetime.component.html +++ b/packages/angular/test/base/src/app/standalone/value-accessors/datetime/datetime.component.html @@ -6,6 +6,6 @@

IonDatetime Value Accessors

- + diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/input/input.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/input/input.component.html index e63235cea5a..aa9a2feeff6 100644 --- a/packages/angular/test/base/src/app/standalone/value-accessors/input/input.component.html +++ b/packages/angular/test/base/src/app/standalone/value-accessors/input/input.component.html @@ -5,7 +5,7 @@

IonInput Value Accessors

- + diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/radio-group/radio-group.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/radio-group/radio-group.component.html index a511dbbbe50..274dfd24788 100644 --- a/packages/angular/test/base/src/app/standalone/value-accessors/radio-group/radio-group.component.html +++ b/packages/angular/test/base/src/app/standalone/value-accessors/radio-group/radio-group.component.html @@ -7,7 +7,7 @@

IonRadioGroup Value Accessors

- One + One Two diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/range/range.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/range/range.component.html index 9a496fbcd3c..969f4b13c7f 100644 --- a/packages/angular/test/base/src/app/standalone/value-accessors/range/range.component.html +++ b/packages/angular/test/base/src/app/standalone/value-accessors/range/range.component.html @@ -4,6 +4,6 @@

IonRange Value Accessors

This test checks the form integrations with ion-range to make sure values are correctly assigned to the form group.

- + diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/searchbar/searchbar.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/searchbar/searchbar.component.html index 9a6137c65ba..c667a787d24 100644 --- a/packages/angular/test/base/src/app/standalone/value-accessors/searchbar/searchbar.component.html +++ b/packages/angular/test/base/src/app/standalone/value-accessors/searchbar/searchbar.component.html @@ -5,6 +5,6 @@

IonSearchbar Value Accessors

group.

- + diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/segment/segment.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/segment/segment.component.html index 98016fcde68..fca9dba8bcc 100644 --- a/packages/angular/test/base/src/app/standalone/value-accessors/segment/segment.component.html +++ b/packages/angular/test/base/src/app/standalone/value-accessors/segment/segment.component.html @@ -6,7 +6,7 @@

IonSegment Value Accessors

- + Paid diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/select/select.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/select/select.component.html new file mode 100644 index 00000000000..28dbda038da --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/select/select.component.html @@ -0,0 +1,14 @@ +
+

IonSelect Value Accessors

+

+ This test checks the form integrations with ion-select to make sure values are correctly assigned to the form + group. +

+ + + + Apples + Bananas + + +
diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/select/select.component.ts b/packages/angular/test/base/src/app/standalone/value-accessors/select/select.component.ts new file mode 100644 index 00000000000..35246438f5f --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/select/select.component.ts @@ -0,0 +1,26 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms"; +import { IonSelect, IonSelectOption } from "@ionic/angular/standalone"; +import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component"; + +@Component({ + selector: 'app-select', + templateUrl: 'select.component.html', + standalone: true, + imports: [ + IonSelect, + IonSelectOption, + ReactiveFormsModule, + FormsModule, + ValueAccessorTestComponent + ] +}) +export class SelectComponent { + + form = this.fb.group({ + select: ['bananas', Validators.required], + }); + + constructor(private fb: FormBuilder) { } + +} diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/textarea/textarea.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/textarea/textarea.component.html index 678d42d391a..05ab51817a1 100644 --- a/packages/angular/test/base/src/app/standalone/value-accessors/textarea/textarea.component.html +++ b/packages/angular/test/base/src/app/standalone/value-accessors/textarea/textarea.component.html @@ -6,6 +6,6 @@

IonTextarea Value Accessors

- + diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/toggle/toggle.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/toggle/toggle.component.html index 08888059f99..b7d74781c55 100644 --- a/packages/angular/test/base/src/app/standalone/value-accessors/toggle/toggle.component.html +++ b/packages/angular/test/base/src/app/standalone/value-accessors/toggle/toggle.component.html @@ -5,6 +5,6 @@

IonToggle Value Accessors

- +