Files
ionic-framework/packages/angular/standalone/src/directives/range.ts
Liam DeBeasi 82d6309ef1 fix(angular): remove form control side effects (#28359)
Issue number: resolves #28358

---------

<!-- 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. -->


28f2ec9c62
exposed a (possible) `ng-packagr` bug where the form control components
were being re-assigned, which breaks treeshaking. These components were
considered side effects and were always being pulled into the bundle.

This resulted in a higher than expected bundle size. This issue appears
to be caused by using 2 decorators **and** referring to the class in
`useExisting` (for providers). Doing just one of these does not
reproduce the issue.

The compiled output looks something like this:

```typescript
let IonToggle = IonToggle_1 = /*@__PURE__*/ class IonToggle extends ValueAccessor {
    constructor(c, r, z, injector) {
        super(injector, r);
        this.z = z;
        c.detach();
        this.el = r.nativeElement;
        proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']);
    }
    writeValue(value) {
        this.elementRef.nativeElement.checked = this.lastValue = value;
        setIonicClasses(this.elementRef);
    }
    handleIonChange(el) {
        this.handleValueChange(el, el.checked);
    }
};
/** @nocollapse */ IonToggle.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: IonToggle, deps: [{ token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i0.NgZone }, { token: i0.Injector }], target: i0.ɵɵFactoryTarget.Component });
/** @nocollapse */ IonToggle.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.2.12", type: IonToggle, isStandalone: true, selector: "ion-toggle", inputs: { checked: "checked", color: "color", disabled: "disabled", enableOnOffLabels: "enableOnOffLabels", justify: "justify", labelPlacement: "labelPlacement", legacy: "legacy", mode: "mode", name: "name", value: "value" }, host: { listeners: { "ionChange": "handleIonChange($event.target)" } }, providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: IonToggle_1,
            multi: true,
        },
    ], usesInheritance: true, ngImport: i0, template: '<ng-content></ng-content>', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
IonToggle = IonToggle_1 = __decorate([
    ProxyCmp({
        defineCustomElementFn: defineCustomElement$1i,
        inputs: TOGGLE_INPUTS,
    })
], IonToggle);
```

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Removed the `ProxyCmp` usage in favor of manually calling proxyInputs
and proxyMethods.
-  Also saw that select was missing a form control test, so I added one

The compiled code now looks something like this:

```typescript
class IonToggle extends ValueAccessor {
    constructor(c, r, z, injector) {
        super(injector, r);
        this.z = z;
        defineCustomElement$1i();
        proxyInputs(IonToggle, TOGGLE_INPUTS);
        c.detach();
        this.el = r.nativeElement;
        proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']);
    }
    writeValue(value) {
        this.elementRef.nativeElement.checked = this.lastValue = value;
        setIonicClasses(this.elementRef);
    }
    handleIonChange(el) {
        this.handleValueChange(el, el.checked);
    }
}
/** @nocollapse */ IonToggle.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: IonToggle, deps: [{ token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i0.NgZone }, { token: i0.Injector }], target: i0.ɵɵFactoryTarget.Component });
/** @nocollapse */ IonToggle.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.2.12", type: IonToggle, isStandalone: true, selector: "ion-toggle", inputs: { checked: "checked", color: "color", disabled: "disabled", enableOnOffLabels: "enableOnOffLabels", justify: "justify", labelPlacement: "labelPlacement", legacy: "legacy", mode: "mode", name: "name", value: "value" }, host: { listeners: { "ionChange": "handleIonChange($event.target)" } }, providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: IonToggle,
            multi: true,
        },
    ], usesInheritance: true, ngImport: i0, template: '<ng-content></ng-content>', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
```

## 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. -->

Ryan provided some context on a related Stencil bug where doing
reassignments broke treeshaking in Webpack. While the source of this bug
is not Stencil, understanding the Stencil bug helped me better
understand this issue:

https://github.com/ionic-team/stencil/issues/3191
https://github.com/ionic-team/stencil/pull/3248
https://github.com/ionic-team/stencil/pull/4188 (fixes an issue
introduced in the above stencil PR)

Dev build: `7.5.1-dev.11697480817.10fa2601`
2023-10-18 19:03:20 +00:00

132 lines
3.8 KiB
TypeScript

import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
HostListener,
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 {
RangeChangeEventDetail,
RangeKnobMoveStartEventDetail,
RangeKnobMoveEndEventDetail,
Components,
} from '@ionic/core/components';
import { defineCustomElement } from '@ionic/core/components/ion-range.js';
/**
* 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',
'color',
'debounce',
'disabled',
'dualKnobs',
'label',
'labelPlacement',
'legacy',
'max',
'min',
'mode',
'name',
'pin',
'pinFormatter',
'snaps',
'step',
'ticks',
'value',
];
@Component({
selector: 'ion-range',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: RANGE_INPUTS,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: IonRange,
multi: true,
},
],
standalone: true,
})
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);
}
}
export declare interface IonRange extends Components.IonRange {
/**
* The `ionChange` event is fired for `<ion-range>` elements when the user
modifies the element's value:
- When the user releases the knob after dragging;
- When the user moves the knob with keyboard arrows
`ionChange` is not fired when the value is changed programmatically.
*/
ionChange: EventEmitter<CustomEvent<RangeChangeEventDetail>>;
/**
* The `ionInput` event is fired for `<ion-range>` elements when the value
is modified. Unlike `ionChange`, `ionInput` is fired continuously
while the user is dragging the knob.
*/
ionInput: EventEmitter<CustomEvent<RangeChangeEventDetail>>;
/**
* Emitted when the range has focus.
*/
ionFocus: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the range loses focus.
*/
ionBlur: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the user starts moving the range knob, whether through
mouse drag, touch gesture, or keyboard interaction.
*/
ionKnobMoveStart: EventEmitter<CustomEvent<RangeKnobMoveStartEventDetail>>;
/**
* Emitted when the user finishes moving the range knob, whether through
mouse drag, touch gesture, or keyboard interaction.
*/
ionKnobMoveEnd: EventEmitter<CustomEvent<RangeKnobMoveEndEventDetail>>;
}