revert: feat(elements): implement `@angular/elements` #19469 (#20152)

This PR was merged without API docs and general rollout plan.

We can't release this as is in 5.1 without a plan for documentation, cli integration, etc.
This commit is contained in:
Igor Minar 2017-11-03 23:54:54 +01:00 committed by Victor Berchet
parent 200d92d030
commit 3997d97806
35 changed files with 23 additions and 3975 deletions

View File

@ -279,14 +279,6 @@ groups:
- IgorMinar #fallback
- mhevery #fallback
elements:
conditions:
files:
- "packages/elements/*"
users:
- mhevery #primary
- IgorMinar #fallback
benchpress:
conditions:
files:

View File

@ -211,7 +211,6 @@ The following is the list of supported scopes:
* **compiler**
* **compiler-cli**
* **core**
* **elements**
* **forms**
* **http**
* **language-service**

View File

@ -48,7 +48,6 @@ module.exports = new Package('angular-api', [basePackage, typeScriptPackage])
'common/testing/index.ts',
'core/index.ts',
'core/testing/index.ts',
'elements/index.ts',
'forms/index.ts',
'http/index.ts',
'http/testing/index.ts',

View File

@ -11,16 +11,15 @@ const { API_SOURCE_PATH } = require('../config');
const packageMap = {
animations: ['animations/index.ts', 'animations/browser/index.ts', 'animations/browser/testing/index.ts'],
common: ['common/index.ts', 'common/testing/index.ts', 'common/http/index.ts', 'common/http/testing/index.ts'],
common: ['common/index.ts', 'common/testing/index.ts'],
core: ['core/index.ts', 'core/testing/index.ts'],
elements: ['elements/index.ts'],
forms: ['forms/index.ts'],
http: ['http/index.ts', 'http/testing/index.ts'],
'platform-browser': ['platform-browser/index.ts', 'platform-browser/animations/index.ts', 'platform-browser/testing/index.ts'],
'platform-browser-dynamic': ['platform-browser-dynamic/index.ts', 'platform-browser-dynamic/testing/index.ts'],
'platform-server': ['platform-server/index.ts', 'platform-server/testing/index.ts'],
'platform-webworker': ['platform-webworker/index.ts'],
'platform-webworker-dynamic': ['platform-webworker-dynamic/index.ts'],
'platform-webworker-dynamic': 'platform-webworker-dynamic/index.ts',
router: ['router/index.ts', 'router/testing/index.ts', 'router/upgrade/index.ts'],
'service-worker': ['service-worker/index.ts'],
upgrade: ['upgrade/index.ts', 'upgrade/static/index.ts']

View File

@ -24,8 +24,7 @@ PACKAGES=(core
compiler-cli
language-service
benchpress
service-worker
elements)
service-worker)
TSC_PACKAGES=(compiler-cli
language-service

View File

@ -38,15 +38,6 @@ module.exports = function(config) {
'test-events.js',
'shims_for_IE.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},
'node_modules/reflect-metadata/Reflect.js',
'tools/build/file2modulename.js',

View File

@ -43,7 +43,6 @@
"@types/selenium-webdriver": "3.0.7",
"@types/source-map": "^0.5.1",
"@types/systemjs": "0.19.32",
"@webcomponents/custom-elements": "^1.0.4",
"angular": "1.5.0",
"angular-animate": "1.5.0",
"angular-mocks": "1.5.0",
@ -82,7 +81,6 @@
"karma-sourcemap-loader": "0.3.6",
"madge": "0.5.0",
"minimist": "1.2.0",
"mutation-observer": "^1.0.3",
"node-uuid": "1.4.8",
"protractor": "5.1.2",
"rewire": "2.5.2",

View File

@ -1,14 +0,0 @@
/**
* @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
*/
// This file is not used to build this module. It is only used during editing
// by the TypeScript language service and during build for verification. `ngc`
// replaces this file with production index.ts when it rewrites private symbol
// names.
export * from './public_api';

View File

@ -1,22 +0,0 @@
{
"name": "@angular/elements",
"version": "0.0.0-PLACEHOLDER",
"description": "Angular - library for using Angular in a web browser",
"main": "./bundles/elements.umd.js",
"module": "./esm5/elements.js",
"es2015": "./esm2015/elements.js",
"typings": "./elements.d.ts",
"author": "angular",
"license": "MIT",
"dependencies": {
"tslib": "^1.7.1"
},
"peerDependencies": {
"@angular/core": "0.0.0-PLACEHOLDER",
"@angular/platform-browser": "0.0.0-PLACEHOLDER"
},
"repository": {
"type": "git",
"url": "https://github.com/angular/angular.git"
}
}

View File

@ -1,19 +0,0 @@
/**
* @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
*/
/**
* @module
* @description
* Entry point for all public APIs of the `elements` package.
*/
export {NgElement, NgElementWithProps} from './src/ng-element';
export {NgElementConstructor} from './src/ng-element-constructor';
export {registerAsCustomElements} from './src/register-as-custom-elements';
export {VERSION} from './src/version';
// This file only reexports content of the `src` folder. Keep it that way.

View File

@ -1,28 +0,0 @@
/**
* @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
*/
const resolve = require('rollup-plugin-node-resolve');
const sourcemaps = require('rollup-plugin-sourcemaps');
const globals = {
'@angular/core': 'ng.core',
'@angular/platform-browser': 'ng.platformBrowser',
'rxjs/Subscription': 'Rx',
};
module.exports = {
entry: '../../dist/packages-dist/elements/esm5/elements.js',
dest: '../../dist/packages-dist/elements/bundles/elements.umd.js',
format: 'umd',
exports: 'named',
amd: {id: '@angular/elements'},
moduleName: 'ng.elements',
plugins: [resolve(), sourcemaps()],
external: Object.keys(globals),
globals: globals
};

View File

@ -1,54 +0,0 @@
/**
* @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
*/
// NOTE: This is a (slightly improved) version of what is used in ngUpgrade's
// `DowngradeComponentAdapter`.
// TODO(gkalpak): Investigate if it makes sense to share the code.
import {isElement, matchesSelector} from './utils';
export function extractProjectableNodes(host: HTMLElement, ngContentSelectors: string[]): Node[][] {
const nodes = host.childNodes;
const projectableNodes: Node[][] = ngContentSelectors.map(() => []);
let wildcardIndex = -1;
ngContentSelectors.some((selector, i) => {
if (selector === '*') {
wildcardIndex = i;
return true;
}
return false;
});
for (let i = 0, ii = nodes.length; i < ii; ++i) {
const node = nodes[i];
const ngContentIndex = findMatchingIndex(node, ngContentSelectors, wildcardIndex);
if (ngContentIndex !== -1) {
projectableNodes[ngContentIndex].push(node);
}
}
return projectableNodes;
}
function findMatchingIndex(node: Node, selectors: string[], defaultIndex: number): number {
let matchingIndex = defaultIndex;
if (isElement(node)) {
selectors.some((selector, i) => {
if ((selector !== '*') && matchesSelector(node, selector)) {
matchingIndex = i;
return true;
}
return false;
});
}
return matchingIndex;
}

View File

@ -1,18 +0,0 @@
/**
* @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, Injector, NgZone} from '@angular/core';
export class NgElementApplicationContext {
applicationRef = this.injector.get<ApplicationRef>(ApplicationRef);
ngZone = this.injector.get<NgZone>(NgZone);
constructor(public injector: Injector) {}
runInNgZone<R>(cb: () => R): R { return this.ngZone.run(cb); }
}

View File

@ -1,141 +0,0 @@
/**
* @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 {ComponentFactory, EventEmitter} from '@angular/core';
import {NgElementImpl, NgElementWithProps} from './ng-element';
import {NgElementApplicationContext} from './ng-element-application-context';
import {camelToKebabCase, throwError} from './utils';
/**
* TODO(gkalpak): Add docs.
* @experimental
*/
export interface NgElementConstructor<T, P> {
readonly is: string;
readonly observedAttributes: string[];
upgrade(host: HTMLElement): NgElementWithProps<T, P>;
new (): NgElementWithProps<T, P>;
}
export interface NgElementConstructorInternal<T, P> extends NgElementConstructor<T, P> {
readonly onConnected: EventEmitter<NgElementWithProps<T, P>>;
readonly onDisconnected: EventEmitter<NgElementWithProps<T, P>>;
upgrade(host: HTMLElement, ignoreUpgraded?: boolean): NgElementWithProps<T, P>;
}
type WithProperties<P> = {
[property in keyof P]: P[property]
};
// For more info on `PotentialCustomElementName` rules see:
// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
const PCEN_RE = createPcenRe();
const PCEN_BLACKLIST = [
'annotation-xml',
'color-profile',
'font-face',
'font-face-src',
'font-face-uri',
'font-face-format',
'font-face-name',
'missing-glyph',
];
export function createNgElementConstructor<T, P>(
appContext: NgElementApplicationContext,
componentFactory: ComponentFactory<T>): NgElementConstructorInternal<T, P> {
const selector = componentFactory.selector;
if (!isPotentialCustomElementName(selector)) {
throwError(
`Using '${selector}' as a custom element name is not allowed. ` +
'See https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name for more info.');
}
const inputs = componentFactory.inputs.map(({propName, templateName}) => ({
propName,
attrName: camelToKebabCase(templateName),
}));
const outputs =
componentFactory.outputs.map(({propName, templateName}) => ({
propName,
// TODO(gkalpak): Verify this is what we want and document.
eventName: templateName,
}));
// Note: According to the spec, this needs to be an ES2015 class
// (i.e. not transpiled to an ES5 constructor function).
// TODO(gkalpak): Document that if you are using ES5 sources you need to include a polyfill (e.g.
// https://github.com/webcomponents/custom-elements/blob/32f043c3a/src/native-shim.js).
class NgElementConstructorImpl extends NgElementImpl<T> {
static readonly is = selector;
static readonly observedAttributes = inputs.map(input => input.attrName);
static readonly onConnected = new EventEmitter<NgElementWithProps<T, P>>();
static readonly onDisconnected = new EventEmitter<NgElementWithProps<T, P>>();
static upgrade(host: HTMLElement, ignoreUpgraded = false): NgElementWithProps<T, P> {
const ngElement = new NgElementConstructorImpl();
ngElement.setHost(host);
ngElement.connectedCallback(ignoreUpgraded);
return ngElement as typeof ngElement & WithProperties<P>;
}
constructor() {
super(appContext, componentFactory, inputs, outputs);
const ngElement = this as this & WithProperties<P>;
this.onConnected.subscribe(() => NgElementConstructorImpl.onConnected.emit(ngElement));
this.onDisconnected.subscribe(() => NgElementConstructorImpl.onDisconnected.emit(ngElement));
}
}
inputs.forEach(({propName}) => {
Object.defineProperty(NgElementConstructorImpl.prototype, propName, {
get: function(this: NgElementImpl<any>) { return this.getInputValue(propName); },
set: function(this: NgElementImpl<any>, newValue: any) {
this.setInputValue(propName, newValue);
},
configurable: true,
enumerable: true,
});
});
return NgElementConstructorImpl as typeof NgElementConstructorImpl & {
new (): NgElementConstructorImpl&WithProperties<P>;
};
}
function createPcenRe() {
// According to [the
// spec](https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name),
// `pcenChar` is allowed to contain Unicode characters in the 10000-EFFFF range. But in order to
// match this characters with a RegExp, we need the implementation to support the `u` flag.
// On browsers that do not support it, valid PotentialCustomElementNames using characters in the
// 10000-EFFFF range will still cause an error (but these characters are not expected to be used
// in practice).
let pcenChar = '-.0-9_a-z\\u00B7\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u037D\\u037F-\\u1FFF' +
'\\u200C-\\u200D\\u203F-\\u2040\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF' +
'\\uF900-\\uFDCF\\uFDF0-\\uFFFD';
let flags = '';
if (RegExp.prototype.hasOwnProperty('unicode')) {
pcenChar += '\\u{10000}-\\u{EFFFF}';
flags += 'u';
}
return RegExp(`^[a-z][${pcenChar}]*-[${pcenChar}]*$`, flags);
}
function isPotentialCustomElementName(name: string): boolean {
return PCEN_RE.test(name) && (PCEN_BLACKLIST.indexOf(name) === -1);
}

View File

@ -1,367 +0,0 @@
/**
* @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();
}
}

View File

@ -1,155 +0,0 @@
/**
* @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 {ComponentFactoryResolver, NgModuleRef, Type} from '@angular/core';
import {DOCUMENT} from '@angular/platform-browser';
import {NgElement} from './ng-element';
import {NgElementApplicationContext} from './ng-element-application-context';
import {NgElementConstructor, NgElementConstructorInternal, createNgElementConstructor} from './ng-element-constructor';
import {scheduler, throwError} from './utils';
/**
* TODO(gkalpak): Add docs.
* @experimental
*/
export class NgElements<T> {
private doc = this.moduleRef.injector.get<Document>(DOCUMENT);
private definitions = new Map<string, NgElementConstructorInternal<any, any>>();
private upgradedElements = new Set<NgElement<any>>();
private appContext = new NgElementApplicationContext(this.moduleRef.injector);
private changeDetectionScheduled = false;
constructor(public readonly moduleRef: NgModuleRef<T>, customElementComponents: Type<any>[]) {
const resolver = moduleRef.componentFactoryResolver;
customElementComponents.forEach(
componentType => this.defineNgElement(this.appContext, resolver, componentType));
}
detachAll(root: Element = this.doc.documentElement): void {
const upgradedElements = Array.from(this.upgradedElements.values());
const elementsToDetach: NgElement<any>[] = [];
this.traverseTree(root, (node: HTMLElement) => {
upgradedElements.some(ngElement => {
if (ngElement.getHost() === node) {
elementsToDetach.push(ngElement);
return true;
}
return false;
});
});
// Detach in reverse traversal order.
this.appContext.runInNgZone(
() => elementsToDetach.reverse().forEach(ngElement => ngElement.detach()));
}
detectChanges(): void {
this.changeDetectionScheduled = false;
this.appContext.runInNgZone(
() => this.upgradedElements.forEach(ngElement => ngElement.detectChanges()));
}
forEach(
cb:
(def: NgElementConstructor<any, any>, selector: string,
map: Map<string, NgElementConstructor<any, any>>) => void): void {
return this.definitions.forEach(cb);
}
get<C, P>(selector: string): NgElementConstructor<C, P>|undefined {
return this.definitions.get(selector);
}
markDirty(): void {
if (!this.changeDetectionScheduled) {
this.changeDetectionScheduled = true;
scheduler.scheduleBeforeRender(() => this.detectChanges());
}
}
register(customElements?: CustomElementRegistry): void {
if (!customElements && (typeof window !== 'undefined')) {
customElements = window.customElements;
}
if (!customElements) {
throwError('Custom Elements are not supported in this environment.');
}
this.definitions.forEach(def => customElements !.define(def.is, def));
}
upgradeAll(root: Element = this.doc.documentElement): void {
const definitions = Array.from(this.definitions.values());
this.appContext.runInNgZone(() => {
this.traverseTree(root, (node: HTMLElement) => {
const nodeName = node.nodeName.toLowerCase();
definitions.some(def => {
if (def.is === nodeName) {
// TODO(gkalpak): What happens if `node` contains more custom elements
// (as projectable content)?
def.upgrade(node, true);
return true;
}
return false;
});
});
});
}
private defineNgElement(
appContext: NgElementApplicationContext, resolver: ComponentFactoryResolver,
componentType: Type<any>): void {
const componentFactory = resolver.resolveComponentFactory(componentType);
const def = createNgElementConstructor<any, any>(appContext, componentFactory);
const selector = def.is;
if (this.definitions.has(selector)) {
throwError(
`Defining an Angular custom element with selector '${selector}' is not allowed, ` +
'because one is already defined.');
}
def.onConnected.subscribe((ngElement: NgElement<T>) => this.upgradedElements.add(ngElement));
def.onDisconnected.subscribe(
(ngElement: NgElement<T>) => this.upgradedElements.delete(ngElement));
this.definitions.set(selector, def);
}
// TODO(gkalpak): Add support for traversing through `shadowRoot`
// (as should happen according to the spec).
// TODO(gkalpak): Investigate security implications (e.g. as seen in
// https://github.com/angular/angular.js/pull/15699).
private traverseTree(root: Element, cb: (node: HTMLElement) => void): void {
let currentNode: Element|null = root;
const getNextNonDescendant = (node: Element): Element | null => {
let currNode: Element|null = node;
let nextNode: Element|null = null;
while (!nextNode && currNode && (currNode !== root)) {
nextNode = currNode.nextElementSibling;
currNode = currNode.parentElement;
}
return nextNode;
};
while (currentNode) {
if (currentNode instanceof HTMLElement) {
cb(currentNode);
}
currentNode = currentNode.firstElementChild || getNextNonDescendant(currentNode);
}
}
}

View File

@ -1,37 +0,0 @@
/**
* @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 {NgModuleFactory, NgModuleRef, PlatformRef, Type} from '@angular/core';
import {NgElements} from './ng-elements';
import {isFunction} from './utils';
/**
* TODO(gkalpak): Add docs.
* @experimental
*/
export function registerAsCustomElements<T>(
customElementComponents: Type<any>[], platformRef: PlatformRef,
moduleFactory: NgModuleFactory<T>): Promise<NgModuleRef<T>>;
export function registerAsCustomElements<T>(
customElementComponents: Type<any>[],
bootstrapFn: () => Promise<NgModuleRef<T>>): Promise<NgModuleRef<T>>;
export function registerAsCustomElements<T>(
customElementComponents: Type<any>[],
platformRefOrBootstrapFn: PlatformRef | (() => Promise<NgModuleRef<T>>),
moduleFactory?: NgModuleFactory<T>): Promise<NgModuleRef<T>> {
const bootstrapFn = isFunction(platformRefOrBootstrapFn) ?
platformRefOrBootstrapFn :
() => platformRefOrBootstrapFn.bootstrapModuleFactory(moduleFactory !);
return bootstrapFn().then(moduleRef => {
const ngElements = new NgElements(moduleRef, customElementComponents);
ngElements.register();
return moduleRef;
});
}

View File

@ -1,112 +0,0 @@
/**
* @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 {Type} from '@angular/core';
const elProto = Element.prototype as any;
const matches = elProto.matches || elProto.matchesSelector || elProto.mozMatchesSelector ||
elProto.msMatchesSelector || elProto.oMatchesSelector || elProto.webkitMatchesSelector;
/**
* Provide methods for scheduling the execution of a callback.
*/
export const scheduler = {
/**
* Schedule a callback to be called after some delay.
*/
schedule(cb: () => void, delay: number): () =>
void{const id = window.setTimeout(cb, delay); return () => window.clearTimeout(id);},
/**
* Schedule a callback to be called before the next render.
* (If `window.requestAnimationFrame()` is not available, use `scheduler.schedule()` instead.)
*/
scheduleBeforeRender(cb: () => void): () => void{
// TODO(gkalpak): Implement a better way of accessing `requestAnimationFrame()`
// (e.g. accounting for vendor prefix, SSR-compatibility, etc).
if (typeof window.requestAnimationFrame === 'undefined') {
return scheduler.schedule(cb, 16);
} const id = window.requestAnimationFrame(cb);
return () => window.cancelAnimationFrame(id);
},
};
/**
* Convert a camelCased string to kebab-cased.
*/
export function camelToKebabCase(input: string): string {
return input.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`);
}
/**
* Create a `CustomEvent` (even on browsers where `CustomEvent` is not a constructor).
*/
export function createCustomEvent(doc: Document, name: string, detail: any): CustomEvent {
const bubbles = false;
const cancelable = false;
// On IE9-11, `CustomEvent` is not a constructor.
if (typeof CustomEvent !== 'function') {
const event = doc.createEvent('CustomEvent');
event.initCustomEvent(name, bubbles, cancelable, detail);
return event;
}
return new CustomEvent(name, {bubbles, cancelable, detail});
}
/**
* Return the name of the component or the first line of its stringified version.
*/
export function getComponentName(component: Type<any>): string {
return (component as any).overriddenName || component.name ||
component.toString().split('\n', 1)[0];
}
/**
* Check whether the input is an `Element`.
*/
export function isElement(node: Node): node is Element {
return node.nodeType === Node.ELEMENT_NODE;
}
/**
* Check whether the input is a function.
*/
export function isFunction(value: any): value is Function {
return typeof value === 'function';
}
/**
* Convert a kebab-cased string to camelCased.
*/
export function kebabToCamelCase(input: string): string {
return input.replace(/-([a-z\d])/g, (_, char) => char.toUpperCase());
}
/**
* Check whether an `Element` matches a CSS selector.
*/
export function matchesSelector(element: Element, selector: string): boolean {
return matches.call(element, selector);
}
/**
* Test two values for strict equality, accounting for the fact that `NaN !== NaN`.
*/
export function strictEquals(value1: any, value2: any): boolean {
return value1 === value2 || (value1 !== value1 && value2 !== value2);
}
/**
* Throw an error with the specified message.
* (It provides a centralized place where it is easy to apply some change/behavior to all errors.)
*/
export function throwError(message: string): void {
throw Error(message);
}

View File

@ -1,14 +0,0 @@
/**
* @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 {Version} from '@angular/core';
/**
* @experimental
*/
export const VERSION = new Version('0.0.0-PLACEHOLDER');

View File

@ -1,83 +0,0 @@
/**
* @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 {extractProjectableNodes} from '../src/extract-projectable-nodes';
export function main() {
describe('extractProjectableNodes()', () => {
let elem: HTMLElement;
let childNodes: NodeList;
const expectProjectableNodes = (matches: {[selector: string]: number[]}) => {
const selectors = Object.keys(matches);
const expected = selectors.map(selector => {
const matchingIndices = matches[selector];
return matchingIndices.map(idx => childNodes[idx]);
});
expect(extractProjectableNodes(elem, selectors)).toEqual(expected);
};
const test = (matches: {[selector: string]: number[]}) => () => expectProjectableNodes(matches);
beforeEach(() => {
elem = document.createElement('div');
elem.innerHTML = '<div class="foo" first="">' +
'<span class="bar"></span>' +
'</div>' +
'<span id="bar"></span>' +
'<!-- Comment -->' +
'Text' +
'<blink class="foo" id="quux"></blink>' +
'More text';
childNodes = Array.prototype.slice.call(elem.childNodes);
});
it('should match each node to the corresponding selector', test({
'[first]': [0],
'#bar': [1],
'#quux': [4],
}));
it('should ignore non-matching nodes', test({
'.zoo': [],
}));
it('should only match top-level child nodes', test({
'span': [1],
'.bar': [],
}));
it('should support complex selectors', test({
'.foo:not(div)': [4],
'div + #bar': [1],
}));
it('should match each node with the first matching selector', test({
'div': [0],
'.foo': [4],
'blink': [],
}));
describe('(with wildcard selector)', () => {
it('should match non-element nodes to `*` (but still ignore comments)', test({
'div,span,blink': [0, 1, 4],
'*': [2, 3, 5],
}));
it('should match otherwise unmatched nodes to `*`', test({
'div,blink': [0, 4],
'*': [1, 2, 3, 5],
}));
it('should give higher priority to `*` (eve if it appears first)', test({
'*': [2, 3, 5],
'div,span,blink': [0, 1, 4],
}));
});
});
}

View File

@ -1,52 +0,0 @@
/**
* @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, Injector, NgZone} from '@angular/core';
import {NgElementApplicationContext} from '../src/ng-element-application-context';
export function main() {
describe('NgElementApplicationContext', () => {
let mockInjector: Injector;
let mockZone: NgZone;
let ctx: NgElementApplicationContext;
beforeEach(() => {
mockZone = new NgZone({});
mockInjector = Injector.create([
{provide: ApplicationRef, useValue: 'mockApplicationRef'},
{provide: NgZone, useValue: mockZone},
]);
ctx = new NgElementApplicationContext(mockInjector);
});
it('should expose the `ApplicationRef`',
() => { expect(ctx.applicationRef as any).toBe('mockApplicationRef'); });
it('should expose the `Injector`', () => { expect(ctx.injector).toBe(mockInjector); });
it('should expose the `NgZone`', () => { expect(ctx.ngZone).toBe(mockZone); });
describe('runInNgZone()', () => {
it('should always run the callback inside the Angular zone', () => {
(spyOn(NgZone, 'isInAngularZone').and as any).returnValues(false, true);
spyOn(mockZone, 'run').and.callThrough();
const callbackSpy = (jasmine.createSpy('callback').and as any).returnValues('foo', 'bar');
const retValues = [
ctx.runInNgZone(callbackSpy),
ctx.runInNgZone(callbackSpy),
];
expect(mockZone.run).toHaveBeenCalledTimes(2);
expect(callbackSpy).toHaveBeenCalledTimes(2);
expect(retValues).toEqual(['foo', 'bar']);
});
});
});
}

View File

@ -1,349 +0,0 @@
/**
* @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, Component, ComponentFactory, EventEmitter, Inject, Input, NgModule, NgModuleRef, NgZone, Output, destroyPlatform} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {Subscription} from 'rxjs/Subscription';
import {NgElementImpl, NgElementWithProps} from '../src/ng-element';
import {NgElementApplicationContext} from '../src/ng-element-application-context';
import {NgElementConstructorInternal, createNgElementConstructor} from '../src/ng-element-constructor';
import {installMockScheduler, patchEnv, restoreEnv, supportsCustomElements} from '../testing/index';
type WithFooBar = {
fooFoo: string,
barBar: string
};
export function main() {
if (!supportsCustomElements()) {
return;
}
describe('NgElementConstructor', () => {
let moduleRef: NgModuleRef<TestModule>;
let c: NgElementConstructorInternal<TestComponent, WithFooBar>;
beforeAll(() => patchEnv());
beforeAll(done => {
installMockScheduler(true);
destroyPlatform();
platformBrowserDynamic()
.bootstrapModule(TestModule)
.then(ref => {
moduleRef = ref;
const appContext = new NgElementApplicationContext(ref.injector);
const factory = ref.componentFactoryResolver.resolveComponentFactory(TestComponent);
c = createNgElementConstructor(appContext, factory);
// The `@webcomponents/custom-elements/src/native-shim.js` polyfill, that we use to
// enable ES2015 classes transpiled to ES5 constructor functions to be used as Custom
// Elements in tests, only works if the elements have been registered with
// `customElements.define()`.
customElements.define(c.is, c);
})
.then(done, done.fail);
});
afterAll(() => destroyPlatform());
afterAll(() => restoreEnv());
describe('is', () => {
it('should be derived from the component\'s selector',
() => { expect(c.is).toBe('test-component-for-ngec'); });
it('should be a valid custom element name', () => {
const buildTestFn = (selector: string) => {
const mockAppContext = {} as NgElementApplicationContext;
const mockFactory = { selector } as ComponentFactory<any>;
return () => createNgElementConstructor(mockAppContext, mockFactory);
};
const buildError = (selector: string) =>
`Using '${selector}' as a custom element name is not allowed. ` +
'See https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name for more info.';
const validNames = [
'foo-bar',
'baz-',
'q-u-x',
'this_is-fine.too',
'this_is-fine.3',
'this.is-φίνε.4',
'tΉΪς.is-Φine.5',
];
const invalidNames = [
'foo',
'BAR',
'baz-Qux',
'φine-not',
'not:fine-at:all',
'.no-no',
'[nay-nay]',
'close-but,not-quite',
':not(my-element)',
// Blacklisted:
'color-profile',
'font-face-format',
'missing-glyph',
];
validNames.forEach(name => expect(buildTestFn(name)).not.toThrowError(buildError(name)));
invalidNames.forEach(name => expect(buildTestFn(name)).toThrowError(buildError(name)));
});
});
describe('observedAttributes', () => {
it('should be derived from the component\'s inputs', () => {
expect(c.observedAttributes).toEqual(['foo-foo', 'barbar']);
});
});
describe('constructor()', () => {
let e: NgElementWithProps<TestComponent, WithFooBar>;
beforeEach(() => {
e = new c();
e.connectedCallback();
});
it('should create an `NgElement`', () => {
// When using the `Object.setPrototypeOf()` shim, we can't check for the `NgElementImpl`
// prototype. Check for `HTMLElement` instead.
const ParentClass = (Object as any).setPrototypeOf.$$shimmed ? HTMLElement : NgElementImpl;
expect(e).toEqual(jasmine.any(ParentClass));
expect(e.getHost()).toBe(e);
expect(e.ngElement).toBe(e);
});
it('should pass `ApplicationRef` to the element', () => {
const appRef = moduleRef.injector.get<ApplicationRef>(ApplicationRef);
const component = e.componentRef !.instance;
component.fooFoo = 'newFoo';
component.barBar = 'newBar';
expect(e.innerHTML).toBe('TestComponent|foo(foo)|bar()');
appRef.tick();
expect(e.innerHTML).toBe('TestComponent|foo(newFoo)|bar(newBar)');
});
it('should pass `NgModuleRef` injector to the element', () => {
const component = e.componentRef !.instance;
expect(component.testValue).toBe('TEST');
});
it('should pass appropriate inputs to the element', () => {
const component = e.componentRef !.instance;
expect(component.fooFoo).toBe('foo');
expect(component.barBar).toBeUndefined();
e.attributeChangedCallback('foo-foo', null, 'newFoo');
expect(component.fooFoo).toBe('newFoo');
e.attributeChangedCallback('barbar', null, 'newBar');
expect(component.barBar).toBe('newBar');
});
it('should pass appropriate outputs to the element', () => {
const bazListener = jasmine.createSpy('bazListener');
const quxListener = jasmine.createSpy('quxListener');
const component = e.componentRef !.instance;
e.addEventListener('bazBaz', bazListener);
e.addEventListener('quxqux', quxListener);
component.bazBaz.emit(false);
component.quxQux.emit({qux: true});
expect(bazListener).toHaveBeenCalledWith(jasmine.objectContaining({
type: 'bazBaz',
detail: false,
}));
expect(quxListener).toHaveBeenCalledWith(jasmine.objectContaining({
type: 'quxqux',
detail: {qux: true},
}));
});
it('should set up property getters/setters for the inputs', () => {
const getInputValueSpy =
spyOn(e as any as NgElementImpl<any>, 'getInputValue').and.callThrough();
const setInputValueSpy =
spyOn(e as any as NgElementImpl<any>, 'setInputValue').and.callThrough();
(e as any).randomProp = 'ignored';
expect(setInputValueSpy).not.toHaveBeenCalled();
expect((e as any).randomProp).toBe('ignored');
expect(getInputValueSpy).not.toHaveBeenCalled();
e.fooFoo = 'newFoo';
expect(setInputValueSpy).toHaveBeenCalledWith('fooFoo', 'newFoo');
expect(e.fooFoo).toBe('newFoo');
expect(getInputValueSpy).toHaveBeenCalledWith('fooFoo');
e.barBar = 'newBar';
expect(setInputValueSpy).toHaveBeenCalledWith('barBar', 'newBar');
expect(e.barBar).toBe('newBar');
expect(getInputValueSpy).toHaveBeenCalledWith('barBar');
});
});
describe('upgrade()', () => {
let host: HTMLElement;
let e: NgElementWithProps<TestComponent, WithFooBar>;
beforeEach(() => {
host = document.createElement('div');
e = c.upgrade(host);
});
it('should create an `NgElement`', () => {
// When using the `Object.setPrototypeOf()` shim, we can't check for the `NgElementImpl`
// prototype. Check for `HTMLElement` instead.
const ParentClass = (Object as any).setPrototypeOf.$$shimmed ? HTMLElement : NgElementImpl;
expect(e).toEqual(jasmine.any(ParentClass));
});
it('should immediatelly instantiate the underlying component', () => {
expect(e.ngElement).toBe(e);
expect(e.getHost().innerHTML).toBe('TestComponent|foo(foo)|bar()');
});
it('should use the specified host', () => {
expect(e.getHost()).toBe(host);
expect((host as typeof e).ngElement).toBe(e);
});
it('should throw if the host is already upgraded (ignoreUpgraded: false)', () => {
const errorMessage =
'Upgrading \'DIV\' element to component \'TestComponent\' is not allowed, ' +
'because the element is already upgraded to component \'TestComponent\'.';
expect(() => c.upgrade(host)).toThrowError(errorMessage);
expect(() => c.upgrade(host, false)).toThrowError(errorMessage);
});
it('should do nothing if the host is already upgraded (ignoreUpgraded: true)', () => {
const compRef = e.componentRef !;
expect(() => c.upgrade(host, true)).not.toThrow();
expect((host as typeof e).ngElement).toBe(e);
expect((host as typeof e).ngElement !.componentRef).toBe(compRef);
});
});
describe('onConnected', () => {
let onConnectedSpy: jasmine.Spy;
let subscription: Subscription;
beforeEach(() => {
onConnectedSpy = jasmine.createSpy('onConnected');
subscription = c.onConnected.subscribe(onConnectedSpy);
});
afterEach(() => subscription.unsubscribe());
it('should emit every time an `NgElement` is connected', () => {
const e1 = new c();
expect(onConnectedSpy).not.toHaveBeenCalled();
e1.connectedCallback();
expect(onConnectedSpy).toHaveBeenCalledTimes(1);
expect(onConnectedSpy).toHaveBeenCalledWith(e1);
onConnectedSpy.calls.reset();
const e2 = c.upgrade(document.createElement('div'));
expect(onConnectedSpy).toHaveBeenCalledTimes(1);
expect(onConnectedSpy).toHaveBeenCalledWith(e2);
onConnectedSpy.calls.reset();
(e1 as any as NgElementImpl<TestComponent>).onConnected.emit('ignored' as any);
expect(onConnectedSpy).toHaveBeenCalledTimes(1);
expect(onConnectedSpy).toHaveBeenCalledWith(e1);
onConnectedSpy.calls.reset();
(e2 as any as NgElementImpl<TestComponent>).onConnected.emit('ignored' as any);
expect(onConnectedSpy).toHaveBeenCalledTimes(1);
expect(onConnectedSpy).toHaveBeenCalledWith(e2);
});
});
describe('onDisconnected', () => {
let onDisconnectedSpy: jasmine.Spy;
let subscription: Subscription;
beforeEach(() => {
onDisconnectedSpy = jasmine.createSpy('onDisconnected');
subscription = c.onDisconnected.subscribe(onDisconnectedSpy);
});
afterEach(() => subscription.unsubscribe());
it('should emit every time an `NgElement` is disconnected', () => {
const e1 = new c();
e1.connectedCallback();
expect(onDisconnectedSpy).not.toHaveBeenCalled();
e1.disconnectedCallback();
expect(onDisconnectedSpy).toHaveBeenCalledTimes(1);
expect(onDisconnectedSpy).toHaveBeenCalledWith(e1);
onDisconnectedSpy.calls.reset();
const e2 = c.upgrade(document.createElement('div'));
expect(onDisconnectedSpy).not.toHaveBeenCalled();
e2.disconnectedCallback();
expect(onDisconnectedSpy).toHaveBeenCalledTimes(1);
expect(onDisconnectedSpy).toHaveBeenCalledWith(e2);
onDisconnectedSpy.calls.reset();
(e1 as any as NgElementImpl<TestComponent>).onDisconnected.emit('ignored' as any);
expect(onDisconnectedSpy).toHaveBeenCalledTimes(1);
expect(onDisconnectedSpy).toHaveBeenCalledWith(e1);
onDisconnectedSpy.calls.reset();
(e2 as any as NgElementImpl<TestComponent>).onDisconnected.emit('ignored' as any);
expect(onDisconnectedSpy).toHaveBeenCalledTimes(1);
expect(onDisconnectedSpy).toHaveBeenCalledWith(e2);
});
});
// Helpers
@Component({
selector: 'test-component-for-ngec',
template: 'TestComponent|foo({{ fooFoo }})|bar({{ barBar }})',
})
class TestComponent {
@Input() fooFoo: string = 'foo';
@Input('barbar') barBar: string;
@Output() bazBaz = new EventEmitter<boolean>();
@Output('quxqux') quxQux = new EventEmitter<object>();
constructor(@Inject('TEST_VALUE') public testValue: string) {}
}
@NgModule({
imports: [BrowserModule],
providers: [
{provide: 'TEST_VALUE', useValue: 'TEST'},
],
declarations: [TestComponent],
entryComponents: [TestComponent],
})
class TestModule {
ngDoBootstrap() {}
}
});
}

File diff suppressed because it is too large Load Diff

View File

@ -1,552 +0,0 @@
/**
* @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, Component, EventEmitter, Inject, Input, NgModule, NgModuleRef, Output, destroyPlatform} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {NgElement} from '../src/ng-element';
import {NgElements} from '../src/ng-elements';
import {AsyncMockScheduler, installMockScheduler, patchEnv, restoreEnv, supportsCustomElements} from '../testing/index';
export function main() {
if (!supportsCustomElements()) {
return;
}
describe('NgElements', () => {
const DESTROY_DELAY = 10;
let uid = 0;
let mockScheduler: AsyncMockScheduler;
let moduleRef: NgModuleRef<TestModule>;
let e: NgElements<TestModule>;
beforeAll(() => patchEnv());
beforeAll(done => {
mockScheduler = installMockScheduler();
destroyPlatform();
platformBrowserDynamic()
.bootstrapModule(TestModule)
.then(ref => moduleRef = ref)
.then(done, done.fail);
});
afterAll(() => destroyPlatform());
afterAll(() => restoreEnv());
beforeEach(() => {
mockScheduler.reset();
e = new NgElements(moduleRef, [TestComponentX, TestComponentY]);
// The `@webcomponents/custom-elements/src/native-shim.js` polyfill, that we use to enable
// ES2015 classes transpiled to ES5 constructor functions to be used as Custom Elements in
// tests, only works if the elements have been registered with `customElements.define()`.
// (Using dummy selectors to ensure that the browser does not automatically upgrade the
// inserted elements.)
e.forEach(ctor => customElements.define(`${ctor.is}-${++uid}`, ctor));
});
describe('constructor()', () => {
it('should set the `moduleRef` property',
() => { expect(e.moduleRef.instance).toEqual(jasmine.any(TestModule)); });
it('should create an `NgElementConstructor` for each component', () => {
const XConstructor = e.get('test-component-for-nges-x') !;
expect(XConstructor).toEqual(jasmine.any(Function));
expect(XConstructor.is).toBe('test-component-for-nges-x');
expect(XConstructor.observedAttributes).toEqual(['x-foo']);
const YConstructor = e.get('test-component-for-nges-y') !;
expect(YConstructor).toEqual(jasmine.any(Function));
expect(YConstructor.is).toBe('test-component-for-nges-y');
expect(YConstructor.observedAttributes).toEqual(['ybar']);
});
it('should throw if there are components with the same selector', () => {
const duplicateComponents = [TestComponentX, TestComponentX];
const errorMessage =
'Defining an Angular custom element with selector \'test-component-for-nges-x\' is not ' +
'allowed, because one is already defined.';
expect(() => new NgElements(e.moduleRef, duplicateComponents)).toThrowError(errorMessage);
});
});
describe('detachAll()', () => {
let root: Element;
let detachSpies: Map<string, jasmine.Spy>;
beforeEach(() => {
root = document.createElement('div');
root.innerHTML = `
<div>
<test-component-for-nges-x id="x1"></test-component-for-nges-x>,
<ul>
<li>
<span></span>
</li>
<li>
<test-component-for-nges-x id="x2"></test-component-for-nges-x>
</li>
<li>
<span>
<test-component-for-nges-y id="y1">
<test-component-for-nges-x id="x3">PROJECTED_CONTENT</test-component-for-nges-x>
</test-component-for-nges-y>
</span>
</li>
</ul>
<span>
<test-component-for-nges-x id="x4" x-foo="newFoo"></test-component-for-nges-x>
<test-component-for-nges-y id="y2" ybar="newBar"></test-component-for-nges-y>
</span>
</div>
`;
e.upgradeAll(root);
detachSpies = new Map();
Array.prototype.forEach.call(
root.querySelectorAll('test-component-for-nges-x,test-component-for-nges-y'),
(node: NgElement<any>) => detachSpies.set(node.id, spyOn(node.ngElement !, 'detach')));
expect(detachSpies.size).toBe(6);
});
it('should detach all upgraded elements in the specified sub-tree', () => {
e.detachAll(root);
detachSpies.forEach(spy => expect(spy).toHaveBeenCalledWith());
});
it('should detach the root node itself (if appropriate)', () => {
const yNode = root.querySelector('#y1') !;
const xNode = root.querySelector('#x3') !;
e.detachAll(yNode);
detachSpies.forEach((spy, id) => {
const expectedCallCount = (id === 'y1' || id === 'x3') ? 1 : 0;
expect(spy.calls.count()).toBe(expectedCallCount);
});
});
// For more info on "shadow-including tree order" see:
// https://dom.spec.whatwg.org/#concept-shadow-including-tree-order
it('should detach nodes in reverse "shadow-including tree order"', () => {
const ids: string[] = [];
detachSpies.forEach((spy, id) => spy.and.callFake(() => ids.push(id)));
e.detachAll(root);
expect(ids).toEqual(['y2', 'x4', 'x3', 'y1', 'x2', 'x1']);
});
it('should ignore already detached elements', () => {
const xNode = root.querySelector('#x1') !;
const ngElement = (xNode as NgElement<any>).ngElement !;
// Detach node.
ngElement.disconnectedCallback();
mockScheduler.tick(DESTROY_DELAY);
// Detach the whole sub-tree (including the already detached node).
e.detachAll(root);
detachSpies.forEach((spy, id) => {
const expectedCallCount = (id === 'x1') ? 0 : 1;
expect(spy.calls.count()).toBe(expectedCallCount, id);
});
});
it('should detach the whole document if no root node is specified', () => {
e.detachAll();
detachSpies.forEach(spy => expect(spy).not.toHaveBeenCalled());
document.body.appendChild(root);
e.detachAll();
detachSpies.forEach(spy => expect(spy).toHaveBeenCalledTimes(1));
});
it('should not run change detection after detaching each component', () => {
const appRef = moduleRef.injector.get<ApplicationRef>(ApplicationRef);
const tickSpy = spyOn(appRef, 'tick');
e.detachAll(root);
expect(tickSpy).toHaveBeenCalledTimes(1);
});
});
describe('detectChanges()', () => {
let xElement: NgElement<TestComponentX>;
let yElement: NgElement<TestComponentY>;
let xDetectChangesSpy: jasmine.Spy;
let yDetectChangesSpy: jasmine.Spy;
beforeEach(() => {
const XConstructor = e.get<TestComponentX, {}>('test-component-for-nges-x') !;
const YConstructor = e.get<TestComponentY, {}>('test-component-for-nges-y') !;
xElement = new XConstructor();
yElement = new YConstructor();
xDetectChangesSpy = spyOn(xElement, 'detectChanges');
yDetectChangesSpy = spyOn(yElement, 'detectChanges');
});
it('should not affect unconnected elements', () => {
e.detectChanges();
expect(xDetectChangesSpy).not.toHaveBeenCalled();
expect(yDetectChangesSpy).not.toHaveBeenCalled();
});
it('should call `detectChanges()` on all connected elements', () => {
xElement.connectedCallback();
xDetectChangesSpy.calls.reset();
e.detectChanges();
expect(xDetectChangesSpy).toHaveBeenCalledTimes(1);
expect(yDetectChangesSpy).not.toHaveBeenCalled();
yElement.connectedCallback();
yDetectChangesSpy.calls.reset();
e.detectChanges();
expect(xDetectChangesSpy).toHaveBeenCalledTimes(2);
expect(yDetectChangesSpy).toHaveBeenCalledTimes(1);
});
it('should not affect disconnected elements', () => {
xElement.connectedCallback();
yElement.connectedCallback();
xDetectChangesSpy.calls.reset();
yDetectChangesSpy.calls.reset();
e.detectChanges();
expect(xDetectChangesSpy).toHaveBeenCalledTimes(1);
expect(yDetectChangesSpy).toHaveBeenCalledTimes(1);
xElement.disconnectedCallback();
mockScheduler.tick(DESTROY_DELAY);
e.detectChanges();
expect(xDetectChangesSpy).toHaveBeenCalledTimes(1);
expect(yDetectChangesSpy).toHaveBeenCalledTimes(2);
yElement.disconnectedCallback();
mockScheduler.tick(DESTROY_DELAY);
e.detectChanges();
expect(xDetectChangesSpy).toHaveBeenCalledTimes(1);
expect(yDetectChangesSpy).toHaveBeenCalledTimes(2);
});
it('should allow scheduling more change detection', () => {
const detectChangesSpy = spyOn(e, 'detectChanges').and.callThrough();
e.markDirty();
e.markDirty();
mockScheduler.flushBeforeRender();
expect(detectChangesSpy).toHaveBeenCalledTimes(1);
detectChangesSpy.calls.reset();
e.markDirty();
e.detectChanges();
e.markDirty();
mockScheduler.flushBeforeRender();
expect(detectChangesSpy).toHaveBeenCalledTimes(3);
});
it('should not run global change detection after checking each component', () => {
const appRef = moduleRef.injector.get<ApplicationRef>(ApplicationRef);
const tickSpy = spyOn(appRef, 'tick');
xElement.connectedCallback();
yElement.connectedCallback();
tickSpy.calls.reset();
e.detectChanges();
expect(tickSpy).toHaveBeenCalledTimes(1);
});
});
describe('forEach()', () => {
it('should allow looping through all `NgElementConstructor`s', () => {
const selectors = ['test-component-for-nges-x', 'test-component-for-nges-y'];
const callbackSpy = jasmine.createSpy('callback');
e.forEach(callbackSpy);
expect(callbackSpy).toHaveBeenCalledTimes(selectors.length);
selectors.forEach(
selector => expect(callbackSpy)
.toHaveBeenCalledWith(e.get(selector), selector, jasmine.any(Map)));
});
});
describe('get()', () => {
it('should return the `ngElementConstructor` for the specified selector (if any)', () => {
expect(e.get('test-component-for-nges-x')).toEqual(jasmine.any(Function));
expect(e.get('test-component-for-nges-y')).toEqual(jasmine.any(Function));
expect(e.get('test-component-for-nges-z')).toBeUndefined();
});
});
describe('register()', () => {
let defineSpy: jasmine.Spy;
beforeEach(() => defineSpy = spyOn(window.customElements, 'define'));
it('should add each `NgElementConstructor` to the `CustomElementRegistry`', () => {
e.register();
expect(defineSpy).toHaveBeenCalledTimes(2);
e.forEach((ctor, selector) => expect(defineSpy).toHaveBeenCalledWith(selector, ctor));
});
it('should support specifying a different `CustomElementRegistry`', () => {
const mockDefineSpy = jasmine.createSpy('mockDefine');
e.register({ define: mockDefineSpy } as any);
expect(defineSpy).not.toHaveBeenCalled();
expect(mockDefineSpy).toHaveBeenCalledTimes(2);
e.forEach((ctor, selector) => expect(mockDefineSpy).toHaveBeenCalledWith(selector, ctor));
});
it('should throw if there is no `CustomElementRegistry`', () => {
const originalDescriptor = Object.getOwnPropertyDescriptor(window, 'customElements');
const errorMessage = 'Custom Elements are not supported in this environment.';
try {
delete window.customElements;
expect(() => e.register()).toThrowError(errorMessage);
expect(() => e.register(null as any)).toThrowError(errorMessage);
} finally {
Object.defineProperty(window, 'customElements', originalDescriptor);
}
});
});
describe('markDirty()', () => {
let detectChangesSpy: jasmine.Spy;
beforeEach(() => detectChangesSpy = spyOn(e, 'detectChanges'));
it('should schedule change detection', () => {
e.markDirty();
expect(detectChangesSpy).not.toHaveBeenCalled();
mockScheduler.flushBeforeRender();
expect(detectChangesSpy).toHaveBeenCalledWith();
});
it('should not schedule change detection if already scheduled', () => {
e.markDirty();
e.markDirty();
e.markDirty();
mockScheduler.flushBeforeRender();
expect(detectChangesSpy).toHaveBeenCalledTimes(1);
});
});
describe('upgradeAll()', () => {
const multiTrim = (input: string | null) => input && input.replace(/\s+/g, '');
let root: Element;
beforeEach(() => {
root = document.createElement('div');
root.innerHTML = `
<div>
DIV(
<test-component-for-nges-x id="x1"></test-component-for-nges-x>,
<ul>
UL(
<li>
LI(<span>SPAN</span>)
</li>,
<li>
LI(<test-component-for-nges-x id="x2"></test-component-for-nges-x>)
</li>,
<li>
LI(
<span>
SPAN(
<test-component-for-nges-y id="y1">
<test-component-for-nges-x id="x3">PROJECTED_CONTENT</test-component-for-nges-x>
</test-component-for-nges-y>
)
</span>
)
</li>
)
</ul>,
<span>
SPAN(
<test-component-for-nges-x id="x4" x-foo="newFoo"></test-component-for-nges-x>,
<test-component-for-nges-y id="y2" ybar="newBar"></test-component-for-nges-y>
)
</span>
)
</div>
`;
});
it('should upgrade all matching elements in the specified sub-tree', () => {
e.upgradeAll(root);
expect(multiTrim(root.textContent)).toBe(multiTrim(`
DIV(
TestComponentX(xFoo)(),
UL(
LI(SPAN),
LI(TestComponentX(xFoo)()),
LI(SPAN(TestComponentY()(TestComponentX(xFoo)(PROJECTED_CONTENT))))
),
SPAN(
TestComponentX(newFoo)(),
TestComponentY(newBar)()
)
)
`));
});
it('should upgrade the root node itself (if appropriate)', () => {
const yNode = root.querySelector('#y1') !;
e.upgradeAll(yNode);
expect(multiTrim(yNode.textContent))
.toBe('TestComponentY()(TestComponentX(xFoo)(PROJECTED_CONTENT))');
});
// For more info on "shadow-including tree order" see:
// https://dom.spec.whatwg.org/#concept-shadow-including-tree-order
it('should upgrade nodes in "shadow-including tree order"', () => {
const ids: string[] = [];
e.forEach(
def => spyOn(def, 'upgrade').and.callFake((node: HTMLElement) => ids.push(node.id)));
e.upgradeAll(root);
expect(ids).toEqual(['x1', 'x2', 'y1', 'x3', 'x4', 'y2']);
});
it('should ignore already upgraded elements (same component)', () => {
const xNode = root.querySelector('#x1') as HTMLElement;
const XConstructor = e.get<TestComponentX, {}>('test-component-for-nges-x') !;
// Upgrade node to matching `NgElement`.
expect(XConstructor.is).toBe(xNode.nodeName.toLowerCase());
const oldNgElement = XConstructor.upgrade(xNode);
const oldComponent = oldNgElement.componentRef !.instance;
// Upgrade the whole sub-tree (including the already upgraded node).
e.upgradeAll(root);
const newNgElement = (xNode as NgElement<any>).ngElement !;
const newComponent = newNgElement.componentRef !.instance;
expect(newNgElement).toBe(oldNgElement);
expect(newComponent).toBe(oldComponent);
expect(newComponent).toEqual(jasmine.any(TestComponentX));
});
it('should ignore already upgraded elements (different component)', () => {
const xNode = root.querySelector('#x1') as HTMLElement;
const YConstructor = e.get<TestComponentY, {}>('test-component-for-nges-y') !;
// Upgrade node to matching `NgElement`.
expect(YConstructor.is).not.toBe(xNode.nodeName.toLowerCase());
const oldNgElement = YConstructor.upgrade(xNode);
const oldComponent = oldNgElement.componentRef !.instance;
// Upgrade the whole sub-tree (including the already upgraded node).
e.upgradeAll(root);
const newNgElement = (xNode as NgElement<any>).ngElement !;
const newComponent = newNgElement.componentRef !.instance;
expect(newNgElement).toBe(oldNgElement);
expect(newComponent).toBe(oldComponent);
expect(newComponent).toEqual(jasmine.any(TestComponentY));
});
it('should upgrade the whole document if no root node is specified', () => {
const expectedUpgradedTextContent = multiTrim(`
DIV(
TestComponentX(xFoo)(),
UL(
LI(SPAN),
LI(TestComponentX(xFoo)()),
LI(SPAN(TestComponentY()(TestComponentX(xFoo)(PROJECTED_CONTENT))))
),
SPAN(
TestComponentX(newFoo)(),
TestComponentY(newBar)()
)
)
`);
e.upgradeAll();
expect(multiTrim(root.textContent)).not.toBe(expectedUpgradedTextContent);
document.body.appendChild(root);
e.upgradeAll();
expect(multiTrim(root.textContent)).toBe(expectedUpgradedTextContent);
});
it('should not run global change detection after upgrading each component', () => {
const appRef = moduleRef.injector.get<ApplicationRef>(ApplicationRef);
const tickSpy = spyOn(appRef, 'tick');
e.upgradeAll(root);
expect(tickSpy).toHaveBeenCalledTimes(1);
});
});
// Helpers
@Component({
selector: 'test-component-for-nges-x',
template: 'TestComponentX({{ xFoo }})(<ng-content></ng-content>)',
})
class TestComponentX {
@Input() xFoo: string = 'xFoo';
@Output() xBaz = new EventEmitter<boolean>();
constructor(@Inject('TEST_VALUE') public testValue: string) {}
}
@Component({
selector: 'test-component-for-nges-y',
template: 'TestComponentY({{ yBar }})(<ng-content></ng-content>)',
})
class TestComponentY {
@Input('ybar') yBar: string;
@Output('yqux') yQux = new EventEmitter<object>();
constructor(@Inject('TEST_VALUE') public testValue: string) {}
}
@NgModule({
imports: [BrowserModule],
providers: [
{provide: 'TEST_VALUE', useValue: {value: 'TEST'}},
],
declarations: [TestComponentX, TestComponentY],
entryComponents: [TestComponentX, TestComponentY],
})
class TestModule {
ngDoBootstrap() {}
}
});
}

View File

@ -1,127 +0,0 @@
/**
* @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 {CompilerFactory, Component, NgModule, NgModuleFactory, NgModuleRef, PlatformRef, Type, destroyPlatform} from '@angular/core';
import {BrowserModule, platformBrowser} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {NgElementImpl} from '../src/ng-element';
import {registerAsCustomElements} from '../src/register-as-custom-elements';
import {isFunction} from '../src/utils';
import {patchEnv, restoreEnv, supportsCustomElements} from '../testing/index';
type BootstrapFn<M> = () => Promise<NgModuleRef<M>>;
type ArgsWithModuleFactory<M> = [PlatformRef, NgModuleFactory<M>];
type ArgsWithBootstrapFn<M> = [BootstrapFn<M>];
export function main() {
if (!supportsCustomElements()) {
return;
}
describe('registerAsCustomElements()', () => {
const createArgsToRegisterWithModuleFactory = (platformFn: () => PlatformRef) => {
const tempPlatformRef = platformBrowserDynamic();
const compilerFactory = tempPlatformRef.injector.get(CompilerFactory) as CompilerFactory;
const compiler = compilerFactory.createCompiler([]);
tempPlatformRef.destroy();
const platformRef = platformFn();
const moduleFactory = compiler.compileModuleSync(TestModule);
return [platformRef, moduleFactory] as ArgsWithModuleFactory<TestModule>;
};
const createArgsToRegisterWithBootstrapFn =
() => [() => platformBrowserDynamic().bootstrapModule(TestModule)] as
ArgsWithBootstrapFn<TestModule>;
beforeAll(() => patchEnv());
afterAll(() => restoreEnv());
// Run the tests with both an `NgModuleFactory` and a `bootstrapFn()`.
runTests(
'with `NgModuleFactory` (on `platformBrowserDynamic`)',
() => createArgsToRegisterWithModuleFactory(platformBrowserDynamic));
runTests(
'with `NgModuleFactory` (on `platformBrowser`)',
() => createArgsToRegisterWithModuleFactory(platformBrowser));
runTests('with `bootstrapFn()`', createArgsToRegisterWithBootstrapFn);
function runTests<M>(
description: string, createArgs: () => ArgsWithModuleFactory<M>| ArgsWithBootstrapFn<M>) {
describe(description, () => {
const customElementComponents: Type<any>[] = [FooBarComponent, BazQuxComponent];
const hasBootstrapFn = (arr: any[]): arr is ArgsWithBootstrapFn<M> => isFunction(arr[0]);
let doRegister: () => Promise<NgModuleRef<M>>;
let defineSpy: jasmine.Spy;
beforeEach(() => {
destroyPlatform();
const args = createArgs();
doRegister = hasBootstrapFn(args) ?
() => registerAsCustomElements(customElementComponents, args[0]) :
() => registerAsCustomElements(customElementComponents, args[0], args[1]);
defineSpy = spyOn(customElements, 'define');
});
afterEach(() => destroyPlatform());
it('should bootstrap the `NgModule` and return an `NgModuleRef` instance', done => {
doRegister()
.then(ref => expect(ref.instance).toEqual(jasmine.any(TestModule)))
.then(done, done.fail);
});
it('should define a custom element for each component', done => {
doRegister()
.then(() => {
expect(defineSpy).toHaveBeenCalledTimes(2);
expect(defineSpy).toHaveBeenCalledWith('foo-bar', jasmine.any(Function));
expect(defineSpy).toHaveBeenCalledWith('baz-qux', jasmine.any(Function));
expect(defineSpy.calls.argsFor(0)[1]).toEqual(jasmine.objectContaining({
is: 'foo-bar',
observedAttributes: [],
upgrade: jasmine.any(Function),
}));
expect(defineSpy.calls.argsFor(1)[1]).toEqual(jasmine.objectContaining({
is: 'baz-qux',
observedAttributes: [],
upgrade: jasmine.any(Function),
}));
})
.then(done, done.fail);
});
});
}
});
}
@Component({
selector: 'foo-bar',
template: 'FooBar',
})
class FooBarComponent {
}
@Component({
selector: 'baz-qux',
template: 'BazQux',
})
class BazQuxComponent {
}
@NgModule({
imports: [BrowserModule],
declarations: [FooBarComponent, BazQuxComponent],
entryComponents: [FooBarComponent, BazQuxComponent],
})
class TestModule {
ngDoBootstrap() {}
}

View File

@ -1,259 +0,0 @@
/**
* @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 {Type} from '@angular/core';
import {camelToKebabCase, createCustomEvent, getComponentName, isElement, isFunction, kebabToCamelCase, matchesSelector, scheduler, strictEquals, throwError} from '../src/utils';
export function main() {
describe('utils', () => {
describe('scheduler', () => {
describe('schedule()', () => {
let setTimeoutSpy: jasmine.Spy;
let clearTimeoutSpy: jasmine.Spy;
beforeEach(() => {
setTimeoutSpy = spyOn(window, 'setTimeout').and.returnValue(42);
clearTimeoutSpy = spyOn(window, 'clearTimeout');
});
it('should delegate to `window.setTimeout()`', () => {
const cb = () => null;
const delay = 1337;
scheduler.schedule(cb, delay);
expect(setTimeoutSpy).toHaveBeenCalledWith(cb, delay);
});
it('should return a function for cancelling the scheduled job', () => {
const cancelFn = scheduler.schedule(() => null, 0);
expect(clearTimeoutSpy).not.toHaveBeenCalled();
cancelFn();
expect(clearTimeoutSpy).toHaveBeenCalledWith(42);
});
});
describe('scheduleBeforeRender()', () => {
if (typeof window.requestAnimationFrame === 'undefined') {
const mockCancelFn = () => undefined;
let scheduleSpy: jasmine.Spy;
beforeEach(
() => scheduleSpy = spyOn(scheduler, 'schedule').and.returnValue(mockCancelFn));
it('should delegate to `scheduler.schedule()`', () => {
const cb = () => null;
expect(scheduler.scheduleBeforeRender(cb)).toBe(mockCancelFn);
expect(scheduleSpy).toHaveBeenCalledWith(cb, 16);
});
} else {
let requestAnimationFrameSpy: jasmine.Spy;
let cancelAnimationFrameSpy: jasmine.Spy;
beforeEach(() => {
requestAnimationFrameSpy = spyOn(window, 'requestAnimationFrame').and.returnValue(42);
cancelAnimationFrameSpy = spyOn(window, 'cancelAnimationFrame');
});
it('should delegate to `window.requestAnimationFrame()`', () => {
const cb = () => null;
scheduler.scheduleBeforeRender(cb);
expect(requestAnimationFrameSpy).toHaveBeenCalledWith(cb);
});
it('should return a function for cancelling the scheduled job', () => {
const cancelFn = scheduler.scheduleBeforeRender(() => null);
expect(cancelAnimationFrameSpy).not.toHaveBeenCalled();
cancelFn();
expect(cancelAnimationFrameSpy).toHaveBeenCalledWith(42);
});
}
});
});
describe('camelToKebabCase()', () => {
it('should convert camel-case to kebab-case', () => {
expect(camelToKebabCase('fooBarBazQux')).toBe('foo-bar-baz-qux');
expect(camelToKebabCase('foo1Bar2Baz3Qux4')).toBe('foo1-bar2-baz3-qux4');
});
it('should keep existing dashes',
() => { expect(camelToKebabCase('fooBar-baz-Qux')).toBe('foo-bar-baz--qux'); });
});
describe('createCustomEvent()', () => {
it('should create a custom event (with appropriate properties)', () => {
const value = {bar: 'baz'};
const event = createCustomEvent(document, 'foo', value);
expect(event).toEqual(jasmine.any(CustomEvent));
expect(event).toEqual(jasmine.any(Event));
expect(event.type).toBe('foo');
expect(event.bubbles).toBe(false);
expect(event.cancelable).toBe(false);
expect(event.detail).toEqual(value);
});
});
describe('getComponentName()', () => {
it('should return the component\'s name', () => {
class Foo {}
expect(getComponentName(Foo)).toBe('Foo');
});
it('should return the `overriddenName` (if present)', () => {
class Foo {
static overriddenName = 'Bar';
}
expect(getComponentName(Foo)).toBe('Bar');
});
it('should return the first line of the stringified component if no name', () => {
const Foo = {toString: () => 'Baz\nQux'};
expect(getComponentName(Foo as Type<any>)).toBe('Baz');
});
});
describe('isElement()', () => {
it('should return true for Element nodes', () => {
const elems = [
document.body,
document.createElement('div'),
document.createElement('option'),
document.documentElement,
];
elems.forEach(n => expect(isElement(n)).toBe(true));
});
it('should return false for non-Element nodes', () => {
const nonElems = [
document,
document.createAttribute('foo'),
document.createDocumentFragment(),
document.createComment('bar'),
document.createTextNode('baz'),
];
nonElems.forEach(n => expect(isElement(n)).toBe(false));
});
});
describe('isFunction()', () => {
it('should return true for functions', () => {
const obj = {foo: function() {}, bar: () => null, baz() {}};
const fns = [
function(){},
() => null,
obj.foo,
obj.bar,
obj.baz,
Function,
Date,
];
fns.forEach(v => expect(isFunction(v)).toBe(true));
});
it('should return false for non-functions', () => {
const nonFns = [
undefined,
null,
true,
42,
{},
];
nonFns.forEach(v => expect(isFunction(v)).toBe(false));
});
});
describe('kebabToCamelCase()', () => {
it('should convert camel-case to kebab-case', () => {
expect(kebabToCamelCase('foo-bar-baz-qux')).toBe('fooBarBazQux');
expect(kebabToCamelCase('foo1-bar2-baz3-qux4')).toBe('foo1Bar2Baz3Qux4');
expect(kebabToCamelCase('foo-1-bar-2-baz-3-qux-4')).toBe('foo1Bar2Baz3Qux4');
});
it('should keep uppercase letters', () => {
expect(kebabToCamelCase('foo-barBaz-Qux')).toBe('fooBarBaz-Qux');
expect(kebabToCamelCase('foo-barBaz--qux')).toBe('fooBarBaz-Qux');
});
});
describe('matchesSelector()', () => {
let li: HTMLLIElement;
beforeEach(() => {
const div = document.createElement('div');
div.innerHTML = `
<div class="bar" id="barDiv">
<span class="baz"></span>
<ul class="baz" id="bazUl">
<li class="qux" id="quxLi"></li>
</ul>
</div>
`;
li = div.querySelector('li') !;
});
it('should return whether the element matches the selector', () => {
expect(matchesSelector(li, 'li')).toBe(true);
expect(matchesSelector(li, '.qux')).toBe(true);
expect(matchesSelector(li, '#quxLi')).toBe(true);
expect(matchesSelector(li, '.qux#quxLi:not(.quux)')).toBe(true);
expect(matchesSelector(li, '.bar > #bazUl > li')).toBe(true);
expect(matchesSelector(li, '.bar .baz ~ .baz li')).toBe(true);
expect(matchesSelector(li, 'ol')).toBe(false);
expect(matchesSelector(li, '.quux')).toBe(false);
expect(matchesSelector(li, '#quuxOl')).toBe(false);
expect(matchesSelector(li, '.qux#quxLi:not(li)')).toBe(false);
expect(matchesSelector(li, '.bar > #bazUl > .quxLi')).toBe(false);
expect(matchesSelector(li, 'div span ul li')).toBe(false);
});
});
describe('strictEquals()', () => {
it('should perform strict equality check', () => {
const values = [
undefined,
null,
true,
false,
42,
'42',
() => undefined,
() => undefined,
{},
{},
];
values.forEach((v1, i) => {
values.forEach((v2, j) => { expect(strictEquals(v1, v2)).toBe(i === j); });
});
});
it('should consider two `NaN` values equals', () => {
expect(strictEquals(NaN, NaN)).toBe(true);
expect(strictEquals(NaN, 'foo')).toBe(false);
expect(strictEquals(NaN, 42)).toBe(false);
expect(strictEquals(NaN, null)).toBe(false);
expect(strictEquals(NaN, undefined)).toBe(false);
});
});
describe('throwError()', () => {
it('should throw an error based on the specified message',
() => { expect(() => throwError('Test')).toThrowError('Test'); });
});
});
}

View File

@ -1,115 +0,0 @@
/**
* @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 {scheduler} from '../src/utils';
export interface MockScheduler {
schedule: (typeof scheduler)['schedule'];
scheduleBeforeRender: (typeof scheduler)['scheduleBeforeRender'];
}
export class AsyncMockScheduler implements MockScheduler {
private uid = 0;
private pendingBeforeRenderCallbacks: ({id: number, cb: () => void})[] = [];
private pendingDelayedCallbacks: ({id: number, cb: () => void, delay: number})[] = [];
flushBeforeRender(): void {
while (this.pendingBeforeRenderCallbacks.length) {
const cb = this.pendingBeforeRenderCallbacks.shift() !.cb;
cb();
}
}
reset(): void {
this.pendingBeforeRenderCallbacks.length = 0;
this.pendingDelayedCallbacks.length = 0;
}
schedule(cb: () => void, delay: number): () => void {
const id = ++this.uid;
let idx = this.pendingDelayedCallbacks.length;
for (let i = this.pendingDelayedCallbacks.length - 1; i >= 0; --i) {
if (this.pendingDelayedCallbacks[i].delay <= delay) {
idx = i + 1;
break;
}
}
this.pendingDelayedCallbacks.splice(idx, 0, {id, cb, delay});
return () => this.remove(id, this.pendingDelayedCallbacks);
}
scheduleBeforeRender(cb: () => void): () => void {
const id = ++this.uid;
this.pendingBeforeRenderCallbacks.push({id, cb});
return () => this.remove(id, this.pendingBeforeRenderCallbacks);
}
tick(ms: number): void {
this.flushBeforeRender();
this.pendingDelayedCallbacks.forEach(item => item.delay -= ms);
this.pendingDelayedCallbacks = this.pendingDelayedCallbacks.filter(item => {
if (item.delay <= 0) {
const cb = item.cb;
cb();
return false;
}
return true;
});
}
private remove(id: number, items: {id: number}[]): void {
for (let i = 0, ii = items.length; i < ii; ++i) {
if (items[i].id === id) {
items.splice(i, 1);
break;
}
}
}
}
export class SyncMockScheduler implements MockScheduler {
schedule(cb: () => void, delay: number): () => void {
cb();
return () => undefined;
}
scheduleBeforeRender(cb: () => void): () => void {
cb();
return () => undefined;
}
}
export function installMockScheduler(isSync?: false): AsyncMockScheduler;
export function installMockScheduler(isSync: true): SyncMockScheduler;
export function installMockScheduler(isSync?: boolean): AsyncMockScheduler|SyncMockScheduler {
const mockScheduler = isSync ? new SyncMockScheduler() : new AsyncMockScheduler();
Object.keys(scheduler).forEach((method: keyof typeof scheduler) => {
spyOn(scheduler, method).and.callFake(mockScheduler[method].bind(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';
}

View File

@ -1,27 +0,0 @@
{
"extends": "../tsconfig-build.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": ".",
"paths": {
"@angular/common": ["../../dist/packages/common"],
"@angular/core": ["../../dist/packages/core"],
"@angular/platform-browser": ["../../dist/packages/platform-browser"]
},
"outDir": "../../dist/packages/elements"
},
"files": [
"public_api.ts",
"../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": false,
"skipTemplateCodegen": true,
"flatModuleOutFile": "elements.js",
"flatModuleId": "@angular/elements"
}
}

View File

@ -62,40 +62,25 @@ System.config({
'@angular/platform-server': {main: 'index.js', defaultExtension: 'js'},
'@angular/platform-webworker': {main: 'index.js', defaultExtension: 'js'},
'@angular/platform-webworker-dynamic': {main: 'index.js', defaultExtension: 'js'},
'@angular/elements': {main: 'index.js', defaultExtension: 'js'},
}
});
// Load browser-specific CustomElement polyfills, set up the test injector, import all the specs,
// execute their `main()` method and kick off Karma (Jasmine).
Promise
.resolve()
// Load browser-specific polyfills for custom elements.
.then(function() { return loadCustomElementsPolyfills(); })
// Load necessary testing packages.
.then(function() {
return Promise.all([
System.import('@angular/core/testing'),
System.import('@angular/platform-browser-dynamic/testing'),
System.import('@angular/platform-browser/animations')
]);
// Set up the test injector, then import all the specs, execute their `main()`
// method and kick off Karma (Jasmine).
System.import('@angular/core/testing')
.then(function(coreTesting) {
return Promise
.all([
System.import('@angular/platform-browser-dynamic/testing'),
System.import('@angular/platform-browser/animations')
])
.then(function(mods) {
coreTesting.TestBed.initTestEnvironment(
[mods[0].BrowserDynamicTestingModule, mods[1].NoopAnimationsModule],
mods[0].platformBrowserDynamicTesting());
});
})
// 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() {
return Promise.all(Object
.keys(window.__karma__.files) // All files served by Karma.
@ -112,106 +97,9 @@ Promise
});
}));
})
// Kick off karma (Jasmine).
.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.defineProperty(setPrototypeOf, '$$shimmed', {value: true});
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) {
return /_spec\.js$/.test(path);
}

View File

@ -62,7 +62,6 @@ var specFiles: any =
'@angular/router/test/integration/bootstrap_spec.*',
'@angular/integration_test/symbol_inspector/**',
'@angular/upgrade/**',
'@angular/elements/**',
'@angular/**/e2e_test/**',
'angular1_router/**',
'payload_tests/**',

View File

@ -28,7 +28,7 @@ const entrypoints = [
'dist/packages-dist/service-worker/service-worker.d.ts',
'dist/packages-dist/service-worker/config.d.ts', 'dist/packages-dist/animations/browser.d.ts',
'dist/packages-dist/animations/browser/testing.d.ts',
'dist/packages-dist/platform-browser/animations.d.ts', 'dist/packages-dist/elements/elements.d.ts'
'dist/packages-dist/platform-browser/animations.d.ts'
];
const publicApiDir = 'tools/public_api_guard';

View File

@ -1,30 +0,0 @@
/** @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 interface NgElementConstructor<T, P> {
readonly is: string;
readonly observedAttributes: string[];
new (): NgElementWithProps<T, P>;
upgrade(host: HTMLElement): NgElementWithProps<T, P>;
}
/** @experimental */
export declare type NgElementWithProps<T, P> = NgElement<T> & {
[property in keyof
/** @experimental */
export declare function registerAsCustomElements<T>(customElementComponents: Type<any>[], platformRef: PlatformRef, moduleFactory: NgModuleFactory<T>): Promise<NgModuleRef<T>>;
/** @experimental */
export declare const VERSION: Version;

View File

@ -21,7 +21,6 @@
"compiler",
"compiler-cli",
"core",
"elements",
"forms",
"http",
"language-service",

View File

@ -51,19 +51,19 @@ describe('validate-commit-message.js', function() {
expect(validateMessage('refactor(docs): something')).toBe(INVALID);
['INVALID COMMIT MSG: "fix(Compiler): something"\n' +
' => ERROR: "Compiler" is not an allowed scope.\n' +
' => SCOPES: aio, animations, bazel, benchpress, common, compiler, compiler-cli, core, elements, forms, http, language-service, platform-browser, platform-browser-dynamic, platform-server, platform-webworker, platform-webworker-dynamic, router, service-worker, upgrade, packaging, changelog',
' => SCOPES: aio, animations, bazel, benchpress, common, compiler, compiler-cli, core, forms, http, language-service, platform-browser, platform-browser-dynamic, platform-server, platform-webworker, platform-webworker-dynamic, router, service-worker, upgrade, packaging, changelog',
'INVALID COMMIT MSG: "feat(bah): something"\n' +
' => ERROR: "bah" is not an allowed scope.\n' +
' => SCOPES: aio, animations, bazel, benchpress, common, compiler, compiler-cli, core, elements, forms, http, language-service, platform-browser, platform-browser-dynamic, platform-server, platform-webworker, platform-webworker-dynamic, router, service-worker, upgrade, packaging, changelog',
' => SCOPES: aio, animations, bazel, benchpress, common, compiler, compiler-cli, core, forms, http, language-service, platform-browser, platform-browser-dynamic, platform-server, platform-webworker, platform-webworker-dynamic, router, service-worker, upgrade, packaging, changelog',
'INVALID COMMIT MSG: "style(webworker): something"\n' +
' => ERROR: "webworker" is not an allowed scope.\n' +
' => SCOPES: aio, animations, bazel, benchpress, common, compiler, compiler-cli, core, elements, forms, http, language-service, platform-browser, platform-browser-dynamic, platform-server, platform-webworker, platform-webworker-dynamic, router, service-worker, upgrade, packaging, changelog',
' => SCOPES: aio, animations, bazel, benchpress, common, compiler, compiler-cli, core, forms, http, language-service, platform-browser, platform-browser-dynamic, platform-server, platform-webworker, platform-webworker-dynamic, router, service-worker, upgrade, packaging, changelog',
'INVALID COMMIT MSG: "refactor(security): something"\n' +
' => ERROR: "security" is not an allowed scope.\n' +
' => SCOPES: aio, animations, bazel, benchpress, common, compiler, compiler-cli, core, elements, forms, http, language-service, platform-browser, platform-browser-dynamic, platform-server, platform-webworker, platform-webworker-dynamic, router, service-worker, upgrade, packaging, changelog',
' => SCOPES: aio, animations, bazel, benchpress, common, compiler, compiler-cli, core, forms, http, language-service, platform-browser, platform-browser-dynamic, platform-server, platform-webworker, platform-webworker-dynamic, router, service-worker, upgrade, packaging, changelog',
'INVALID COMMIT MSG: "refactor(docs): something"\n' +
' => ERROR: "docs" is not an allowed scope.\n' +
' => SCOPES: aio, animations, bazel, benchpress, common, compiler, compiler-cli, core, elements, forms, http, language-service, platform-browser, platform-browser-dynamic, platform-server, platform-webworker, platform-webworker-dynamic, router, service-worker, upgrade, packaging, changelog']
' => SCOPES: aio, animations, bazel, benchpress, common, compiler, compiler-cli, core, forms, http, language-service, platform-browser, platform-browser-dynamic, platform-server, platform-webworker, platform-webworker-dynamic, router, service-worker, upgrade, packaging, changelog']
.forEach((expectedErrorMessage, index) => {
expect(expectedErrorMessage).toEqual(errors[index]);
});

View File

@ -143,10 +143,6 @@
version "0.19.32"
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:
version "0.2.1"
resolved "https://registry.yarnpkg.com/Base64/-/Base64-0.2.1.tgz#ba3a4230708e186705065e66babdd4c35cf60028"
@ -4892,10 +4888,6 @@ multipipe@^0.1.2:
dependencies:
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:
version "0.0.5"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"