Skip to content

Detached loader change detection #274

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

Merged
merged 2 commits into from
Jun 7, 2016
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
31 changes: 27 additions & 4 deletions nativescript-angular/common/detached-loader.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import {ComponentRef, ViewContainerRef, Component, Type, ViewChild, ComponentResolver} from '@angular/core';
import {ComponentRef, ViewContainerRef, Component, Type, ViewChild, ComponentResolver, ChangeDetectorRef, Host} from '@angular/core';
import trace = require("trace");

type AnyComponentRef = ComponentRef<any>;
interface PendingLoadEntry {
componentType: Type;
resolveCallback: (AnyComponentRef) => void;
}

export const CATEGORY = "detached-loader";
function log(message: string) {
trace.write(message, CATEGORY);
}


/**
* Wrapper component used for loading components when navigating
* It uses DetachedContainer as selector so that it is containerRef is not attached to the visual tree.
Expand All @@ -20,10 +27,11 @@ export class DetachedLoader {
private viewLoaded = false;
private pendingLoads: PendingLoadEntry[] = [];

constructor(private compiler: ComponentResolver) {
}
constructor(private compiler: ComponentResolver, private changeDetector: ChangeDetectorRef) { }

public ngAfterViewInit() {
log("DetachedLoader.ngAfterViewInit");

this.viewLoaded = true;
this.pendingLoads.forEach(loadEntry => {
this.loadInLocation(loadEntry.componentType).then(loadedRef => {
Expand All @@ -35,15 +43,30 @@ export class DetachedLoader {
private loadInLocation(componentType: Type): Promise<ComponentRef<any>> {
return this.compiler.resolveComponent(componentType).then((componentFactory) => {
return this.containerRef.createComponent(componentFactory, this.containerRef.length, this.containerRef.parentInjector, null);
});
}).then((compRef) => {
log("DetachedLoader.loadInLocation component loaded -> markForCheck");
// Component is created, buit may not be checked if we are loading
// inside component with OnPush CD strategy. Mark us for check to be sure CD will reach us.
// We are inside a promise here so no need for setTimeout - CD should trigger after the promise.
this.changeDetector.markForCheck();
return compRef;
})
}

public loadComponent(componentType: Type): Promise<ComponentRef<any>> {
log("DetachedLoader.loadComponent viewLoaded: " + this.viewLoaded);

// Check if called before placeholder is initialized.
// Delay load if so.
if (this.viewLoaded) {
return this.loadInLocation(componentType);
} else {
// loadComponent called, but detached-loader is still not initialized.
// Mark it for change and trigger change detection to be sure it will be initialized,
// so that loading can conitionue.
log("DetachedLoader.loadComponent -> markForCheck(with setTimeout())")
setTimeout(() => this.changeDetector.markForCheck(), 0);

return new Promise((resolve, reject) => {
this.pendingLoads.push({
componentType: componentType,
Expand Down
105 changes: 105 additions & 0 deletions tests/app/tests/detached-loader-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//make sure you import mocha-config before @angular/core
import {assert} from "./test-config";
import {Component, ElementRef, Renderer, AfterViewInit, OnInit, ViewChild, ChangeDetectionStrategy} from "@angular/core";
import {ProxyViewContainer} from "ui/proxy-view-container";
import {Red} from "color/known-colors";
import {dumpView} from "./test-utils";
import {TestApp} from "./test-app";
import {LayoutBase} from "ui/layouts/layout-base";
import {StackLayout} from "ui/layouts/stack-layout";
import {DetachedLoader} from "nativescript-angular/common/detached-loader";

@Component({
template: `<StackLayout><Label text="COMPONENT"></Label></StackLayout>`
})
class TestComponent { }


class LoaderComponentBase {
@ViewChild(DetachedLoader) public loader: DetachedLoader;

public ready: Promise<LoaderComponent>;
private resolve;
constructor() {
this.ready = new Promise((reslove, reject) => {
this.resolve = reslove;
})
}
ngAfterViewInit() {
this.resolve(this);
}
}

@Component({
selector: 'loader-component-on-push',
directives: [DetachedLoader],
template: `
<StackLayout>
<DetachedContainer #loader></DetachedContainer>
</StackLayout>
`
})
export class LoaderComponent extends LoaderComponentBase { }

@Component({
selector: 'loader-component-on-push',
directives: [DetachedLoader],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<StackLayout>
<DetachedContainer #loader></DetachedContainer>
</StackLayout>
`
})
export class LoaderComponentOnPush extends LoaderComponentBase { }

describe ('DetachedLoader', () => {
let testApp: TestApp = null;

before(() => {
return TestApp.create().then((app) => {
testApp = app;
})
});

after(() => {
testApp.dispose();
});

afterEach(() => {
testApp.disposeComponents();
});

it("creates component", (done) => {
testApp.loadComponent(LoaderComponent)
.then((componentRef) => {
// wait for the ngAfterViewInit
return (<LoaderComponent>componentRef.instance).ready;
})
.then((comp) => {
// load test component with loader
return comp.loader.loadComponent(TestComponent);
})
.then((compRef) => {
done();
})
.catch(done);
});


it("creates component when ChangeDetectionStrategy is OnPush", (done) => {
testApp.loadComponent(LoaderComponentOnPush)
.then((componentRef) => {
// wait for the ngAfterViewInit
return (<LoaderComponentOnPush>componentRef.instance).ready;
})
.then((comp) => {
// load test component with loader
return comp.loader.loadComponent(TestComponent);
})
.then((compRef) => {
done();
})
.catch(done);
});
})