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): inline overlay dynamic template data #24521

Merged
merged 1 commit into from
Jan 6, 2022
Merged
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
1 change: 0 additions & 1 deletion angular/src/directives/overlays/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ export class IonModal {
protected el: HTMLElement;

constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be done for inline popovers as well? https://github.com/ionic-team/ionic-framework/blob/main/angular/src/directives/overlays/popover.ts#L107

Additionally, how does this impact change detection with a controller (if it impacts it at all)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, this did affect popovers as well. I've updated the PR to resolve and test both. I've also reworked the commit since this is more about inline overlays and less specific to just modals.

Tested with controllers & attached change detection and things are working as expected. I cannot notice an impact.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering why this call was added in the first place, and traced it back to the inline modal feature PR: https://github.com/ionic-team/ionic-framework/pull/23341/files#diff-d123478d8d877b48ad22b0a74610bcd649b94548e5e5c56a173bb3506d506e88R25 @liamdebeasi Can you think of why this was added originally? I'm wondering if there was some edge case bug this was meant to fix 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c.detach() is what all the other components do, which is why I added it https://github.com/ionic-team/ionic-framework/blob/main/angular/src/directives/proxies.ts#L26

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh... do we have our own change detection set up or something? 🤨

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How we "wrap" our web components into Angular is sorta special. We basically target the same selector tag as our web component, but then use Angular's content projection with ng-content to slot the existing web component inside the wrapping Angular component; since ng-content isn't an actual DOM node this results in ion-modal equalling our web component node.

We then typically detach from all of Angular's change detection, because the updating of the web component should be driven through our implementation in Stencil and independent to anything in Angular. We don't want to cause an unwanted repaint of the web component. We only pass through inputs from our proxy to the web component node, so that the web component can decide when a re-render is necessary.

Overlays are a special condition (inline most specifically), because we are reliant on using ng-template since we are using template outlets (Angular feature). In Ivy, they fixed ng-template to have its own change detection reference to render changes inside just that template context. When we "detach" we are opting out of that behavior. With controller implementations the custom Angular component the implementer creates; would drive the change detection of the template (which wouldn't be detached by default).

For all our other UI components, opting out makes sense. We aren't needing the benefits of functionality that Angular brings with what the web component needs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that's what those are for here! Thanks for the in-depth explanation 👍 If controller modals/popovers still work as expected (i.e. no extra repaints) then this sounds good to me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to note this somewhere either in the Ionic wiki or in the comments in this code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a notion doc for now in our internal documents.

this.el = r.nativeElement;

this.el.addEventListener('willPresent', () => {
Expand Down
1 change: 0 additions & 1 deletion angular/src/directives/overlays/popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ export class IonPopover {
protected el: HTMLElement;

constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;

this.el.addEventListener('willPresent', () => {
Expand Down
20 changes: 20 additions & 0 deletions angular/test/test-app/e2e/src/modal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,23 @@ describe('Modals', () => {
});

});


describe('Modals: Inline', () => {
beforeEach(() => {
cy.visit('/modal-inline');
});

it('should initially have no items', () => {
cy.get('ion-list ion-item').should('not.exist');
});

it('should have items after 1500ms', () => {
cy.wait(1500);

cy.get('ion-list ion-item:nth-child(1)').should('have.text', 'A');
cy.get('ion-list ion-item:nth-child(2)').should('have.text', 'B');
cy.get('ion-list ion-item:nth-child(3)').should('have.text', 'C');
cy.get('ion-list ion-item:nth-child(4)').should('have.text', 'D');
});
});
18 changes: 18 additions & 0 deletions angular/test/test-app/e2e/src/popover.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
describe('Popovers: Inline', () => {
beforeEach(() => {
cy.visit('/popover-inline');
});

it('should initially have no items', () => {
cy.get('ion-list ion-item').should('not.exist');
});

it('should have items after 1500ms', () => {
cy.wait(1500);

cy.get('ion-list ion-item:nth-child(1)').should('have.text', 'A');
cy.get('ion-list ion-item:nth-child(2)').should('have.text', 'B');
cy.get('ion-list ion-item:nth-child(3)').should('have.text', 'C');
cy.get('ion-list ion-item:nth-child(4)').should('have.text', 'D');
});
});
2 changes: 2 additions & 0 deletions angular/test/test-app/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ const routes: Routes = [
{ path: 'inputs', component: InputsComponent },
{ path: 'form', component: FormComponent },
{ path: 'modals', component: ModalComponent },
{ path: 'modal-inline', loadChildren: () => import('./modal-inline').then(m => m.ModalInlineModule) },
{ path: 'view-child', component: ViewChildComponent },
{ path: 'popover-inline', loadChildren: () => import('./popover-inline').then(m => m.PopoverInlineModule) },
{ path: 'providers', component: ProvidersComponent },
{ path: 'router-link', component: RouterLinkComponent },
{ path: 'router-link-page', component: RouterLinkPageComponent },
Expand Down
2 changes: 2 additions & 0 deletions angular/test/test-app/src/app/modal-inline/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './modal-inline.component';
export * from './modal-inline.module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { ModalInlineComponent } from ".";

@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: ModalInlineComponent
}
])
],
exports: [RouterModule]
})
export class ModalInlineRoutingModule { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<ion-modal [isOpen]="true" [breakpoints]="[0.1, 0.5, 1]" [initialBreakpoint]="0.5">
<ng-template>
<ion-content>
<ion-list>
<ion-item *ngFor="let item of items">
<ion-label>{{ item }}</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ng-template>
</ion-modal>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AfterViewInit, Component } from "@angular/core";

