feat(elements): implement NgElement
This commit is contained in:
parent
75cf70ae04
commit
aed4a11d01
@ -38,6 +38,15 @@ module.exports = function(config) {
|
|||||||
'test-events.js',
|
'test-events.js',
|
||||||
'shims_for_IE.js',
|
'shims_for_IE.js',
|
||||||
'node_modules/systemjs/dist/system.src.js',
|
'node_modules/systemjs/dist/system.src.js',
|
||||||
|
|
||||||
|
// Serve polyfills necessary for testing the `elements` package.
|
||||||
|
{
|
||||||
|
pattern: 'node_modules/@webcomponents/custom-elements/**/*.js',
|
||||||
|
included: false,
|
||||||
|
watched: false
|
||||||
|
},
|
||||||
|
{pattern: 'node_modules/mutation-observer/index.js', included: false, watched: false},
|
||||||
|
|
||||||
{pattern: 'node_modules/rxjs/**', included: false, watched: false, served: true},
|
{pattern: 'node_modules/rxjs/**', included: false, watched: false, served: true},
|
||||||
'node_modules/reflect-metadata/Reflect.js',
|
'node_modules/reflect-metadata/Reflect.js',
|
||||||
'tools/build/file2modulename.js',
|
'tools/build/file2modulename.js',
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
"@types/selenium-webdriver": "3.0.7",
|
"@types/selenium-webdriver": "3.0.7",
|
||||||
"@types/source-map": "^0.5.1",
|
"@types/source-map": "^0.5.1",
|
||||||
"@types/systemjs": "0.19.32",
|
"@types/systemjs": "0.19.32",
|
||||||
|
"@webcomponents/custom-elements": "^1.0.4",
|
||||||
"angular": "1.5.0",
|
"angular": "1.5.0",
|
||||||
"angular-animate": "1.5.0",
|
"angular-animate": "1.5.0",
|
||||||
"angular-mocks": "1.5.0",
|
"angular-mocks": "1.5.0",
|
||||||
@ -81,6 +82,7 @@
|
|||||||
"karma-sourcemap-loader": "0.3.6",
|
"karma-sourcemap-loader": "0.3.6",
|
||||||
"madge": "0.5.0",
|
"madge": "0.5.0",
|
||||||
"minimist": "1.2.0",
|
"minimist": "1.2.0",
|
||||||
|
"mutation-observer": "^1.0.3",
|
||||||
"node-uuid": "1.4.8",
|
"node-uuid": "1.4.8",
|
||||||
"protractor": "5.1.2",
|
"protractor": "5.1.2",
|
||||||
"rewire": "2.5.2",
|
"rewire": "2.5.2",
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
* @description
|
* @description
|
||||||
* Entry point for all public APIs of the `elements` package.
|
* Entry point for all public APIs of the `elements` package.
|
||||||
*/
|
*/
|
||||||
|
export {NgElement, NgElementWithProps} from './src/ng-element';
|
||||||
export {VERSION} from './src/version';
|
export {VERSION} from './src/version';
|
||||||
|
|
||||||
// This file only reexports content of the `src` folder. Keep it that way.
|
// This file only reexports content of the `src` folder. Keep it that way.
|
||||||
|
@ -11,6 +11,7 @@ const sourcemaps = require('rollup-plugin-sourcemaps');
|
|||||||
|
|
||||||
const globals = {
|
const globals = {
|
||||||
'@angular/core': 'ng.core',
|
'@angular/core': 'ng.core',
|
||||||
|
'rxjs/Subscription': 'Rx',
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
367
packages/elements/src/ng-element.ts
Normal file
367
packages/elements/src/ng-element.ts
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
/**
|
||||||
|
* @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 {ApplicationRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges} from '@angular/core';
|
||||||
|
import {Subscription} from 'rxjs/Subscription';
|
||||||
|
|
||||||
|
import {extractProjectableNodes} from './extract-projectable-nodes';
|
||||||
|
import {NgElementApplicationContext} from './ng-element-application-context';
|
||||||
|
import {createCustomEvent, getComponentName, isFunction, scheduler, strictEquals, throwError} from './utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO(gkalpak): Add docs.
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export type NgElementWithProps<T, P> = NgElement<T>& {[property in keyof P]: P[property]};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO(gkalpak): Add docs.
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export interface NgElement<T> extends HTMLElement {
|
||||||
|
ngElement: NgElement<T>|null;
|
||||||
|
componentRef: ComponentRef<T>|null;
|
||||||
|
|
||||||
|
attributeChangedCallback(
|
||||||
|
attrName: string, oldValue: string|null, newValue: string, namespace?: string): void;
|
||||||
|
connectedCallback(): void;
|
||||||
|
detach(): void;
|
||||||
|
detectChanges(): void;
|
||||||
|
disconnectedCallback(): void;
|
||||||
|
getHost(): HTMLElement;
|
||||||
|
markDirty(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an `NgElement` input.
|
||||||
|
* Similar to a `ComponentFactory` input (`{propName: string, templateName: string}`),
|
||||||
|
* except that `attrName` is derived by kebab-casing `templateName`.
|
||||||
|
*/
|
||||||
|
export interface NgElementInput {
|
||||||
|
propName: string;
|
||||||
|
attrName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an `NgElement` input.
|
||||||
|
* Similar to a `ComponentFactory` output (`{propName: string, templateName: string}`),
|
||||||
|
* except that `templateName` is renamed to `eventName`.
|
||||||
|
*/
|
||||||
|
export interface NgElementOutput {
|
||||||
|
propName: string;
|
||||||
|
eventName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An enum of possible lifecycle phases for `NgElement`s.
|
||||||
|
*/
|
||||||
|
const enum NgElementLifecyclePhase {
|
||||||
|
// The element has been instantiated, but not connected.
|
||||||
|
// (The associated component has not been created yet.)
|
||||||
|
unconnected = 'unconnected',
|
||||||
|
// The element has been instantiated and connected.
|
||||||
|
// (The associated component has been created.)
|
||||||
|
connected = 'connected',
|
||||||
|
// The element has been instantiated, connected and then disconnected.
|
||||||
|
// (The associated component has been created and then destroyed.)
|
||||||
|
disconnected = 'disconnected',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NgElementConnected<T> extends NgElementImpl<T> {
|
||||||
|
ngElement: NgElementConnected<T>;
|
||||||
|
componentRef: ComponentRef<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class NgElementImpl<T> extends HTMLElement implements NgElement<T> {
|
||||||
|
private static DESTROY_DELAY = 10;
|
||||||
|
ngElement: NgElement<T>|null = null;
|
||||||
|
componentRef: ComponentRef<T>|null = null;
|
||||||
|
onConnected = new EventEmitter<void>();
|
||||||
|
onDisconnected = new EventEmitter<void>();
|
||||||
|
|
||||||
|
private host = this as HTMLElement;
|
||||||
|
private readonly componentName = getComponentName(this.componentFactory.componentType);
|
||||||
|
private readonly initialInputValues = new Map<string, any>();
|
||||||
|
private readonly uninitializedInputs = new Set<string>();
|
||||||
|
private readonly outputSubscriptions = new Map<string, Subscription>();
|
||||||
|
private inputChanges: SimpleChanges|null = null;
|
||||||
|
private implementsOnChanges = false;
|
||||||
|
private changeDetectionScheduled = false;
|
||||||
|
private lifecyclePhase: NgElementLifecyclePhase = NgElementLifecyclePhase.unconnected;
|
||||||
|
private cancelDestruction: (() => void)|null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private appContext: NgElementApplicationContext,
|
||||||
|
private componentFactory: ComponentFactory<T>, private readonly inputs: NgElementInput[],
|
||||||
|
private readonly outputs: NgElementOutput[]) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(
|
||||||
|
attrName: string, oldValue: string|null, newValue: string, namespace?: string): void {
|
||||||
|
const input = this.inputs.find(input => input.attrName === attrName) !;
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
this.setInputValue(input.propName, newValue);
|
||||||
|
} else {
|
||||||
|
throwError(
|
||||||
|
`Calling 'attributeChangedCallback()' with unknown attribute '${attrName}' ` +
|
||||||
|
`on component '${this.componentName}' is not allowed.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback(ignoreUpgraded = false): void {
|
||||||
|
this.assertNotInPhase(NgElementLifecyclePhase.disconnected, 'connectedCallback');
|
||||||
|
|
||||||
|
if (this.cancelDestruction !== null) {
|
||||||
|
this.cancelDestruction();
|
||||||
|
this.cancelDestruction = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.lifecyclePhase === NgElementLifecyclePhase.connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = this.host as NgElement<T>;
|
||||||
|
|
||||||
|
if (host.ngElement) {
|
||||||
|
if (ignoreUpgraded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingNgElement = (host as NgElementConnected<T>).ngElement;
|
||||||
|
const existingComponentName = getComponentName(existingNgElement.componentRef.componentType);
|
||||||
|
|
||||||
|
throwError(
|
||||||
|
`Upgrading '${this.host.nodeName}' element to component '${this.componentName}' is not allowed, ` +
|
||||||
|
`because the element is already upgraded to component '${existingComponentName}'.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.appContext.runInNgZone(() => {
|
||||||
|
this.lifecyclePhase = NgElementLifecyclePhase.connected;
|
||||||
|
const cThis = (this as any as NgElementConnected<T>);
|
||||||
|
|
||||||
|
const childInjector = Injector.create([], cThis.appContext.injector);
|
||||||
|
const projectableNodes =
|
||||||
|
extractProjectableNodes(cThis.host, cThis.componentFactory.ngContentSelectors);
|
||||||
|
cThis.componentRef =
|
||||||
|
cThis.componentFactory.create(childInjector, projectableNodes, cThis.host);
|
||||||
|
cThis.implementsOnChanges =
|
||||||
|
isFunction((cThis.componentRef.instance as any as OnChanges).ngOnChanges);
|
||||||
|
|
||||||
|
cThis.initializeInputs();
|
||||||
|
cThis.initializeOutputs();
|
||||||
|
cThis.detectChanges();
|
||||||
|
|
||||||
|
cThis.appContext.applicationRef.attachView(cThis.componentRef.hostView);
|
||||||
|
|
||||||
|
// Ensure `ngElement` is set on the host too (even for manually upgraded elements)
|
||||||
|
// in order to be able to detect that the element has been been upgraded.
|
||||||
|
cThis.ngElement = host.ngElement = cThis;
|
||||||
|
|
||||||
|
cThis.onConnected.emit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
detach(): void { this.disconnectedCallback(); }
|
||||||
|
|
||||||
|
detectChanges(): void {
|
||||||
|
if (this.lifecyclePhase === NgElementLifecyclePhase.disconnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.assertNotInPhase(NgElementLifecyclePhase.unconnected, 'detectChanges');
|
||||||
|
|
||||||
|
this.appContext.runInNgZone(() => {
|
||||||
|
const cThis = this as any as NgElementConnected<T>;
|
||||||
|
|
||||||
|
cThis.changeDetectionScheduled = false;
|
||||||
|
|
||||||
|
cThis.callNgOnChanges();
|
||||||
|
cThis.componentRef.changeDetectorRef.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
if (this.lifecyclePhase === NgElementLifecyclePhase.disconnected ||
|
||||||
|
this.cancelDestruction !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.assertNotInPhase(NgElementLifecyclePhase.unconnected, 'disconnectedCallback');
|
||||||
|
|
||||||
|
const doDestroy = () => this.appContext.runInNgZone(() => this.destroy());
|
||||||
|
this.cancelDestruction = scheduler.schedule(doDestroy, NgElementImpl.DESTROY_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHost(): HTMLElement { return this.host; }
|
||||||
|
|
||||||
|
getInputValue(propName: string): any {
|
||||||
|
this.assertNotInPhase(NgElementLifecyclePhase.disconnected, 'getInputValue');
|
||||||
|
|
||||||
|
if (this.lifecyclePhase === NgElementLifecyclePhase.unconnected) {
|
||||||
|
return this.initialInputValues.get(propName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cThis = this as any as NgElementConnected<T>;
|
||||||
|
|
||||||
|
return (cThis.componentRef.instance as any)[propName];
|
||||||
|
}
|
||||||
|
|
||||||
|
markDirty(): void {
|
||||||
|
if (!this.changeDetectionScheduled) {
|
||||||
|
this.changeDetectionScheduled = true;
|
||||||
|
scheduler.scheduleBeforeRender(() => this.detectChanges());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setHost(host: HTMLElement): void {
|
||||||
|
this.assertNotInPhase(NgElementLifecyclePhase.connected, 'setHost');
|
||||||
|
this.assertNotInPhase(NgElementLifecyclePhase.disconnected, 'setHost');
|
||||||
|
|
||||||
|
this.host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInputValue(propName: string, newValue: any): void {
|
||||||
|
this.assertNotInPhase(NgElementLifecyclePhase.disconnected, 'setInputValue');
|
||||||
|
|
||||||
|
if (this.lifecyclePhase === NgElementLifecyclePhase.unconnected) {
|
||||||
|
this.initialInputValues.set(propName, newValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cThis = this as any as NgElementConnected<T>;
|
||||||
|
|
||||||
|
if (!strictEquals(newValue, cThis.getInputValue(propName))) {
|
||||||
|
cThis.recordInputChange(propName, newValue);
|
||||||
|
(cThis.componentRef.instance as any)[propName] = newValue;
|
||||||
|
cThis.markDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertNotInPhase(phase: NgElementLifecyclePhase, caller: keyof this): void {
|
||||||
|
if (this.lifecyclePhase === phase) {
|
||||||
|
throwError(
|
||||||
|
`Calling '${caller}()' on ${phase} component '${this.componentName}' is not allowed.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private callNgOnChanges(this: NgElementConnected<T>): void {
|
||||||
|
if (this.implementsOnChanges && this.inputChanges !== null) {
|
||||||
|
const inputChanges = this.inputChanges;
|
||||||
|
this.inputChanges = null;
|
||||||
|
(this.componentRef.instance as any as OnChanges).ngOnChanges(inputChanges);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private destroy() {
|
||||||
|
const cThis = this as any as NgElementConnected<T>;
|
||||||
|
|
||||||
|
cThis.componentRef.destroy();
|
||||||
|
cThis.outputs.forEach(output => cThis.unsubscribeFromOutput(output));
|
||||||
|
|
||||||
|
this.ngElement = (this.host as NgElement<any>).ngElement = null;
|
||||||
|
cThis.host.innerHTML = '';
|
||||||
|
|
||||||
|
cThis.lifecyclePhase = NgElementLifecyclePhase.disconnected;
|
||||||
|
cThis.onDisconnected.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private dispatchCustomEvent(eventName: string, value: any): void {
|
||||||
|
const event = createCustomEvent(this.host.ownerDocument, eventName, value);
|
||||||
|
|
||||||
|
this.dispatchEvent(event);
|
||||||
|
|
||||||
|
if (this.host !== this) {
|
||||||
|
this.host.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeInputs(): void {
|
||||||
|
this.inputs.forEach(({propName, attrName}) => {
|
||||||
|
let initialValue;
|
||||||
|
|
||||||
|
if (this.initialInputValues.has(propName)) {
|
||||||
|
// The property has already been set (prior to initialization).
|
||||||
|
// Update the component instance.
|
||||||
|
initialValue = this.initialInputValues.get(propName);
|
||||||
|
} else if (this.host.hasAttribute(attrName)) {
|
||||||
|
// A matching attribute exists.
|
||||||
|
// Update the component instance.
|
||||||
|
initialValue = this.host.getAttribute(attrName);
|
||||||
|
} else {
|
||||||
|
// The property does not have an initial value.
|
||||||
|
this.uninitializedInputs.add(propName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.uninitializedInputs.has(propName)) {
|
||||||
|
// The property does have an initial value.
|
||||||
|
// Forward it to the component instance.
|
||||||
|
this.setInputValue(propName, initialValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.initialInputValues.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeOutputs(this: NgElementConnected<T>): void {
|
||||||
|
this.outputs.forEach(output => this.subscribeToOutput(output));
|
||||||
|
}
|
||||||
|
|
||||||
|
private recordInputChange(propName: string, currentValue: any): void {
|
||||||
|
if (!this.implementsOnChanges) {
|
||||||
|
// The component does not implement `OnChanges`. Ignore the change.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inputChanges === null) {
|
||||||
|
this.inputChanges = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingChange = this.inputChanges[propName];
|
||||||
|
|
||||||
|
if (pendingChange) {
|
||||||
|
pendingChange.currentValue = currentValue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFirstChange = this.uninitializedInputs.has(propName);
|
||||||
|
const previousValue = isFirstChange ? undefined : this.getInputValue(propName);
|
||||||
|
this.inputChanges[propName] = new SimpleChange(previousValue, currentValue, isFirstChange);
|
||||||
|
|
||||||
|
if (isFirstChange) {
|
||||||
|
this.uninitializedInputs.delete(propName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private subscribeToOutput(this: NgElementConnected<T>, output: NgElementOutput): void {
|
||||||
|
const {propName, eventName} = output;
|
||||||
|
const emitter = (this.componentRef.instance as any)[output.propName] as EventEmitter<any>;
|
||||||
|
|
||||||
|
if (!emitter) {
|
||||||
|
throwError(`Missing emitter '${propName}' on component '${this.componentName}'.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.unsubscribeFromOutput(output);
|
||||||
|
|
||||||
|
const subscription =
|
||||||
|
emitter.subscribe((value: any) => this.dispatchCustomEvent(eventName, value));
|
||||||
|
this.outputSubscriptions.set(propName, subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsubscribeFromOutput({propName}: NgElementOutput): void {
|
||||||
|
if (!this.outputSubscriptions.has(propName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = this.outputSubscriptions.get(propName) !;
|
||||||
|
|
||||||
|
this.outputSubscriptions.delete(propName);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
1232
packages/elements/test/ng-element_spec.ts
Normal file
1232
packages/elements/test/ng-element_spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -98,3 +98,18 @@ export function installMockScheduler(isSync?: boolean): AsyncMockScheduler|SyncM
|
|||||||
|
|
||||||
return mockScheduler;
|
return mockScheduler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function patchEnv() {
|
||||||
|
// This helper function is defined in `test-main.js`. See there for more details.
|
||||||
|
(window as any).$$patchInnerHtmlProp();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreEnv() {
|
||||||
|
// This helper function is defined in `test-main.js`. See there for more details.
|
||||||
|
(window as any).$$restoreInnerHtmlProp();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function supportsCustomElements() {
|
||||||
|
// The browser does not natively support custom elements and is not polyfillable.
|
||||||
|
return typeof customElements !== 'undefined';
|
||||||
|
}
|
||||||
|
138
test-main.js
138
test-main.js
@ -67,21 +67,35 @@ System.config({
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Set up the test injector, then import all the specs, execute their `main()`
|
// Load browser-specific CustomElement polyfills, set up the test injector, import all the specs,
|
||||||
// method and kick off Karma (Jasmine).
|
// execute their `main()` method and kick off Karma (Jasmine).
|
||||||
System.import('@angular/core/testing')
|
Promise
|
||||||
.then(function(coreTesting) {
|
.resolve()
|
||||||
return Promise
|
|
||||||
.all([
|
// Load browser-specific polyfills for custom elements.
|
||||||
System.import('@angular/platform-browser-dynamic/testing'),
|
.then(function() { return loadCustomElementsPolyfills(); })
|
||||||
System.import('@angular/platform-browser/animations')
|
|
||||||
])
|
// Load necessary testing packages.
|
||||||
.then(function(mods) {
|
.then(function() {
|
||||||
coreTesting.TestBed.initTestEnvironment(
|
return Promise.all([
|
||||||
[mods[0].BrowserDynamicTestingModule, mods[1].NoopAnimationsModule],
|
System.import('@angular/core/testing'),
|
||||||
mods[0].platformBrowserDynamicTesting());
|
System.import('@angular/platform-browser-dynamic/testing'),
|
||||||
});
|
System.import('@angular/platform-browser/animations')
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Set up the test injector.
|
||||||
|
.then(function(mods) {
|
||||||
|
var coreTesting = mods[0];
|
||||||
|
var pbdTesting = mods[1];
|
||||||
|
var pbAnimations = mods[2];
|
||||||
|
|
||||||
|
coreTesting.TestBed.initTestEnvironment(
|
||||||
|
[pbdTesting.BrowserDynamicTestingModule, pbAnimations.NoopAnimationsModule],
|
||||||
|
pbdTesting.platformBrowserDynamicTesting());
|
||||||
|
})
|
||||||
|
|
||||||
|
// Import all the specs and execute their `main()` method.
|
||||||
.then(function() {
|
.then(function() {
|
||||||
return Promise.all(Object
|
return Promise.all(Object
|
||||||
.keys(window.__karma__.files) // All files served by Karma.
|
.keys(window.__karma__.files) // All files served by Karma.
|
||||||
@ -98,9 +112,105 @@ System.import('@angular/core/testing')
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Kick off karma (Jasmine).
|
||||||
.then(function() { __karma__.start(); }, function(error) { console.error(error); });
|
.then(function() { __karma__.start(); }, function(error) { console.error(error); });
|
||||||
|
|
||||||
|
|
||||||
|
function loadCustomElementsPolyfills() {
|
||||||
|
var loadedPromise = Promise.resolve();
|
||||||
|
|
||||||
|
// The custom elements polyfill relies on `MutationObserver`.
|
||||||
|
if (!window.MutationObserver) {
|
||||||
|
loadedPromise =
|
||||||
|
loadedPromise
|
||||||
|
.then(function() { return System.import('node_modules/mutation-observer/index.js'); })
|
||||||
|
.then(function(MutationObserver) { window.MutationObserver = MutationObserver; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// The custom elements polyfill relies on `Object.setPrototypeOf()`.
|
||||||
|
if (!Object.setPrototypeOf) {
|
||||||
|
var getDescriptor = function getDescriptor(obj, prop) {
|
||||||
|
var descriptor;
|
||||||
|
while (obj && !descriptor) {
|
||||||
|
descriptor = Object.getOwnPropertyDescriptor(obj, prop);
|
||||||
|
obj = Object.getPrototypeOf(obj);
|
||||||
|
}
|
||||||
|
return descriptor || {};
|
||||||
|
};
|
||||||
|
var setPrototypeOf = function setPrototypeOf(obj, proto) {
|
||||||
|
for (var prop in proto) {
|
||||||
|
if (!obj.hasOwnProperty(prop)) {
|
||||||
|
Object.defineProperty(obj, prop, getDescriptor(proto, prop));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.setPrototypeOf = setPrototypeOf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The custom elements polyfill will patch `(HTML)Element` properties, including `innerHTML`:
|
||||||
|
// https://github.com/webcomponents/custom-elements/blob/32f043c3a5e5fc3e035342c0ef10c6786fa416d7/src/Patch/Element.js#L28-L78
|
||||||
|
// The patched `innerHTML` setter will try to traverse the DOM (via `nextSibling`), which leads to
|
||||||
|
// infinite loops when testing `HtmlSanitizer` with cloberred elements on browsers that do not
|
||||||
|
// support the `<template>` element:
|
||||||
|
// https://github.com/angular/angular/blob/213baa37b0b71e72d00ad7b606ebfc2ade06b934/packages/platform-browser/src/security/html_sanitizer.ts#L29-L38
|
||||||
|
// To avoid that, we "unpatch" `(HTML)Element#innerHTML` and apply the patch only for the relevant
|
||||||
|
// `@angular/elements` tests.
|
||||||
|
var patchTarget;
|
||||||
|
var originalDescriptor;
|
||||||
|
if (!window.customElements) {
|
||||||
|
var candidatePatchTarget = window.Element.prototype;
|
||||||
|
var candidateOriginalDescriptor =
|
||||||
|
Object.getOwnPropertyDescriptor(candidatePatchTarget, 'innerHTML');
|
||||||
|
|
||||||
|
if (!originalDescriptor) {
|
||||||
|
candidatePatchTarget = window.HTMLElement.prototype;
|
||||||
|
candidateOriginalDescriptor =
|
||||||
|
Object.getOwnPropertyDescriptor(candidatePatchTarget, 'innerHTML');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateOriginalDescriptor) {
|
||||||
|
patchTarget = candidatePatchTarget;
|
||||||
|
originalDescriptor = candidateOriginalDescriptor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var polyfillPath = !window.customElements ?
|
||||||
|
// Load custom elements polyfill.
|
||||||
|
'node_modules/@webcomponents/custom-elements/custom-elements.min.js' :
|
||||||
|
// Allow ES5 functions as custom element constructors.
|
||||||
|
'node_modules/@webcomponents/custom-elements/src/native-shim.js';
|
||||||
|
|
||||||
|
loadedPromise =
|
||||||
|
loadedPromise.then(function() { return System.import(polyfillPath); }).then(function() {
|
||||||
|
// `packages/compiler/test/schema/schema_extractor.ts` relies on `HTMLElement.name`,
|
||||||
|
// but custom element polyfills will replace `HTMLElement` with an anonymous function.
|
||||||
|
Object.defineProperty(HTMLElement, 'name', {value: 'HTMLElement'});
|
||||||
|
|
||||||
|
// Create helper functions on `window` for patching/restoring `(HTML)Element#innerHTML`.
|
||||||
|
if (!patchTarget) {
|
||||||
|
window.$$patchInnerHtmlProp = window.$$restoreInnerHtmlProp = function() {};
|
||||||
|
} else {
|
||||||
|
var patchedDescriptor = Object.getOwnPropertyDescriptor(patchTarget, 'innerHTML');
|
||||||
|
|
||||||
|
window.$$patchInnerHtmlProp = function() {
|
||||||
|
Object.defineProperty(patchTarget, 'innerHTML', patchedDescriptor);
|
||||||
|
};
|
||||||
|
window.$$restoreInnerHtmlProp = function() {
|
||||||
|
Object.defineProperty(patchTarget, 'innerHTML', originalDescriptor);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Restore `innerHTML`. The patch will be manually applied only during the
|
||||||
|
// `@angular/elements` tests that need it.
|
||||||
|
window.$$restoreInnerHtmlProp();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return loadedPromise;
|
||||||
|
}
|
||||||
|
|
||||||
function onlySpecFiles(path) {
|
function onlySpecFiles(path) {
|
||||||
return /_spec\.js$/.test(path);
|
return /_spec\.js$/.test(path);
|
||||||
}
|
}
|
||||||
|
17
tools/public_api_guard/elements/elements.d.ts
vendored
17
tools/public_api_guard/elements/elements.d.ts
vendored
@ -1,2 +1,19 @@
|
|||||||
|
/** @experimental */
|
||||||
|
export interface NgElement<T> extends HTMLElement {
|
||||||
|
componentRef: ComponentRef<T> | null;
|
||||||
|
ngElement: NgElement<T> | null;
|
||||||
|
attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string, namespace?: string): void;
|
||||||
|
connectedCallback(): void;
|
||||||
|
detach(): void;
|
||||||
|
detectChanges(): void;
|
||||||
|
disconnectedCallback(): void;
|
||||||
|
getHost(): HTMLElement;
|
||||||
|
markDirty(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export declare type NgElementWithProps<T, P> = NgElement<T> & {
|
||||||
|
[property in keyof
|
||||||
|
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export declare const VERSION: Version;
|
export declare const VERSION: Version;
|
||||||
|
@ -143,6 +143,10 @@
|
|||||||
version "0.19.32"
|
version "0.19.32"
|
||||||
resolved "https://registry.yarnpkg.com/@types/systemjs/-/systemjs-0.19.32.tgz#e9204c4cdbc8e275d645c00e6150e68fc5615a24"
|
resolved "https://registry.yarnpkg.com/@types/systemjs/-/systemjs-0.19.32.tgz#e9204c4cdbc8e275d645c00e6150e68fc5615a24"
|
||||||
|
|
||||||
|
"@webcomponents/custom-elements@^1.0.4":
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@webcomponents/custom-elements/-/custom-elements-1.0.4.tgz#f7de0103026f27863ecfabaf1d450b9c21714910"
|
||||||
|
|
||||||
Base64@~0.2.0:
|
Base64@~0.2.0:
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/Base64/-/Base64-0.2.1.tgz#ba3a4230708e186705065e66babdd4c35cf60028"
|
resolved "https://registry.yarnpkg.com/Base64/-/Base64-0.2.1.tgz#ba3a4230708e186705065e66babdd4c35cf60028"
|
||||||
@ -4888,6 +4892,10 @@ multipipe@^0.1.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
duplexer2 "0.0.2"
|
duplexer2 "0.0.2"
|
||||||
|
|
||||||
|
mutation-observer@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/mutation-observer/-/mutation-observer-1.0.3.tgz#42e9222b101bca82e5ba9d5a7acf4a14c0f263d0"
|
||||||
|
|
||||||
mute-stream@0.0.5:
|
mute-stream@0.0.5:
|
||||||
version "0.0.5"
|
version "0.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
|
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user