Skip to content

Commit 8578682

Browse files
shlomiassafmatsko
authored andcommitted
feat(NgComponentOutlet): add NgComponentOutlet directive
Add NgComponentOutlet directive that can be used to dynamically create host views from a supplied component. Closes #11168 Takes over PR #11235
1 parent c0178de commit 8578682

File tree

7 files changed

+403
-1
lines changed

7 files changed

+403
-1
lines changed

modules/@angular/common/src/common.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
export * from './location/index';
1515
export {NgLocaleLocalization, NgLocalization} from './localization';
1616
export {CommonModule} from './common_module';
17-
export {NgClass, NgFor, NgIf, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet} from './directives/index';
17+
export {NgClass, NgFor, NgIf, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index';
1818
export {AsyncPipe, DatePipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, UpperCasePipe, TitleCasePipe} from './pipes/index';
1919
export {VERSION} from './version';
2020
export {Version} from '@angular/core';

modules/@angular/common/src/directives/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {Provider} from '@angular/core';
1010

1111
import {NgClass} from './ng_class';
12+
import {NgComponentOutlet} from './ng_component_outlet';
1213
import {NgFor} from './ng_for';
1314
import {NgIf} from './ng_if';
1415
import {NgPlural, NgPluralCase} from './ng_plural';
@@ -18,6 +19,7 @@ import {NgTemplateOutlet} from './ng_template_outlet';
1819

1920
export {
2021
NgClass,
22+
NgComponentOutlet,
2123
NgFor,
2224
NgIf,
2325
NgPlural,
@@ -29,12 +31,14 @@ export {
2931
NgTemplateOutlet
3032
};
3133

34+
3235
/**
3336
* A collection of Angular directives that are likely to be used in each and every Angular
3437
* application.
3538
*/
3639
export const COMMON_DIRECTIVES: Provider[] = [
3740
NgClass,
41+
NgComponentOutlet,
3842
NgFor,
3943
NgIf,
4044
NgTemplateOutlet,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {ComponentFactoryResolver, ComponentRef, Directive, Injector, Input, OnChanges, Provider, SimpleChanges, Type, ViewContainerRef} from '@angular/core';
10+
11+
12+
/**
13+
* Instantiates a single {@link Component} type and inserts its Host View into current View.
14+
* `NgComponentOutlet` provides a declarative approach for dynamic component creation.
15+
*
16+
* `NgComponentOutlet` requires a component type, if a falsy value is set the view will clear and
17+
* any existing component will get destroyed.
18+
*
19+
* ### Fine tune control
20+
*
21+
* You can control the component creation process by using the following optional attributes:
22+
*
23+
* * `ngOutletInjector`: Optional custom {@link Injector} that will be used as parent for the
24+
* Component.
25+
* Defaults to the injector of the current view container.
26+
*
27+
* * `ngOutletProviders`: Optional injectable objects ({@link Provider}) that are visible to the
28+
* component.
29+
*
30+
* * `ngOutletContent`: Optional list of projectable nodes to insert into the content
31+
* section of the component, if exists. ({@link NgContent}).
32+
*
33+
*
34+
* ### Syntax
35+
*
36+
* Simple
37+
* ```
38+
* <ng-container *ngComponentOutlet="componentTypeExpression"></ng-container>
39+
* ```
40+
*
41+
* Customized
42+
* ```
43+
* <ng-container *ngComponentOutlet="componentTypeExpression;
44+
* injector: injectorExpression;
45+
* content: contentNodesExpression">
46+
* </ng-container>
47+
* ```
48+
*
49+
* # Example
50+
*
51+
* {@example common/ngComponentOutlet/ts/module.ts region='SimpleExample'}
52+
*
53+
* A more complete example with additional options:
54+
*
55+
* {@example common/ngComponentOutlet/ts/module.ts region='CompleteExample'}
56+
*
57+
* @experimental
58+
*/
59+
@Directive({selector: '[ngComponentOutlet]'})
60+
export class NgComponentOutlet implements OnChanges {
61+
@Input() ngComponentOutlet: Type<any>;
62+
@Input() ngComponentOutletInjector: Injector;
63+
@Input() ngComponentOutletContent: any[][];
64+
65+
componentRef: ComponentRef<any>;
66+
67+
constructor(
68+
private _cmpFactoryResolver: ComponentFactoryResolver,
69+
private _viewContainerRef: ViewContainerRef) {}
70+
71+
ngOnChanges(changes: SimpleChanges) {
72+
if (this.componentRef) {
73+
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this.componentRef.hostView));
74+
}
75+
this._viewContainerRef.clear();
76+
this.componentRef = null;
77+
78+
if (this.ngComponentOutlet) {
79+
let injector = this.ngComponentOutletInjector || this._viewContainerRef.parentInjector;
80+
81+
this.componentRef = this._viewContainerRef.createComponent(
82+
this._cmpFactoryResolver.resolveComponentFactory(this.ngComponentOutlet),
83+
this._viewContainerRef.length, injector, this.ngComponentOutletContent);
84+
}
85+
}
86+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {CommonModule} from '@angular/common';
10+
import {NgComponentOutlet} from '@angular/common/src/directives/ng_component_outlet';
11+
import {Component, ComponentRef, Inject, Injector, NO_ERRORS_SCHEMA, NgModule, OpaqueToken, Optional, Provider, QueryList, ReflectiveInjector, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
12+
import {TestBed, async} from '@angular/core/testing';
13+
import {expect} from '@angular/platform-browser/testing/matchers';
14+
15+
export function main() {
16+
describe('insert/remove', () => {
17+
18+
beforeEach(() => { TestBed.configureTestingModule({imports: [TestModule]}); });
19+
20+
it('should do nothing if component is null', async(() => {
21+
const template = `<template *ngComponentOutlet="currentComponent"></template>`;
22+
TestBed.overrideComponent(TestComponent, {set: {template: template}});
23+
let fixture = TestBed.createComponent(TestComponent);
24+
25+
fixture.componentInstance.currentComponent = null;
26+
fixture.detectChanges();
27+
28+
expect(fixture.nativeElement).toHaveText('');
29+
}));
30+
31+
it('should insert content specified by a component', async(() => {
32+
let fixture = TestBed.createComponent(TestComponent);
33+
34+
fixture.detectChanges();
35+
expect(fixture.nativeElement).toHaveText('');
36+
37+
fixture.componentInstance.currentComponent = InjectedComponent;
38+
39+
fixture.detectChanges();
40+
expect(fixture.nativeElement).toHaveText('foo');
41+
}));
42+
43+
it('should emit a ComponentRef once a component was created', async(() => {
44+
let fixture = TestBed.createComponent(TestComponent);
45+
46+
fixture.detectChanges();
47+
expect(fixture.nativeElement).toHaveText('');
48+
49+
fixture.componentInstance.cmpRef = null;
50+
fixture.componentInstance.currentComponent = InjectedComponent;
51+
52+
fixture.detectChanges();
53+
expect(fixture.nativeElement).toHaveText('foo');
54+
expect(fixture.componentInstance.cmpRef).toBeAnInstanceOf(ComponentRef);
55+
expect(fixture.componentInstance.cmpRef.instance).toBeAnInstanceOf(InjectedComponent);
56+
}));
57+
58+
59+
it('should clear view if component becomes null', async(() => {
60+
let fixture = TestBed.createComponent(TestComponent);
61+
62+
fixture.detectChanges();
63+
expect(fixture.nativeElement).toHaveText('');
64+
65+
fixture.componentInstance.currentComponent = InjectedComponent;
66+
67+
fixture.detectChanges();
68+
expect(fixture.nativeElement).toHaveText('foo');
69+
70+
fixture.componentInstance.currentComponent = null;
71+
72+
fixture.detectChanges();
73+
expect(fixture.nativeElement).toHaveText('');
74+
}));
75+
76+
77+
it('should swap content if component changes', async(() => {
78+
let fixture = TestBed.createComponent(TestComponent);
79+
80+
fixture.detectChanges();
81+
expect(fixture.nativeElement).toHaveText('');
82+
83+
fixture.componentInstance.currentComponent = InjectedComponent;
84+
85+
fixture.detectChanges();
86+
expect(fixture.nativeElement).toHaveText('foo');
87+
88+
fixture.componentInstance.currentComponent = InjectedComponentAgain;
89+
90+
fixture.detectChanges();
91+
expect(fixture.nativeElement).toHaveText('bar');
92+
}));
93+
94+
it('should use the injector, if one supplied', async(() => {
95+
let fixture = TestBed.createComponent(TestComponent);
96+
97+
const uniqueValue = {};
98+
fixture.componentInstance.currentComponent = InjectedComponent;
99+
fixture.componentInstance.injector = ReflectiveInjector.resolveAndCreate(
100+
[{provide: TEST_TOKEN, useValue: uniqueValue}], fixture.componentRef.injector);
101+
102+
fixture.detectChanges();
103+
let cmpRef: ComponentRef<InjectedComponent> = fixture.componentInstance.cmpRef;
104+
expect(cmpRef).toBeAnInstanceOf(ComponentRef);
105+
expect(cmpRef.instance).toBeAnInstanceOf(InjectedComponent);
106+
expect(cmpRef.instance.testToken).toBe(uniqueValue);
107+
108+
}));
109+
110+
it('should resolve a with injector', async(() => {
111+
let fixture = TestBed.createComponent(TestComponent);
112+
113+
fixture.componentInstance.cmpRef = null;
114+
fixture.componentInstance.currentComponent = InjectedComponent;
115+
fixture.detectChanges();
116+
let cmpRef: ComponentRef<InjectedComponent> = fixture.componentInstance.cmpRef;
117+
expect(cmpRef).toBeAnInstanceOf(ComponentRef);
118+
expect(cmpRef.instance).toBeAnInstanceOf(InjectedComponent);
119+
expect(cmpRef.instance.testToken).toBeNull();
120+
}));
121+
122+
it('should render projectable nodes, if supplied', async(() => {
123+
const template = `<template>projected foo</template>${TEST_CMP_TEMPLATE}`;
124+
TestBed.overrideComponent(TestComponent, {set: {template: template}})
125+
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
126+
127+
TestBed
128+
.overrideComponent(InjectedComponent, {set: {template: `<ng-content></ng-content>`}})
129+
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
130+
131+
let fixture = TestBed.createComponent(TestComponent);
132+
133+
fixture.detectChanges();
134+
expect(fixture.nativeElement).toHaveText('');
135+
136+
fixture.componentInstance.currentComponent = InjectedComponent;
137+
fixture.componentInstance.projectables =
138+
[fixture.componentInstance.vcRef
139+
.createEmbeddedView(fixture.componentInstance.tplRefs.first)
140+
.rootNodes];
141+
142+
143+
fixture.detectChanges();
144+
expect(fixture.nativeElement).toHaveText('projected foo');
145+
}));
146+
});
147+
}
148+
149+
const TEST_TOKEN = new OpaqueToken('TestToken');
150+
@Component({selector: 'injected-component', template: 'foo'})
151+
class InjectedComponent {
152+
constructor(@Optional() @Inject(TEST_TOKEN) public testToken: any) {}
153+
}
154+
155+
156+
@Component({selector: 'injected-component-again', template: 'bar'})
157+
class InjectedComponentAgain {
158+
}
159+
160+
const TEST_CMP_TEMPLATE =
161+
`<template *ngComponentOutlet="currentComponent; injector: injector; content: projectables"></template>`;
162+
@Component({selector: 'test-cmp', template: TEST_CMP_TEMPLATE})
163+
class TestComponent {
164+
currentComponent: Type<any>;
165+
injector: Injector;
166+
projectables: any[][];
167+
168+
get cmpRef(): ComponentRef<any> { return this.ngComponentOutlet.componentRef; }
169+
set cmpRef(value: ComponentRef<any>) { this.ngComponentOutlet.componentRef = value; }
170+
171+
@ViewChildren(TemplateRef) tplRefs: QueryList<TemplateRef<any>>;
172+
@ViewChild(NgComponentOutlet) ngComponentOutlet: NgComponentOutlet;
173+
174+
constructor(public vcRef: ViewContainerRef) {}
175+
}
176+
177+
@NgModule({
178+
imports: [CommonModule],
179+
declarations: [TestComponent, InjectedComponent, InjectedComponentAgain],
180+
exports: [TestComponent, InjectedComponent, InjectedComponentAgain],
181+
entryComponents: [InjectedComponent, InjectedComponentAgain]
182+
})
183+
export class TestModule {
184+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {$, ExpectedConditions, browser, by, element} from 'protractor';
10+
import {verifyNoBrowserErrors} from '../../../../_common/e2e_util';
11+
12+
function waitForElement(selector: string) {
13+
const EC = ExpectedConditions;
14+
// Waits for the element with id 'abc' to be present on the dom.
15+
browser.wait(EC.presenceOf($(selector)), 20000);
16+
}
17+
18+
describe('ngComponentOutlet', () => {
19+
const URL = 'common/ngComponentOutlet/ts/';
20+
afterEach(verifyNoBrowserErrors);
21+
22+
describe('ng-component-outlet-example', () => {
23+
it('should render simple', () => {
24+
browser.get(URL);
25+
waitForElement('ng-component-outlet-simple-example');
26+
expect(element.all(by.css('hello-world')).getText()).toEqual(['Hello World!']);
27+
});
28+
29+
it('should render complete', () => {
30+
browser.get(URL);
31+
waitForElement('ng-component-outlet-complete-example');
32+
expect(element.all(by.css('complete-component')).getText()).toEqual(['Complete: Ahoj Svet!']);
33+
});
34+
});
35+
});

0 commit comments

Comments
 (0)