Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(angular): setting props on a signal works #28882

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
* ```
*/
export class NavParams {
constructor(public data: { [key: string]: any } = {}) {}
constructor(public data: { [key: string]: any } = {}) {
console.warn(
`[Ionic Warning]: NavParams has been deprecated in favor of using Angular's input API. Developers should migrate to either the @Input decorator or the Signals-based input API.`
);
}

/**
* Get the value of a nav-parameter for the current view
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export { NavController } from './providers/nav-controller';
export { Config, ConfigToken } from './providers/config';
export { Platform } from './providers/platform';

export { bindLifecycleEvents, AngularDelegate } from './providers/angular-delegate';
export { bindLifecycleEvents, AngularDelegate, AngularDelegateWithSignalsSupport } from './providers/angular-delegate';

export type { IonicWindow } from './types/interfaces';
export type { ViewWillEnter, ViewWillLeave, ViewDidEnter, ViewDidLeave } from './types/ionic-lifecycle-hooks';
Expand Down
63 changes: 59 additions & 4 deletions packages/angular/common/src/providers/angular-delegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@ export class AngularDelegate {
}
}

@Injectable()
export class AngularDelegateWithSignalsSupport {
private zone = inject(NgZone);
private applicationRef = inject(ApplicationRef);

create(
environmentInjector: EnvironmentInjector,
injector: Injector,
elementReferenceKey?: string
): AngularFrameworkDelegate {
return new AngularFrameworkDelegate(
environmentInjector,
injector,
this.applicationRef,
this.zone,
elementReferenceKey,
true
);
}
}

export class AngularFrameworkDelegate implements FrameworkDelegate {
private elRefMap = new WeakMap<HTMLElement, ComponentRef<any>>();
private elEventsMap = new WeakMap<HTMLElement, () => void>();
Expand All @@ -51,7 +72,8 @@ export class AngularFrameworkDelegate implements FrameworkDelegate {
private injector: Injector,
private applicationRef: ApplicationRef,
private zone: NgZone,
private elementReferenceKey?: string
private elementReferenceKey?: string,
private enableSignalsSupport?: boolean
) {}

attachViewToDom(container: any, component: any, params?: any, cssClasses?: string[]): Promise<any> {
Expand Down Expand Up @@ -84,7 +106,8 @@ export class AngularFrameworkDelegate implements FrameworkDelegate {
component,
componentProps,
cssClasses,
this.elementReferenceKey
this.elementReferenceKey,
this.enableSignalsSupport
);
resolve(el);
});
Expand Down Expand Up @@ -121,7 +144,8 @@ export const attachView = (
component: any,
params: any,
cssClasses: string[] | undefined,
elementReferenceKey: string | undefined
elementReferenceKey: string | undefined,
enableSignalsSupport: boolean | undefined
): any => {
/**
* Wraps the injector with a custom injector that
Expand Down Expand Up @@ -164,7 +188,38 @@ export const attachView = (
);
}

Object.assign(instance, params);
/**
* Angular 14.1 added support for setInput
* so we need to fall back to Object.assign
* for Angular 14.0.
*/
if (enableSignalsSupport === true && componentRef.setInput !== undefined) {
const { modal, popover, ...otherParams } = params;
/**
* Any key/value pairs set in componentProps
* must be set as inputs on the component instance.
*/
for (const key in otherParams) {
componentRef.setInput(key, otherParams[key]);
}

/**
* Using setInput will cause an error when
* setting modal/popover on a component that
* does not define them as an input. For backwards
* compatibility purposes we fall back to using
* Object.assign for these properties.
*/
if (modal !== undefined) {
Object.assign(instance, { modal });
}

if (popover !== undefined) {
Object.assign(instance, { popover });
}
} else {
Object.assign(instance, params);
}
}
if (cssClasses) {
for (const cssClass of cssClasses) {
Expand Down
20 changes: 16 additions & 4 deletions packages/angular/src/ionic-module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { CommonModule, DOCUMENT } from '@angular/common';
import { ModuleWithProviders, APP_INITIALIZER, NgModule, NgZone } from '@angular/core';
import { ConfigToken, AngularDelegate, provideComponentInputBinding } from '@ionic/angular/common';
import {
ConfigToken,
AngularDelegate,
AngularDelegateWithSignalsSupport,
provideComponentInputBinding,
} from '@ionic/angular/common';
import { IonicConfig } from '@ionic/core';

import { appInitialize } from './app-initialize';
Expand Down Expand Up @@ -52,27 +57,34 @@ const DECLARATIONS = [
IonMaxValidator,
];

type OptInAngularFeatures = {
useSetInputAPI: boolean;
};

@NgModule({
declarations: DECLARATIONS,
exports: DECLARATIONS,
providers: [AngularDelegate, ModalController, PopoverController],
providers: [ModalController, PopoverController],
imports: [CommonModule],
})
export class IonicModule {
static forRoot(config?: IonicConfig): ModuleWithProviders<IonicModule> {
static forRoot(config?: IonicConfig & OptInAngularFeatures): ModuleWithProviders<IonicModule> {
const { useSetInputAPI, ...rest } = config || {};

return {
ngModule: IonicModule,
providers: [
{
provide: ConfigToken,
useValue: config,
useValue: rest,
},
{
provide: APP_INITIALIZER,
useFactory: appInitialize,
multi: true,
deps: [ConfigToken, DOCUMENT, NgZone],
},
useSetInputAPI ? AngularDelegateWithSignalsSupport : AngularDelegate,
provideComponentInputBinding(),
],
};
Expand Down
18 changes: 14 additions & 4 deletions packages/angular/standalone/src/providers/ionic-angular.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { DOCUMENT } from '@angular/common';
import { APP_INITIALIZER } from '@angular/core';
import type { Provider } from '@angular/core';
import { AngularDelegate, ConfigToken, provideComponentInputBinding } from '@ionic/angular/common';
import {
AngularDelegate,
AngularDelegateWithSignalsSupport,
ConfigToken,
provideComponentInputBinding,
} from '@ionic/angular/common';
import { initialize } from '@ionic/core/components';
import type { IonicConfig } from '@ionic/core/components';

import { ModalController } from './modal-controller';
import { PopoverController } from './popover-controller';

export const provideIonicAngular = (config?: IonicConfig): Provider[] => {
type OptInAngularFeatures = {
useSetInputAPI: boolean;
};

export const provideIonicAngular = (config?: IonicConfig & OptInAngularFeatures): Provider[] => {
const { useSetInputAPI, ...rest } = config || {};
/**
* TODO FW-4967
* Use makeEnvironmentProviders once Angular 14 support is dropped.
Expand All @@ -17,7 +27,7 @@ export const provideIonicAngular = (config?: IonicConfig): Provider[] => {
return [
{
provide: ConfigToken,
useValue: config,
useValue: rest,
},
{
provide: APP_INITIALIZER,
Expand All @@ -26,7 +36,7 @@ export const provideIonicAngular = (config?: IonicConfig): Provider[] => {
deps: [ConfigToken, DOCUMENT],
},
provideComponentInputBinding(),
AngularDelegate,
useSetInputAPI ? AngularDelegateWithSignalsSupport : AngularDelegate,
ModalController,
PopoverController,
];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { JsonPipe } from "@angular/common";
import { Component } from "@angular/core";
import { Component, Input } from "@angular/core";

import { IonicModule } from "@ionic/angular";

Expand All @@ -23,7 +23,7 @@ let rootParamsException = false;
})
export class NavRootComponent {

params: any;
@Input() params: any = {};

ngOnInit() {
if (this.params === undefined) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { JsonPipe } from "@angular/common";
import { Component } from "@angular/core";
import { Component, Input } from "@angular/core";

import { IonicModule } from "@ionic/angular";

Expand All @@ -23,7 +23,7 @@ let rootParamsException = false;
})
export class NavRootComponent {

params: any;
@Input() params: any;

ngOnInit() {
if (this.params === undefined) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Component, NgZone } from '@angular/core';
import { AlertController } from '@ionic/angular';
import { NavComponent } from '../nav/nav.component';

@Component({
selector: 'app-alert',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<ion-content class="ion-padding">
<h1>Value</h1>
<h2>{{value}}</h2>
<h3>{{valueFromParams}}</h3>
<h3>{{prop}}</h3>
<p>modal is defined: <span id="modalInstance">{{ !!modal }}</span></p>
<p>ngOnInit: <span id="ngOnInit">{{onInit}}</span></p>
<p>ionViewWillEnter: <span id="ionViewWillEnter">{{willEnter}}</span></p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, Input, NgZone, OnInit, Optional } from '@angular/core';
import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { ModalController, NavParams, IonNav, ViewWillLeave, ViewDidEnter, ViewDidLeave } from '@ionic/angular';
import { ModalController, IonNav, ViewWillLeave, ViewDidEnter, ViewDidLeave } from '@ionic/angular';

@Component({
selector: 'app-modal-example',
Expand All @@ -9,12 +9,12 @@ import { ModalController, NavParams, IonNav, ViewWillLeave, ViewDidEnter, ViewDi
export class ModalExampleComponent implements OnInit, ViewWillLeave, ViewDidEnter, ViewWillLeave, ViewDidLeave {

@Input() value?: string;
@Input() prop?: string;

form = new UntypedFormGroup({
select: new UntypedFormControl([])
});

valueFromParams: string;
onInit = 0;
willEnter = 0;
didEnter = 0;
Expand All @@ -25,11 +25,8 @@ export class ModalExampleComponent implements OnInit, ViewWillLeave, ViewDidEnte

constructor(
private modalCtrl: ModalController,
@Optional() public nav: IonNav,
navParams: NavParams
) {
this.valueFromParams = navParams.get('prop');
}
@Optional() public nav: IonNav
) {}

ngOnInit() {
NgZone.assertInAngularZone();
Expand Down
13 changes: 7 additions & 6 deletions packages/angular/test/base/src/app/lazy/nav/nav.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Component } from '@angular/core';
import { Component, Input } from '@angular/core';
import { ModalExampleComponent } from '../modal-example/modal-example.component';
import { NavParams } from '@ionic/angular';

@Component({
selector: 'app-nav',
Expand All @@ -10,11 +9,13 @@ export class NavComponent {
rootPage = ModalExampleComponent;
rootParams: any;

constructor(
params: NavParams
) {
@Input() value?: string;
@Input() prop?: string;

ngOnInit() {
this.rootParams = {
...params.data
value: this.value,
prop: this.prop
};
}
}
Loading