/**
* Validates that inline modals will correctly display
* dynamic contents that are updated after the modal is
* display.
*/
@Component({
selector: 'app-modal-inline',
templateUrl: 'modal-inline.component.html'
})
export class ModalInlineComponent implements AfterViewInit {

items: string[] = [];

ngAfterViewInit(): void {
setTimeout(() => {
this.items = ['A', 'B', 'C', 'D'];
}, 1000);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { IonicModule } from "@ionic/angular";
import { ModalInlineRoutingModule } from "./modal-inline-routing.module";
import { ModalInlineComponent } from "./modal-inline.component";

@NgModule({
imports: [CommonModule, IonicModule, ModalInlineRoutingModule],
declarations: [ModalInlineComponent],
exports: [ModalInlineComponent]
})
export class ModalInlineModule { }
1 change: 1 addition & 0 deletions angular/test/test-app/src/app/popover-inline/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './popover-inline.module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { PopoverInlineComponent } from "./popover-inline.component";

@NgModule({
imports: [RouterModule.forChild([
{
path: '',
component: PopoverInlineComponent
}
])],
exports: [RouterModule]
})
export class PopoverInlineRoutingModule { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<ion-popover [isOpen]="true">
<ng-template>
<ion-content>
<ion-list>
<ion-item *ngFor="let item of items">
<ion-label>{{ item }}</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ng-template>
</ion-popover>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { AfterViewInit, Component } from "@angular/core";

/**
* Validates that inline popovers will correctly display
* dynamic contents that are updated after the modal is
* display.
*/
@Component({
selector: 'app-popover-inline',
templateUrl: 'popover-inline.component.html'
})
export class PopoverInlineComponent implements AfterViewInit {

items: string[] = [];

ngAfterViewInit(): void {
setTimeout(() => {
this.items = ['A', 'B', 'C', 'D'];
}, 1000);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { IonicModule } from "@ionic/angular";
import { PopoverInlineRoutingModule } from "./popover-inline-routing.module";
import { PopoverInlineComponent } from "./popover-inline.component";

@NgModule({
imports: [CommonModule, IonicModule, PopoverInlineRoutingModule],
declarations: [PopoverInlineComponent],
})
export class PopoverInlineModule { }