Skip to content
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
1 change: 1 addition & 0 deletions packages/compiler/src/identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export class Identifiers {
};
static QueryList: o.ExternalReference = {name: 'QueryList', moduleName: CORE};
static TemplateRef: o.ExternalReference = {name: 'TemplateRef', moduleName: CORE};
static ComponentLifecycle: o.ExternalReference = {name: 'ComponentLifecycle', moduleName: CORE};
static CodegenComponentFactoryResolver: o.ExternalReference = {
name: 'ɵCodegenComponentFactoryResolver',
moduleName: CORE,
Expand Down
5 changes: 4 additions & 1 deletion packages/compiler/src/provider_analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,10 @@ export class ProviderElementContext {
this.viewContext.reflector.resolveExternalReference(
Identifiers.ChangeDetectorRef) ||
tokenReference(dep.token) ===
this.viewContext.reflector.resolveExternalReference(Identifiers.TemplateRef)) {
this.viewContext.reflector.resolveExternalReference(Identifiers.TemplateRef) ||
tokenReference(dep.token) ===
this.viewContext.reflector.resolveExternalReference(
Identifiers.ComponentLifecycle)) {
return dep;
}
if (tokenReference(dep.token) ===
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/linker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
export {COMPILER_OPTIONS, Compiler, CompilerFactory, CompilerOptions, ModuleWithComponentFactories} from './linker/compiler';
export {ComponentFactory, ComponentRef} from './linker/component_factory';
export {ComponentFactoryResolver} from './linker/component_factory_resolver';
export {ComponentLifecycle} from './linker/component_lifecycle';
export {ElementRef} from './linker/element_ref';
export {NgModuleFactory, NgModuleRef} from './linker/ng_module_factory';
export {NgModuleFactoryLoader, getModuleFactory} from './linker/ng_module_factory_loader';
Expand Down
79 changes: 79 additions & 0 deletions packages/core/src/linker/component_lifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Subject} from 'rxjs/Subject';

import {SimpleChanges} from '../change_detection/change_detection';


/**
* An accessor to a component's lifecycle events as observables.
*
* ```
* @Component({
* selector: 'some-cmp',
* template: `
* <div *ngIf="init$ | async">...</div>
* `})
* export class SomeCmp {
* init$: Observable<void>;
* constructor(private lifecycle: ComponentLifecycle, private someService: any) {
* // Get an observable that emits an event when the component is created.
* this.init$ = this.lifecycle.onInit;
* }
*
* ngOnInit() {
* this.someService.events$
* .pipe(
* // Unsubscribe the observable automatically when the component is destroyed.
* takeUntil(this.lifecycle.onDestroy),
* )
* .subscribe(event => {});
* }
* }
* ```
*
* @experimental
*/
export class ComponentLifecycle {
/**
* An observable form of ngOnInit lifecycle event
* @experimental
*/
readonly onInit = new Subject<void>();
/**
* An observable form of ngOnChanges lifecycle event
* @experimental
*/
readonly onChanges = new Subject<SimpleChanges>();
/**
* An observable form of ngAfterContentInit lifecycle event
* @experimental
*/
readonly afterContentInit = new Subject<void>();
/**
* An observable form of ngAfterContentChecked lifecycle event
* @experimental
*/
readonly afterContentChecked = new Subject<void>();
/**
* An observable form of ngAfterViewInit lifecycle event
* @experimental
*/
readonly afterViewInit = new Subject<void>();
/**
* An observable form of ngAfterViewChecked lifecycle event
* @experimental
*/
readonly afterViewChecked = new Subject<void>();
/**
* An observable form of ngOnDestroy lifecycle event
* @experimental
*/
readonly onDestroy = new Subject<void>();
}
46 changes: 40 additions & 6 deletions packages/core/src/view/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import {ChangeDetectorRef, SimpleChange, SimpleChanges, WrappedValue} from '../change_detection/change_detection';
import {Injector, resolveForwardRef} from '../di';
import {ComponentLifecycle} from '../linker/component_lifecycle';
import {ElementRef} from '../linker/element_ref';
import {TemplateRef} from '../linker/template_ref';
import {ViewContainerRef} from '../linker/view_container_ref';
Expand All @@ -24,6 +25,7 @@ const ViewContainerRefTokenKey = tokenKey(ViewContainerRef);
const TemplateRefTokenKey = tokenKey(TemplateRef);
const ChangeDetectorRefTokenKey = tokenKey(ChangeDetectorRef);
const InjectorRefTokenKey = tokenKey(Injector);
const ComponentLifecycleTokenKey = tokenKey(ComponentLifecycle);

export function directiveDef(
checkIndex: number, flags: NodeFlags,
Expand Down Expand Up @@ -197,10 +199,17 @@ export function checkAndUpdateDirectiveInline(
}
if (changes) {
directive.ngOnChanges(changes);
if (view.lifecycle) {
view.lifecycle.onChanges.next(changes);
}
}
if ((def.flags & NodeFlags.OnInit) &&
shouldCallLifecycleInitHook(view, ViewState.InitState_CallingOnInit, def.nodeIndex)) {
directive.ngOnInit();
if (shouldCallLifecycleInitHook(view, ViewState.InitState_CallingOnInit, def.nodeIndex)) {
if ((def.flags & NodeFlags.OnInit)) {
directive.ngOnInit();
}
if (view.lifecycle) {
view.lifecycle.onInit.next();
}
}
if (def.flags & NodeFlags.DoCheck) {
directive.ngDoCheck();
Expand All @@ -222,10 +231,17 @@ export function checkAndUpdateDirectiveDynamic(
}
if (changes) {
directive.ngOnChanges(changes);
if (view.lifecycle) {
view.lifecycle.onChanges.next(changes);
}
}
if ((def.flags & NodeFlags.OnInit) &&
shouldCallLifecycleInitHook(view, ViewState.InitState_CallingOnInit, def.nodeIndex)) {
directive.ngOnInit();
if (shouldCallLifecycleInitHook(view, ViewState.InitState_CallingOnInit, def.nodeIndex)) {
if ((def.flags & NodeFlags.OnInit)) {
directive.ngOnInit();
}
if (view.lifecycle) {
view.lifecycle.onInit.next();
}
}
if (def.flags & NodeFlags.DoCheck) {
directive.ngDoCheck();
Expand Down Expand Up @@ -373,6 +389,9 @@ export function resolveDep(
}
case InjectorRefTokenKey:
return createInjector(view, elDef);
case ComponentLifecycleTokenKey:
const compView = findCompView(view, elDef, allowPrivateServices);
return compView.lifecycle;
default:
const providerDef =
(allowPrivateServices ? elDef.element !.allProviders :
Expand Down Expand Up @@ -547,18 +566,33 @@ function callProviderLifecycles(
if (lifecycles & NodeFlags.AfterContentInit &&
shouldCallLifecycleInitHook(view, ViewState.InitState_CallingAfterContentInit, initIndex)) {
provider.ngAfterContentInit();
if (view.lifecycle) {
view.lifecycle.afterContentInit.next();
}
}
if (lifecycles & NodeFlags.AfterContentChecked) {
provider.ngAfterContentChecked();
if (view.lifecycle) {
view.lifecycle.afterContentChecked.next();
}
}
if (lifecycles & NodeFlags.AfterViewInit &&
shouldCallLifecycleInitHook(view, ViewState.InitState_CallingAfterViewInit, initIndex)) {
provider.ngAfterViewInit();
if (view.lifecycle) {
view.lifecycle.afterViewInit.next();
}
}
if (lifecycles & NodeFlags.AfterViewChecked) {
provider.ngAfterViewChecked();
if (view.lifecycle) {
view.lifecycle.afterViewChecked.next();
}
}
if (lifecycles & NodeFlags.OnDestroy) {
provider.ngOnDestroy();
if (view.lifecycle) {
view.lifecycle.onDestroy.next();
}
}
}
2 changes: 2 additions & 0 deletions packages/core/src/view/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {Injector} from '../di';
import {ErrorHandler} from '../error_handler';
import {ComponentFactory} from '../linker/component_factory';
import {ComponentLifecycle} from '../linker/component_lifecycle';
import {NgModuleRef} from '../linker/ng_module_factory';
import {QueryList} from '../linker/query_list';
import {TemplateRef} from '../linker/template_ref';
Expand Down Expand Up @@ -355,6 +356,7 @@ export interface ViewData {
oldValues: any[];
disposables: DisposableFn[]|null;
initIndex: number;
lifecycle: ComponentLifecycle|null;
}

/**
Expand Down
14 changes: 9 additions & 5 deletions packages/core/src/view/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ComponentLifecycle} from '../linker/component_lifecycle';
import {Renderer2} from '../render/api';

import {checkAndUpdateElementDynamic, checkAndUpdateElementInline, createElement, listenToElementOutputs} from './element';
Expand Down Expand Up @@ -198,14 +199,14 @@ export function createEmbeddedView(
parent: ViewData, anchorDef: NodeDef, viewDef: ViewDefinition, context?: any): ViewData {
// embedded views are seen as siblings to the anchor, so we need
// to get the parent of the anchor and use it as parentIndex.
const view = createView(parent.root, parent.renderer, parent, anchorDef, viewDef);
const view = createView(parent.root, parent.renderer, parent, anchorDef, viewDef, null);
initView(view, parent.component, context);
createViewNodes(view);
return view;
}

export function createRootView(root: RootData, def: ViewDefinition, context?: any): ViewData {
const view = createView(root, root.renderer, null, null, def);
const view = createView(root, root.renderer, null, null, def, null);
initView(view, context, context);
createViewNodes(view);
return view;
Expand All @@ -220,24 +221,27 @@ export function createComponentView(
} else {
compRenderer = parentView.root.rendererFactory.createRenderer(hostElement, rendererType);
}
const lifecycle = new ComponentLifecycle();
return createView(
parentView.root, compRenderer, parentView, nodeDef.element !.componentProvider, viewDef);
parentView.root, compRenderer, parentView, nodeDef.element !.componentProvider, viewDef,
lifecycle);
}

function createView(
root: RootData, renderer: Renderer2, parent: ViewData | null, parentNodeDef: NodeDef | null,
def: ViewDefinition): ViewData {
def: ViewDefinition, lifecycle: ComponentLifecycle | null): ViewData {
const nodes: NodeData[] = new Array(def.nodes.length);
const disposables = def.outputCount ? new Array(def.outputCount) : null;
const view: ViewData = {
def,
parent,
lifecycle,
viewContainerParent: null, parentNodeDef,
context: null,
component: null, nodes,
state: ViewState.CatInit, root, renderer,
oldValues: new Array(def.bindingCount), disposables,
initIndex: -1
initIndex: -1,
};
return view;
}
Expand Down
34 changes: 33 additions & 1 deletion packages/core/test/linker/view_injector_integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactoryResolver, DebugElement, Directive, ElementRef, Host, Inject, InjectionToken, Injector, Input, NgModule, Optional, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewContainerRef} from '@angular/core';
import {Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentLifecycle, DebugElement, Directive, ElementRef, Host, Inject, InjectionToken, Injector, Input, NgModule, Optional, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewContainerRef} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {expect} from '@angular/platform-browser/testing/src/matchers';
Expand Down Expand Up @@ -143,6 +143,22 @@ class PushComponentNeedsChangeDetectorRef {
constructor(public changeDetectorRef: ChangeDetectorRef) {}
}

@Component({selector: '[needsComponentLifecycle]', template: ''})
class ComponentNeedsComponentLifecycle {
initialized = false;
constructor(public lifecycle: ComponentLifecycle) {
this.lifecycle.onInit.subscribe(() => { this.initialized = true; });
}
}

@Directive({selector: '[needsComponentLifecycle]'})
class DirectiveNeedsComponentLifecycle {
initialized = false;
constructor(public lifecycle: ComponentLifecycle) {
this.lifecycle.onInit.subscribe(() => { this.initialized = true; });
}
}

@Pipe({name: 'purePipe', pure: true})
class PurePipe implements PipeTransform {
constructor() {}
Expand Down Expand Up @@ -867,6 +883,22 @@ export function main() {
const instance = el.children[0].injector.get(OptionallyNeedsTemplateRef);
expect(instance.templateRef).toBeNull();
});

it('should inject ComponentLifecycle in Component', () => {
TestBed.configureTestingModule({declarations: [ComponentNeedsComponentLifecycle]});
const el = createComponent('<div needsComponentLifecycle></div>');
const instance = el.children[0].injector.get(ComponentNeedsComponentLifecycle);
expect(instance.lifecycle).toBeDefined();
expect(instance.initialized).toBe(true);
});

it('should inject ComponentLifecycle in Directive', () => {
TestBed.configureTestingModule({declarations: [DirectiveNeedsComponentLifecycle]});
const el = createComponent('<div needsComponentLifecycle></div>');
const instance = el.children[0].injector.get(DirectiveNeedsComponentLifecycle);
expect(instance.lifecycle).toBeDefined();
expect(instance.initialized).toBe(true);
});
});

describe('pipes', () => {
Expand Down