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:
parent
200d92d030
commit
3997d97806
|
@ -279,14 +279,6 @@ groups:
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
|
|
||||||
elements:
|
|
||||||
conditions:
|
|
||||||
files:
|
|
||||||
- "packages/elements/*"
|
|
||||||
users:
|
|
||||||
- mhevery #primary
|
|
||||||
- IgorMinar #fallback
|
|
||||||
|
|
||||||
benchpress:
|
benchpress:
|
||||||
conditions:
|
conditions:
|
||||||
files:
|
files:
|
||||||
|
|
|
@ -211,7 +211,6 @@ The following is the list of supported scopes:
|
||||||
* **compiler**
|
* **compiler**
|
||||||
* **compiler-cli**
|
* **compiler-cli**
|
||||||
* **core**
|
* **core**
|
||||||
* **elements**
|
|
||||||
* **forms**
|
* **forms**
|
||||||
* **http**
|
* **http**
|
||||||
* **language-service**
|
* **language-service**
|
||||||
|
|
|
@ -48,7 +48,6 @@ module.exports = new Package('angular-api', [basePackage, typeScriptPackage])
|
||||||
'common/testing/index.ts',
|
'common/testing/index.ts',
|
||||||
'core/index.ts',
|
'core/index.ts',
|
||||||
'core/testing/index.ts',
|
'core/testing/index.ts',
|
||||||
'elements/index.ts',
|
|
||||||
'forms/index.ts',
|
'forms/index.ts',
|
||||||
'http/index.ts',
|
'http/index.ts',
|
||||||
'http/testing/index.ts',
|
'http/testing/index.ts',
|
||||||
|
|
|
@ -11,16 +11,15 @@ const { API_SOURCE_PATH } = require('../config');
|
||||||
|
|
||||||
const packageMap = {
|
const packageMap = {
|
||||||
animations: ['animations/index.ts', 'animations/browser/index.ts', 'animations/browser/testing/index.ts'],
|
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'],
|
core: ['core/index.ts', 'core/testing/index.ts'],
|
||||||
elements: ['elements/index.ts'],
|
|
||||||
forms: ['forms/index.ts'],
|
forms: ['forms/index.ts'],
|
||||||
http: ['http/index.ts', 'http/testing/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': ['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-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-server': ['platform-server/index.ts', 'platform-server/testing/index.ts'],
|
||||||
'platform-webworker': ['platform-webworker/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'],
|
router: ['router/index.ts', 'router/testing/index.ts', 'router/upgrade/index.ts'],
|
||||||
'service-worker': ['service-worker/index.ts'],
|
'service-worker': ['service-worker/index.ts'],
|
||||||
upgrade: ['upgrade/index.ts', 'upgrade/static/index.ts']
|
upgrade: ['upgrade/index.ts', 'upgrade/static/index.ts']
|
||||||
|
|
3
build.sh
3
build.sh
|
@ -24,8 +24,7 @@ PACKAGES=(core
|
||||||
compiler-cli
|
compiler-cli
|
||||||
language-service
|
language-service
|
||||||
benchpress
|
benchpress
|
||||||
service-worker
|
service-worker)
|
||||||
elements)
|
|
||||||
|
|
||||||
TSC_PACKAGES=(compiler-cli
|
TSC_PACKAGES=(compiler-cli
|
||||||
language-service
|
language-service
|
||||||
|
|
|
@ -38,15 +38,6 @@ 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,7 +43,6 @@
|
||||||
"@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",
|
||||||
|
@ -82,7 +81,6 @@
|
||||||
"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",
|
||||||
|
|
|
@ -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';
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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.
|
|
|
@ -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
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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); }
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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');
|
|
|
@ -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],
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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
|
@ -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() {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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() {}
|
|
||||||
}
|
|
|
@ -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'); });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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';
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
132
test-main.js
132
test-main.js
|
@ -62,40 +62,25 @@ System.config({
|
||||||
'@angular/platform-server': {main: 'index.js', defaultExtension: 'js'},
|
'@angular/platform-server': {main: 'index.js', defaultExtension: 'js'},
|
||||||
'@angular/platform-webworker': {main: 'index.js', defaultExtension: 'js'},
|
'@angular/platform-webworker': {main: 'index.js', defaultExtension: 'js'},
|
||||||
'@angular/platform-webworker-dynamic': {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,
|
// Set up the test injector, then import all the specs, execute their `main()`
|
||||||
// execute their `main()` method and kick off Karma (Jasmine).
|
// method and kick off Karma (Jasmine).
|
||||||
Promise
|
System.import('@angular/core/testing')
|
||||||
.resolve()
|
.then(function(coreTesting) {
|
||||||
|
return Promise
|
||||||
// Load browser-specific polyfills for custom elements.
|
.all([
|
||||||
.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-dynamic/testing'),
|
||||||
System.import('@angular/platform-browser/animations')
|
System.import('@angular/platform-browser/animations')
|
||||||
]);
|
])
|
||||||
})
|
|
||||||
|
|
||||||
// Set up the test injector.
|
|
||||||
.then(function(mods) {
|
.then(function(mods) {
|
||||||
var coreTesting = mods[0];
|
|
||||||
var pbdTesting = mods[1];
|
|
||||||
var pbAnimations = mods[2];
|
|
||||||
|
|
||||||
coreTesting.TestBed.initTestEnvironment(
|
coreTesting.TestBed.initTestEnvironment(
|
||||||
[pbdTesting.BrowserDynamicTestingModule, pbAnimations.NoopAnimationsModule],
|
[mods[0].BrowserDynamicTestingModule, mods[1].NoopAnimationsModule],
|
||||||
pbdTesting.platformBrowserDynamicTesting());
|
mods[0].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.
|
||||||
|
@ -112,106 +97,9 @@ Promise
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
})
|
})
|
||||||
|
|
||||||
// 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.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) {
|
function onlySpecFiles(path) {
|
||||||
return /_spec\.js$/.test(path);
|
return /_spec\.js$/.test(path);
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,6 @@ var specFiles: any =
|
||||||
'@angular/router/test/integration/bootstrap_spec.*',
|
'@angular/router/test/integration/bootstrap_spec.*',
|
||||||
'@angular/integration_test/symbol_inspector/**',
|
'@angular/integration_test/symbol_inspector/**',
|
||||||
'@angular/upgrade/**',
|
'@angular/upgrade/**',
|
||||||
'@angular/elements/**',
|
|
||||||
'@angular/**/e2e_test/**',
|
'@angular/**/e2e_test/**',
|
||||||
'angular1_router/**',
|
'angular1_router/**',
|
||||||
'payload_tests/**',
|
'payload_tests/**',
|
||||||
|
|
|
@ -28,7 +28,7 @@ const entrypoints = [
|
||||||
'dist/packages-dist/service-worker/service-worker.d.ts',
|
'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/service-worker/config.d.ts', 'dist/packages-dist/animations/browser.d.ts',
|
||||||
'dist/packages-dist/animations/browser/testing.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';
|
const publicApiDir = 'tools/public_api_guard';
|
||||||
|
|
|
@ -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;
|
|
|
@ -21,7 +21,6 @@
|
||||||
"compiler",
|
"compiler",
|
||||||
"compiler-cli",
|
"compiler-cli",
|
||||||
"core",
|
"core",
|
||||||
"elements",
|
|
||||||
"forms",
|
"forms",
|
||||||
"http",
|
"http",
|
||||||
"language-service",
|
"language-service",
|
||||||
|
|
|
@ -51,19 +51,19 @@ describe('validate-commit-message.js', function() {
|
||||||
expect(validateMessage('refactor(docs): something')).toBe(INVALID);
|
expect(validateMessage('refactor(docs): something')).toBe(INVALID);
|
||||||
['INVALID COMMIT MSG: "fix(Compiler): something"\n' +
|
['INVALID COMMIT MSG: "fix(Compiler): something"\n' +
|
||||||
' => ERROR: "Compiler" is not an allowed scope.\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' +
|
'INVALID COMMIT MSG: "feat(bah): something"\n' +
|
||||||
' => ERROR: "bah" is not an allowed scope.\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' +
|
'INVALID COMMIT MSG: "style(webworker): something"\n' +
|
||||||
' => ERROR: "webworker" is not an allowed scope.\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' +
|
'INVALID COMMIT MSG: "refactor(security): something"\n' +
|
||||||
' => ERROR: "security" is not an allowed scope.\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' +
|
'INVALID COMMIT MSG: "refactor(docs): something"\n' +
|
||||||
' => ERROR: "docs" is not an allowed scope.\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) => {
|
.forEach((expectedErrorMessage, index) => {
|
||||||
expect(expectedErrorMessage).toEqual(errors[index]);
|
expect(expectedErrorMessage).toEqual(errors[index]);
|
||||||
});
|
});
|
||||||
|
|
|
@ -143,10 +143,6 @@
|
||||||
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"
|
||||||
|
@ -4892,10 +4888,6 @@ 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…
Reference in New Issue