feat(elements): add support for creating custom elements (#22413)
PR Close #22413
This commit is contained in:
parent
cedc04c320
commit
22b96b9690
|
@ -7,6 +7,7 @@
|
||||||
#
|
#
|
||||||
# alexeagle - Alex Eagle
|
# alexeagle - Alex Eagle
|
||||||
# alxhub - Alex Rickabaugh
|
# alxhub - Alex Rickabaugh
|
||||||
|
# andrewseguin - Andrew Seguin
|
||||||
# brocco - Mike Brocchi
|
# brocco - Mike Brocchi
|
||||||
# chuckjaz - Chuck Jazdzewski
|
# chuckjaz - Chuck Jazdzewski
|
||||||
# filipesilva - Filipe Silva
|
# filipesilva - Filipe Silva
|
||||||
|
@ -302,6 +303,16 @@ groups:
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
|
|
||||||
|
elements:
|
||||||
|
conditions:
|
||||||
|
files:
|
||||||
|
- "packages/elements/*"
|
||||||
|
users:
|
||||||
|
- andrewseguin #primary
|
||||||
|
- gkalpak
|
||||||
|
- IgorMinar #fallback
|
||||||
|
- mhevery #fallback
|
||||||
|
|
||||||
benchpress:
|
benchpress:
|
||||||
conditions:
|
conditions:
|
||||||
files:
|
files:
|
||||||
|
|
|
@ -40,6 +40,7 @@ filegroup(
|
||||||
"reflect-metadata",
|
"reflect-metadata",
|
||||||
"source-map-support",
|
"source-map-support",
|
||||||
"minimist",
|
"minimist",
|
||||||
|
"@webcomponents/webcomponentsjs",
|
||||||
"tslib",
|
"tslib",
|
||||||
] for ext in [
|
] for ext in [
|
||||||
"*.js",
|
"*.js",
|
||||||
|
|
|
@ -212,6 +212,7 @@ 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**
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
# Elements
|
||||||
|
|
||||||
|
## Release Status
|
||||||
|
|
||||||
|
**Angular Labs Project** - experimental and unstable. **Breaking Changes Possible**
|
||||||
|
|
||||||
|
Targeted to land in the [6.x release cycle](https://github.com/angular/angular/blob/master/docs/RELEASE_SCHEDULE.md) of Angular - subject to change
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Elements provides an API that allows developers to register Angular Components as Custom Elements
|
||||||
|
("Web Components"), and bridges the built-in DOM API to Angular's component interface and change
|
||||||
|
detection APIs.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
//hello-world.ts
|
||||||
|
import { Component, Input, NgModule } from '@angular/core';
|
||||||
|
import { createNgElementConstructor, getConfigFromComponentFactory } from '@angular/elements';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'hello-world',
|
||||||
|
template: `<h1>Hello {{name}}</h1>`
|
||||||
|
})
|
||||||
|
export class HelloWorld {
|
||||||
|
@Input() name: string = 'World!';
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [ HelloWorld ],
|
||||||
|
entryComponents: [ HelloWorld ]
|
||||||
|
})
|
||||||
|
export class HelloWorldModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
//app.component.ts
|
||||||
|
import { Component, NgModuleRef } from '@angular/core';
|
||||||
|
import { createNgElementConstructor } from '@angular/elements';
|
||||||
|
|
||||||
|
import { HelloWorld } from './hello-world.ngfactory';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.css']
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
constructor(ngModuleRef: NgModuleRef) {
|
||||||
|
const ngElementConfig = getConfigFromComponentFactory(HelloWorld, injector);
|
||||||
|
const NgElementConstructor = createNgElementConstructor(ngElementConfig);
|
||||||
|
customElements.register('hello-world', NgElementConstructor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
Once registered, these components can be used just like built-in HTML elements, because they *are*
|
||||||
|
HTML Elements!
|
||||||
|
|
||||||
|
They can be used in any HTML page:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<hello-world name="Angular"></hello-world>
|
||||||
|
<hello-world name="Typescript"></hello-world>
|
||||||
|
```
|
||||||
|
|
||||||
|
Custom Elements are "self-bootstrapping" - they are automatically started when they are added to the
|
||||||
|
DOM, and automatically destroyed when removed from the DOM.
|
|
@ -457,6 +457,11 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"url": "guide/elements",
|
||||||
|
"title": "Elements",
|
||||||
|
"tooltip": "Exporting Angular Components as Web Components"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Service Workers",
|
"title": "Service Workers",
|
||||||
"tooltip": "Angular service workers: Controlling caching of application resources.",
|
"tooltip": "Angular service workers: Controlling caching of application resources.",
|
||||||
|
|
|
@ -73,6 +73,7 @@ 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,15 +11,16 @@ 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: ['common/index.ts', 'common/testing/index.ts', 'common/http/index.ts', 'common/http/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,7 +24,8 @@ 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
|
||||||
|
|
|
@ -41,6 +41,15 @@ module.exports = function(config) {
|
||||||
'test-events.js',
|
'test-events.js',
|
||||||
'shims_for_IE.js',
|
'shims_for_IE.js',
|
||||||
'node_modules/systemjs/dist/system.src.js',
|
'node_modules/systemjs/dist/system.src.js',
|
||||||
|
|
||||||
|
// Serve polyfills necessary for testing the `elements` package.
|
||||||
|
{
|
||||||
|
pattern: 'node_modules/@webcomponents/custom-elements/**/*.js',
|
||||||
|
included: false,
|
||||||
|
watched: false
|
||||||
|
},
|
||||||
|
{pattern: 'node_modules/mutation-observer/index.js', included: false, watched: false},
|
||||||
|
|
||||||
{pattern: 'node_modules/rxjs/**', included: false, watched: false, served: true},
|
{pattern: 'node_modules/rxjs/**', included: false, watched: false, served: true},
|
||||||
'node_modules/reflect-metadata/Reflect.js',
|
'node_modules/reflect-metadata/Reflect.js',
|
||||||
'tools/build/file2modulename.js',
|
'tools/build/file2modulename.js',
|
||||||
|
|
|
@ -48,6 +48,8 @@
|
||||||
"@types/shelljs": "^0.7.8",
|
"@types/shelljs": "^0.7.8",
|
||||||
"@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",
|
||||||
|
"@webcomponents/webcomponentsjs": "^1.1.0",
|
||||||
"angular": "npm:angular@1.6",
|
"angular": "npm:angular@1.6",
|
||||||
"angular-1.5": "npm:angular@1.5",
|
"angular-1.5": "npm:angular@1.5",
|
||||||
"angular-mocks": "npm:angular-mocks@1.6",
|
"angular-mocks": "npm:angular-mocks@1.6",
|
||||||
|
@ -87,6 +89,7 @@
|
||||||
"karma-sourcemap-loader": "0.3.6",
|
"karma-sourcemap-loader": "0.3.6",
|
||||||
"madge": "0.5.0",
|
"madge": "0.5.0",
|
||||||
"minimist": "1.2.0",
|
"minimist": "1.2.0",
|
||||||
|
"mutation-observer": "^1.0.3",
|
||||||
"node-uuid": "1.4.8",
|
"node-uuid": "1.4.8",
|
||||||
"protractor": "5.1.2",
|
"protractor": "5.1.2",
|
||||||
"rewire": "2.5.2",
|
"rewire": "2.5.2",
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
load("//tools:defaults.bzl", "ng_module")
|
||||||
|
|
||||||
|
ng_module(
|
||||||
|
name = "elements",
|
||||||
|
srcs = glob(
|
||||||
|
[
|
||||||
|
"*.ts",
|
||||||
|
"src/**/*.ts",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
module_name = "@angular/elements",
|
||||||
|
deps = [
|
||||||
|
"//packages/core",
|
||||||
|
"//packages/platform-browser",
|
||||||
|
"@rxjs",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* @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';
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "@angular/elements",
|
||||||
|
"version": "0.0.0-PLACEHOLDER",
|
||||||
|
"description": "Angular - library for using Angular Components as Custom Elements",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* @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 {ComponentFactoryNgElementStrategy, ComponentFactoryNgElementStrategyFactory, getConfigFromComponentFactory} from './src/component-factory-strategy';
|
||||||
|
export {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from './src/element-strategy';
|
||||||
|
export {NgElement, NgElementConfig, NgElementConstructor, createNgElementConstructor} from './src/ng-element-constructor';
|
||||||
|
export {VERSION} from './src/version';
|
||||||
|
|
||||||
|
// This file only reexports content of the `src` folder. Keep it that way.
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* @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',
|
||||||
|
'rxjs/Observable': 'Rx',
|
||||||
|
'rxjs/observable/merge': 'Rx.Observable',
|
||||||
|
'rxjs/operator/map': 'Rx.Observable.prototype'
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
|
@ -0,0 +1,262 @@
|
||||||
|
/**
|
||||||
|
* @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 {Observable} from 'rxjs/Observable';
|
||||||
|
import {merge} from 'rxjs/observable/merge';
|
||||||
|
import {map} from 'rxjs/operator/map';
|
||||||
|
|
||||||
|
import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from './element-strategy';
|
||||||
|
import {extractProjectableNodes} from './extract-projectable-nodes';
|
||||||
|
import {camelToKebabCase, isFunction, scheduler, strictEquals} from './utils';
|
||||||
|
|
||||||
|
/** Time in milliseconds to wait before destroying the component ref when disconnected. */
|
||||||
|
const DESTROY_DELAY = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an NgElementConfig based on the provided component factory and injector. By default,
|
||||||
|
* the observed attributes on the NgElement will be the kebab-case version of the component inputs.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export function getConfigFromComponentFactory(
|
||||||
|
componentFactory: ComponentFactory<any>, injector: Injector) {
|
||||||
|
const attributeToPropertyInputs = new Map<string, string>();
|
||||||
|
componentFactory.inputs.forEach(({propName, templateName}) => {
|
||||||
|
const attr = camelToKebabCase(templateName);
|
||||||
|
attributeToPropertyInputs.set(attr, propName);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
strategyFactory: new ComponentFactoryNgElementStrategyFactory(componentFactory, injector),
|
||||||
|
propertyInputs: componentFactory.inputs.map(({propName}) => propName),
|
||||||
|
attributeToPropertyInputs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory that creates new ComponentFactoryNgElementStrategy instances with the strategy factory's
|
||||||
|
* injector. A new strategy instance is created with the provided component factory which will
|
||||||
|
* create its components on connect.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export class ComponentFactoryNgElementStrategyFactory implements NgElementStrategyFactory {
|
||||||
|
constructor(private componentFactory: ComponentFactory<any>, private injector: Injector) {}
|
||||||
|
|
||||||
|
create() { return new ComponentFactoryNgElementStrategy(this.componentFactory, this.injector); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and destroys a component ref using a component factory and handles change detection
|
||||||
|
* in response to input changes.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export class ComponentFactoryNgElementStrategy implements NgElementStrategy {
|
||||||
|
/** Merged stream of the component's output events. */
|
||||||
|
events: Observable<NgElementStrategyEvent>;
|
||||||
|
|
||||||
|
/** Reference to the component that was created on connect. */
|
||||||
|
private componentRef: ComponentRef<any>;
|
||||||
|
|
||||||
|
/** Changes that have been made to the component ref since the last time onChanges was called. */
|
||||||
|
private inputChanges: SimpleChanges|null = null;
|
||||||
|
|
||||||
|
/** Whether the created component implements the onChanges function. */
|
||||||
|
private implementsOnChanges = false;
|
||||||
|
|
||||||
|
/** Whether a change detection has been scheduled to run on the component. */
|
||||||
|
private scheduledChangeDetectionFn: (() => void)|null = null;
|
||||||
|
|
||||||
|
/** Callback function that when called will cancel a scheduled destruction on the component. */
|
||||||
|
private scheduledDestroyFn: (() => void)|null = null;
|
||||||
|
|
||||||
|
/** Initial input values that were set before the component was created. */
|
||||||
|
private readonly initialInputValues = new Map<string, any>();
|
||||||
|
|
||||||
|
/** Set of inputs that were not initially set when the component was created. */
|
||||||
|
private readonly uninitializedInputs = new Set<string>();
|
||||||
|
|
||||||
|
constructor(private componentFactory: ComponentFactory<any>, private injector: Injector) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new component if one has not yet been created and cancels any scheduled
|
||||||
|
* destruction.
|
||||||
|
*/
|
||||||
|
connect(element: HTMLElement) {
|
||||||
|
// If the element is marked to be destroyed, cancel the task since the component was reconnected
|
||||||
|
if (this.scheduledDestroyFn !== null) {
|
||||||
|
this.scheduledDestroyFn();
|
||||||
|
this.scheduledDestroyFn = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.componentRef) {
|
||||||
|
this.initializeComponent(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules the component to be destroyed after some small delay in case the element is just
|
||||||
|
* being moved across the DOM.
|
||||||
|
*/
|
||||||
|
disconnect() {
|
||||||
|
// Return if there is no componentRef or the component is already scheduled for destruction
|
||||||
|
if (!this.componentRef || this.scheduledDestroyFn !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule the component to be destroyed after a small timeout in case it is being
|
||||||
|
// moved elsewhere in the DOM
|
||||||
|
this.scheduledDestroyFn =
|
||||||
|
scheduler.schedule(() => { this.componentRef !.destroy(); }, DESTROY_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the component property value. If the component has not yet been created, the value is
|
||||||
|
* retrieved from the cached initialization values.
|
||||||
|
*/
|
||||||
|
getPropertyValue(property: string): any {
|
||||||
|
if (!this.componentRef) {
|
||||||
|
return this.initialInputValues.get(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (this.componentRef.instance as any)[property];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the input value for the property. If the component has not yet been created, the value is
|
||||||
|
* cached and set when the component is created.
|
||||||
|
*/
|
||||||
|
setPropertyValue(property: string, value: any): void {
|
||||||
|
if (strictEquals(value, this.getPropertyValue(property))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.componentRef) {
|
||||||
|
this.initialInputValues.set(property, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recordInputChange(property, value);
|
||||||
|
(this.componentRef.instance as any)[property] = value;
|
||||||
|
this.scheduleDetectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new component through the component factory with the provided element host and
|
||||||
|
* sets up its initial inputs, listens for outputs changes, and runs an initial change detection.
|
||||||
|
*/
|
||||||
|
protected initializeComponent(element: HTMLElement) {
|
||||||
|
const childInjector = Injector.create({providers: [], parent: this.injector});
|
||||||
|
const projectableNodes =
|
||||||
|
extractProjectableNodes(element, this.componentFactory.ngContentSelectors);
|
||||||
|
this.componentRef = this.componentFactory.create(childInjector, projectableNodes, element);
|
||||||
|
|
||||||
|
this.implementsOnChanges =
|
||||||
|
isFunction((this.componentRef.instance as any as OnChanges).ngOnChanges);
|
||||||
|
|
||||||
|
this.initializeInputs();
|
||||||
|
this.initializeOutputs();
|
||||||
|
|
||||||
|
this.detectChanges();
|
||||||
|
|
||||||
|
const applicationRef = this.injector.get<ApplicationRef>(ApplicationRef);
|
||||||
|
applicationRef.attachView(this.componentRef.hostView);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set any stored initial inputs on the component's properties. */
|
||||||
|
protected initializeInputs(): void {
|
||||||
|
this.componentFactory.inputs.forEach(({propName}) => {
|
||||||
|
const initialValue = this.initialInputValues.get(propName);
|
||||||
|
if (initialValue) {
|
||||||
|
this.setPropertyValue(propName, initialValue);
|
||||||
|
} else {
|
||||||
|
// Keep track of inputs that were not initialized in case we need to know this for
|
||||||
|
// calling ngOnChanges with SimpleChanges
|
||||||
|
this.uninitializedInputs.add(propName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.initialInputValues.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets up listeners for the component's outputs so that the events stream emits the events. */
|
||||||
|
protected initializeOutputs(): void {
|
||||||
|
const eventEmitters = this.componentFactory.outputs.map(({propName, templateName}) => {
|
||||||
|
const emitter = (this.componentRef !.instance as any)[propName] as EventEmitter<any>;
|
||||||
|
return map.call(emitter, (value: any) => ({name: templateName, value}));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.events = merge(...eventEmitters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Calls ngOnChanges with all the inputs that have changed since the last call. */
|
||||||
|
protected callNgOnChanges(): void {
|
||||||
|
if (!this.implementsOnChanges || this.inputChanges === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(this.componentRef !.instance as any as OnChanges).ngOnChanges(this.inputChanges);
|
||||||
|
this.inputChanges = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules change detection to run on the component.
|
||||||
|
* Ignores subsequent calls if already scheduled.
|
||||||
|
*/
|
||||||
|
protected scheduleDetectChanges(): void {
|
||||||
|
if (this.scheduledChangeDetectionFn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scheduledChangeDetectionFn = scheduler.scheduleBeforeRender(() => {
|
||||||
|
this.detectChanges();
|
||||||
|
this.scheduledChangeDetectionFn = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records input changes so that the component receives SimpleChanges in its onChanges function.
|
||||||
|
*/
|
||||||
|
protected recordInputChange(property: string, currentValue: any): void {
|
||||||
|
// Do not record the change if the component does not implement `OnChanges`.
|
||||||
|
if (!this.componentRef || !this.implementsOnChanges) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inputChanges === null) {
|
||||||
|
this.inputChanges = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there already is a change, modify the current value to match but leave the values for
|
||||||
|
// previousValue and isFirstChange.
|
||||||
|
const pendingChange = this.inputChanges[property];
|
||||||
|
if (pendingChange) {
|
||||||
|
pendingChange.currentValue = currentValue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFirstChange = this.uninitializedInputs.has(property);
|
||||||
|
this.uninitializedInputs.delete(property);
|
||||||
|
|
||||||
|
const previousValue = isFirstChange ? undefined : this.getPropertyValue(property);
|
||||||
|
this.inputChanges[property] = new SimpleChange(previousValue, currentValue, isFirstChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runs change detection on the component. */
|
||||||
|
protected detectChanges(): void {
|
||||||
|
if (!this.componentRef) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callNgOnChanges();
|
||||||
|
this.componentRef !.changeDetectorRef.detectChanges();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* @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} from '@angular/core';
|
||||||
|
import {Observable} from 'rxjs/Observable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for the events emitted through the NgElementStrategy.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export interface NgElementStrategyEvent {
|
||||||
|
name: string;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Underlying strategy used by the NgElement to create/destroy the component and react to input
|
||||||
|
* changes.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export interface NgElementStrategy {
|
||||||
|
events: Observable<NgElementStrategyEvent>;
|
||||||
|
|
||||||
|
connect(element: HTMLElement): void;
|
||||||
|
disconnect(): void;
|
||||||
|
getPropertyValue(propName: string): any;
|
||||||
|
setPropertyValue(propName: string, value: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory used to create new strategies for each NgElement instance.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export interface NgElementStrategyFactory { create(): NgElementStrategy; }
|
|
@ -0,0 +1,54 @@
|
||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
/**
|
||||||
|
* @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 {Subscription} from 'rxjs/Subscription';
|
||||||
|
|
||||||
|
import {NgElementStrategy, NgElementStrategyFactory} from './element-strategy';
|
||||||
|
import {createCustomEvent} from './utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class constructor based on an Angular Component to be used for custom element registration.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export interface NgElementConstructor<P> {
|
||||||
|
readonly observedAttributes: string[];
|
||||||
|
|
||||||
|
new (): NgElement&WithProperties<P>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that extends HTMLElement and implements the functionality needed for a custom element.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export abstract class NgElement extends HTMLElement {
|
||||||
|
protected ngElementStrategy: NgElementStrategy;
|
||||||
|
protected ngElementEventsSubscription: Subscription|null = null;
|
||||||
|
|
||||||
|
abstract attributeChangedCallback(
|
||||||
|
attrName: string, oldValue: string|null, newValue: string, namespace?: string): void;
|
||||||
|
abstract connectedCallback(): void;
|
||||||
|
abstract disconnectedCallback(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional type information that can be added to the NgElement class for properties added based
|
||||||
|
* on the inputs and methods of the underlying component.
|
||||||
|
*/
|
||||||
|
export type WithProperties<P> = {
|
||||||
|
[property in keyof P]: P[property]
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialization configuration for the NgElementConstructor. Provides the strategy factory
|
||||||
|
* that produces a strategy for each instantiated element. Additionally, provides a function
|
||||||
|
* that takes the component factory and provides a map of which attributes should be observed on
|
||||||
|
* the element and which property they are associated with.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export interface NgElementConfig {
|
||||||
|
strategyFactory: NgElementStrategyFactory;
|
||||||
|
propertyInputs: string[];
|
||||||
|
attributeToPropertyInputs: Map<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Creates a custom element class based on an Angular Component. Takes a configuration
|
||||||
|
* that provides initialization information to the created class. E.g. the configuration's injector
|
||||||
|
* will be the initial injector set on the class which will be used for each created instance.
|
||||||
|
*
|
||||||
|
* @description Builds a class that encapsulates the functionality of the provided component and
|
||||||
|
* uses the config's information to provide more context to the class. Takes the component factory's
|
||||||
|
* inputs and outputs to convert them to the proper custom element API and add hooks to input
|
||||||
|
* changes. Passes the config's injector to each created instance (may be overriden with the
|
||||||
|
* static property to affect all newly created instances, or as a constructor argument for
|
||||||
|
* one-off creations).
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export function createNgElementConstructor<P>(config: NgElementConfig): NgElementConstructor<P> {
|
||||||
|
class NgElementImpl extends NgElement {
|
||||||
|
static readonly observedAttributes = Array.from(config.attributeToPropertyInputs.keys());
|
||||||
|
|
||||||
|
constructor(strategyFactoryOverride?: NgElementStrategyFactory) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Use the constructor's strategy factory override if it is present, otherwise default to
|
||||||
|
// the config's factory.
|
||||||
|
const strategyFactory = strategyFactoryOverride || config.strategyFactory;
|
||||||
|
this.ngElementStrategy = strategyFactory.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(
|
||||||
|
attrName: string, oldValue: string|null, newValue: string, namespace?: string): void {
|
||||||
|
const propName = config.attributeToPropertyInputs.get(attrName) !;
|
||||||
|
this.ngElementStrategy.setPropertyValue(propName, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
// Take element attribute inputs and set them as inputs on the strategy
|
||||||
|
config.attributeToPropertyInputs.forEach((propName, attrName) => {
|
||||||
|
const value = this.getAttribute(attrName);
|
||||||
|
if (value) {
|
||||||
|
this.ngElementStrategy.setPropertyValue(propName, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ngElementStrategy.connect(this);
|
||||||
|
|
||||||
|
// Listen for events from the strategy and dispatch them as custom events
|
||||||
|
this.ngElementEventsSubscription = this.ngElementStrategy.events.subscribe(e => {
|
||||||
|
const customEvent = createCustomEvent(this.ownerDocument, e.name, e.value);
|
||||||
|
this.dispatchEvent(customEvent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
this.ngElementStrategy.disconnect();
|
||||||
|
|
||||||
|
if (this.ngElementEventsSubscription) {
|
||||||
|
this.ngElementEventsSubscription.unsubscribe();
|
||||||
|
this.ngElementEventsSubscription = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add getters and setters for each input defined on the Angular Component so that the input
|
||||||
|
// changes can be known.
|
||||||
|
config.propertyInputs.forEach(property => {
|
||||||
|
Object.defineProperty(NgElementImpl.prototype, property, {
|
||||||
|
get: function() { return this.ngElementStrategy.getPropertyValue(property); },
|
||||||
|
set: function(newValue: any) { this.ngElementStrategy.setPropertyValue(property, newValue); },
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (NgElementImpl as any) as NgElementConstructor<P>;
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
/**
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* Returns a function that when executed will cancel the scheduled function.
|
||||||
|
*/
|
||||||
|
schedule(taskFn: () => void, delay: number): () =>
|
||||||
|
void{const id = window.setTimeout(taskFn, 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.)
|
||||||
|
*
|
||||||
|
* Returns a function that when executed will cancel the scheduled function.
|
||||||
|
*/
|
||||||
|
scheduleBeforeRender(taskFn: () => 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') {
|
||||||
|
const frameMs = 16;
|
||||||
|
return scheduler.schedule(taskFn, frameMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = window.requestAnimationFrame(taskFn);
|
||||||
|
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});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* @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 common package.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Version} from '@angular/core';
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export const VERSION = new Version('0.0.0-PLACEHOLDER');
|
|
@ -0,0 +1,49 @@
|
||||||
|
load("//tools:defaults.bzl", "ts_library")
|
||||||
|
load("@build_bazel_rules_typescript//:defs.bzl", "ts_web_test")
|
||||||
|
load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test")
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "test_lib",
|
||||||
|
testonly = 1,
|
||||||
|
srcs = glob(["**/*.ts"]),
|
||||||
|
deps = [
|
||||||
|
"//packages:types",
|
||||||
|
"//packages/compiler",
|
||||||
|
"//packages/core",
|
||||||
|
"//packages/core/testing",
|
||||||
|
"//packages/elements",
|
||||||
|
"//packages/elements/testing",
|
||||||
|
"//packages/platform-browser",
|
||||||
|
"//packages/platform-browser-dynamic",
|
||||||
|
"//packages/platform-browser-dynamic/testing",
|
||||||
|
"//packages/platform-browser/testing",
|
||||||
|
"@rxjs",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "elements_test_bootstrap_scripts",
|
||||||
|
# do not sort
|
||||||
|
srcs = [
|
||||||
|
"//:node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js",
|
||||||
|
"//:node_modules/reflect-metadata/Reflect.js",
|
||||||
|
"//:node_modules/zone.js/dist/zone.js",
|
||||||
|
"//:node_modules/zone.js/dist/async-test.js",
|
||||||
|
"//:node_modules/zone.js/dist/sync-test.js",
|
||||||
|
"//:node_modules/zone.js/dist/fake-async-test.js",
|
||||||
|
"//:node_modules/zone.js/dist/proxy.js",
|
||||||
|
"//:node_modules/zone.js/dist/jasmine-patch.js",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
ts_web_test(
|
||||||
|
name = "test",
|
||||||
|
bootstrap = [
|
||||||
|
":elements_test_bootstrap_scripts",
|
||||||
|
],
|
||||||
|
# do not sort
|
||||||
|
deps = [
|
||||||
|
"//tools/testing:browser",
|
||||||
|
":test_lib",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,78 @@
|
||||||
|
/**
|
||||||
|
* @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, ComponentRef, Injector, NgModuleRef, Type} from '@angular/core';
|
||||||
|
import {Subject} from 'rxjs/Subject';
|
||||||
|
|
||||||
|
import {ComponentFactoryNgElementStrategy} from '../src/component-factory-strategy';
|
||||||
|
|
||||||
|
describe('ComponentFactoryNgElementStrategy', () => {
|
||||||
|
let factory: FakeComponentFactory;
|
||||||
|
let component: FakeComponent;
|
||||||
|
let strategy: ComponentFactoryNgElementStrategy;
|
||||||
|
let injector;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
factory = new FakeComponentFactory();
|
||||||
|
component = factory.componentRef;
|
||||||
|
|
||||||
|
injector = jasmine.createSpyObj('injector', ['get']);
|
||||||
|
const applicationRef = jasmine.createSpyObj('applicationRef', ['attachView']);
|
||||||
|
injector.get.and.returnValue(applicationRef);
|
||||||
|
|
||||||
|
strategy = new ComponentFactoryNgElementStrategy(factory, injector);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('connect', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
strategy.connect(element);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(andrewseguin): Test everything
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export class FakeComponent {
|
||||||
|
output1 = new Subject();
|
||||||
|
output2 = new Subject();
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FakeComponentFactory extends ComponentFactory<any> {
|
||||||
|
componentRef = jasmine.createSpyObj('componentRef', ['instance', 'changeDetectorRef']);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.componentRef.instance = new FakeComponent();
|
||||||
|
this.componentRef.changeDetectorRef =
|
||||||
|
jasmine.createSpyObj('changeDetectorRef', ['detectChanges']);
|
||||||
|
}
|
||||||
|
|
||||||
|
get selector(): string { return 'fake-component'; }
|
||||||
|
get componentType(): Type<any> { return FakeComponent; }
|
||||||
|
get ngContentSelectors(): string[] { return ['content-1', 'content-2']; }
|
||||||
|
get inputs(): {propName: string; templateName: string}[] {
|
||||||
|
return [
|
||||||
|
{propName: 'input1', templateName: 'templateInput1'},
|
||||||
|
{propName: 'input1', templateName: 'templateInput2'},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
get outputs(): {propName: string; templateName: string}[] {
|
||||||
|
return [
|
||||||
|
{propName: 'output1', templateName: 'templateOutput1'},
|
||||||
|
{propName: 'output2', templateName: 'templateOutput2'},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
create(
|
||||||
|
injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string|any,
|
||||||
|
ngModule?: NgModuleRef<any>): ComponentRef<any> {
|
||||||
|
return this.componentRef;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
/**
|
||||||
|
* @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';
|
||||||
|
|
||||||
|
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],
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,186 @@
|
||||||
|
/**
|
||||||
|
* @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 {Component, ComponentFactory, EventEmitter, Input, NgModule, Output, destroyPlatform} from '@angular/core';
|
||||||
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
|
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||||
|
import {Subject} from 'rxjs/Subject';
|
||||||
|
|
||||||
|
import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from '../src/element-strategy';
|
||||||
|
import {NgElementConfig, NgElementConstructor, createNgElementConstructor} from '../src/ng-element-constructor';
|
||||||
|
import {patchEnv, restoreEnv} from '../testing/index';
|
||||||
|
|
||||||
|
type WithFooBar = {
|
||||||
|
fooFoo: string,
|
||||||
|
barBar: string
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof customElements !== 'undefined') {
|
||||||
|
describe('createNgElementConstructor', () => {
|
||||||
|
let NgElementCtor: NgElementConstructor<WithFooBar>;
|
||||||
|
let factory: ComponentFactory<TestComponent>;
|
||||||
|
let strategy: TestStrategy;
|
||||||
|
let strategyFactory: TestStrategyFactory;
|
||||||
|
|
||||||
|
beforeAll(() => patchEnv());
|
||||||
|
beforeAll(done => {
|
||||||
|
destroyPlatform();
|
||||||
|
platformBrowserDynamic()
|
||||||
|
.bootstrapModule(TestModule)
|
||||||
|
.then(ref => {
|
||||||
|
factory = ref.componentFactoryResolver.resolveComponentFactory(TestComponent);
|
||||||
|
strategyFactory = new TestStrategyFactory();
|
||||||
|
strategy = strategyFactory.testStrategy;
|
||||||
|
|
||||||
|
const config: NgElementConfig = {
|
||||||
|
strategyFactory,
|
||||||
|
propertyInputs: ['fooFoo', 'barBar'],
|
||||||
|
attributeToPropertyInputs:
|
||||||
|
new Map<string, string>([['foo-foo', 'fooFoo'], ['barbar', 'barBar']])
|
||||||
|
};
|
||||||
|
NgElementCtor = createNgElementConstructor(config);
|
||||||
|
|
||||||
|
// The `@webcomponents/custom-elements/src/native-shim.js` polyfill allows us to create
|
||||||
|
// new instances of the NgElement which extends HTMLElement, as long as we define it.
|
||||||
|
customElements.define('test-element', NgElementCtor);
|
||||||
|
})
|
||||||
|
.then(done, done.fail);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => destroyPlatform());
|
||||||
|
afterAll(() => restoreEnv());
|
||||||
|
|
||||||
|
it('should use a default strategy for converting component inputs', () => {
|
||||||
|
expect(NgElementCtor.observedAttributes).toEqual(['foo-foo', 'barbar']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send input values from attributes when connected', () => {
|
||||||
|
const element = new NgElementCtor();
|
||||||
|
element.setAttribute('foo-foo', 'value-foo-foo');
|
||||||
|
element.setAttribute('barbar', 'value-barbar');
|
||||||
|
element.connectedCallback();
|
||||||
|
expect(strategy.connectedElement).toBe(element);
|
||||||
|
|
||||||
|
expect(strategy.getPropertyValue('fooFoo')).toBe('value-foo-foo');
|
||||||
|
expect(strategy.getPropertyValue('barBar')).toBe('value-barbar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should listen to output events after connected', () => {
|
||||||
|
const element = new NgElementCtor();
|
||||||
|
element.connectedCallback();
|
||||||
|
|
||||||
|
let eventValue: any = null;
|
||||||
|
element.addEventListener('some-event', (e: CustomEvent) => eventValue = e.detail);
|
||||||
|
strategy.events.next({name: 'some-event', value: 'event-value'});
|
||||||
|
|
||||||
|
expect(eventValue).toEqual('event-value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not listen to output events after disconnected', () => {
|
||||||
|
const element = new NgElementCtor();
|
||||||
|
element.connectedCallback();
|
||||||
|
element.disconnectedCallback();
|
||||||
|
expect(strategy.disconnectCalled).toBe(true);
|
||||||
|
|
||||||
|
let eventValue: any = null;
|
||||||
|
element.addEventListener('some-event', (e: CustomEvent) => eventValue = e.detail);
|
||||||
|
strategy.events.next({name: 'some-event', value: 'event-value'});
|
||||||
|
|
||||||
|
expect(eventValue).toEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set getters/setters on the element', () => {
|
||||||
|
const element = new NgElementCtor();
|
||||||
|
element.fooFoo = 'foo-foo-value';
|
||||||
|
element.barBar = 'barBar-value';
|
||||||
|
|
||||||
|
expect(strategy.inputs.get('fooFoo')).toBe('foo-foo-value');
|
||||||
|
expect(strategy.inputs.get('barBar')).toBe('barBar-value');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with different attribute strategy', () => {
|
||||||
|
let NgElementCtorWithChangedAttr: NgElementConstructor<WithFooBar>;
|
||||||
|
let element: HTMLElement;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
strategyFactory = new TestStrategyFactory();
|
||||||
|
strategy = strategyFactory.testStrategy;
|
||||||
|
NgElementCtorWithChangedAttr = createNgElementConstructor({
|
||||||
|
strategyFactory: strategyFactory,
|
||||||
|
propertyInputs: ['prop1', 'prop2'],
|
||||||
|
attributeToPropertyInputs:
|
||||||
|
new Map<string, string>([['attr-1', 'prop1'], ['attr-2', 'prop2']])
|
||||||
|
});
|
||||||
|
|
||||||
|
customElements.define('test-element-with-changed-attributes', NgElementCtorWithChangedAttr);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => { element = new NgElementCtorWithChangedAttr(); });
|
||||||
|
|
||||||
|
it('should affect which attributes are watched', () => {
|
||||||
|
expect(NgElementCtorWithChangedAttr.observedAttributes).toEqual(['attr-1', 'attr-2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send attribute values as inputs when connected', () => {
|
||||||
|
const element = new NgElementCtorWithChangedAttr();
|
||||||
|
element.setAttribute('attr-1', 'value-1');
|
||||||
|
element.setAttribute('attr-2', 'value-2');
|
||||||
|
element.setAttribute('attr-3', 'value-3'); // Made-up attribute
|
||||||
|
element.connectedCallback();
|
||||||
|
|
||||||
|
expect(strategy.getPropertyValue('prop1')).toBe('value-1');
|
||||||
|
expect(strategy.getPropertyValue('prop2')).toBe('value-2');
|
||||||
|
expect(strategy.getPropertyValue('prop3')).not.toBe('value-3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
@Component({
|
||||||
|
selector: 'test-component',
|
||||||
|
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>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [BrowserModule],
|
||||||
|
declarations: [TestComponent],
|
||||||
|
entryComponents: [TestComponent],
|
||||||
|
})
|
||||||
|
class TestModule {
|
||||||
|
ngDoBootstrap() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestStrategy implements NgElementStrategy {
|
||||||
|
connectedElement: HTMLElement|null = null;
|
||||||
|
disconnectCalled = false;
|
||||||
|
inputs = new Map<string, any>();
|
||||||
|
|
||||||
|
events = new Subject<NgElementStrategyEvent>();
|
||||||
|
|
||||||
|
connect(element: HTMLElement): void { this.connectedElement = element; }
|
||||||
|
|
||||||
|
disconnect(): void { this.disconnectCalled = true; }
|
||||||
|
|
||||||
|
getPropertyValue(propName: string): any { return this.inputs.get(propName); }
|
||||||
|
|
||||||
|
setPropertyValue(propName: string, value: string): void { this.inputs.set(propName, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestStrategyFactory implements NgElementStrategyFactory {
|
||||||
|
testStrategy = new TestStrategy();
|
||||||
|
|
||||||
|
create(): NgElementStrategy { return this.testStrategy; }
|
||||||
|
}
|
|
@ -0,0 +1,231 @@
|
||||||
|
/**
|
||||||
|
* @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 {camelToKebabCase, createCustomEvent, isElement, isFunction, kebabToCamelCase, matchesSelector, scheduler, strictEquals} from '../src/utils';
|
||||||
|
|
||||||
|
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('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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,19 @@
|
||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
load("//tools:defaults.bzl", "ts_library")
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "testing",
|
||||||
|
srcs = glob(
|
||||||
|
[
|
||||||
|
"*.ts",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
module_name = "@angular/elements/testing",
|
||||||
|
deps = [
|
||||||
|
"//packages/core",
|
||||||
|
"//packages/elements",
|
||||||
|
"//packages/platform-browser",
|
||||||
|
"@rxjs",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,115 @@
|
||||||
|
/**
|
||||||
|
* @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';
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig-build.json",
|
||||||
|
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"rootDir": ".",
|
||||||
|
"paths": {
|
||||||
|
"@angular/core": ["../../dist/packages/core"],
|
||||||
|
"@angular/platform-browser": ["../../dist/packages/platform-browser"],
|
||||||
|
"rxjs/*": ["../../node_modules/rxjs/*"]
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
138
test-main.js
138
test-main.js
|
@ -65,25 +65,40 @@ 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'},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Set up the test injector, then import all the specs, execute their `main()`
|
// Load browser-specific CustomElement polyfills, set up the test injector, import all the specs,
|
||||||
// method and kick off Karma (Jasmine).
|
// execute their `main()` method and kick off Karma (Jasmine).
|
||||||
System.import('@angular/core/testing')
|
Promise
|
||||||
.then(function(coreTesting) {
|
.resolve()
|
||||||
return Promise
|
|
||||||
.all([
|
// Load browser-specific polyfills for custom elements.
|
||||||
System.import('@angular/platform-browser-dynamic/testing'),
|
.then(function() { return loadCustomElementsPolyfills(); })
|
||||||
System.import('@angular/platform-browser/animations')
|
|
||||||
])
|
// Load necessary testing packages.
|
||||||
.then(function(mods) {
|
.then(function() {
|
||||||
coreTesting.TestBed.initTestEnvironment(
|
return Promise.all([
|
||||||
[mods[0].BrowserDynamicTestingModule, mods[1].NoopAnimationsModule],
|
System.import('@angular/core/testing'),
|
||||||
mods[0].platformBrowserDynamicTesting());
|
System.import('@angular/platform-browser-dynamic/testing'),
|
||||||
});
|
System.import('@angular/platform-browser/animations')
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Set up the test injector.
|
||||||
|
.then(function(mods) {
|
||||||
|
var coreTesting = mods[0];
|
||||||
|
var pbdTesting = mods[1];
|
||||||
|
var pbAnimations = mods[2];
|
||||||
|
|
||||||
|
coreTesting.TestBed.initTestEnvironment(
|
||||||
|
[pbdTesting.BrowserDynamicTestingModule, pbAnimations.NoopAnimationsModule],
|
||||||
|
pbdTesting.platformBrowserDynamicTesting());
|
||||||
|
})
|
||||||
|
|
||||||
|
// Import all the specs and execute their `main()` method.
|
||||||
.then(function() {
|
.then(function() {
|
||||||
return Promise.all(Object
|
return Promise.all(Object
|
||||||
.keys(window.__karma__.files) // All files served by Karma.
|
.keys(window.__karma__.files) // All files served by Karma.
|
||||||
|
@ -97,9 +112,104 @@ System.import('@angular/core/testing')
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Kick off karma (Jasmine).
|
||||||
.then(function() { __karma__.start(); }, function(error) { console.error(error); });
|
.then(function() { __karma__.start(); }, function(error) { console.error(error); });
|
||||||
|
|
||||||
|
|
||||||
|
function loadCustomElementsPolyfills() {
|
||||||
|
var loadedPromise = Promise.resolve();
|
||||||
|
|
||||||
|
// The custom elements polyfill relies on `MutationObserver`.
|
||||||
|
if (!window.MutationObserver) {
|
||||||
|
loadedPromise =
|
||||||
|
loadedPromise
|
||||||
|
.then(function() { return System.import('node_modules/mutation-observer/index.js'); })
|
||||||
|
.then(function(MutationObserver) { window.MutationObserver = MutationObserver; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// The custom elements polyfill relies on `Object.setPrototypeOf()`.
|
||||||
|
if (!Object.setPrototypeOf) {
|
||||||
|
var getDescriptor = function getDescriptor(obj, prop) {
|
||||||
|
var descriptor;
|
||||||
|
while (obj && !descriptor) {
|
||||||
|
descriptor = Object.getOwnPropertyDescriptor(obj, prop);
|
||||||
|
obj = Object.getPrototypeOf(obj);
|
||||||
|
}
|
||||||
|
return descriptor || {};
|
||||||
|
};
|
||||||
|
var setPrototypeOf = function setPrototypeOf(obj, proto) {
|
||||||
|
for (var prop in proto) {
|
||||||
|
if (!obj.hasOwnProperty(prop)) {
|
||||||
|
Object.defineProperty(obj, prop, getDescriptor(proto, prop));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.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) {
|
||||||
|
['Element', 'HTMLElement']
|
||||||
|
.map(function(name) { return window[name].prototype; })
|
||||||
|
.some(function(candidatePatchTarget) {
|
||||||
|
var candidateOriginalDescriptor =
|
||||||
|
Object.getOwnPropertyDescriptor(candidatePatchTarget, 'innerHTML');
|
||||||
|
|
||||||
|
if (candidateOriginalDescriptor) {
|
||||||
|
patchTarget = candidatePatchTarget;
|
||||||
|
originalDescriptor = candidateOriginalDescriptor;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,7 @@ 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/**',
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
/** @experimental */
|
||||||
|
export declare class ComponentFactoryNgElementStrategy implements NgElementStrategy {
|
||||||
|
events: Observable<NgElementStrategyEvent>;
|
||||||
|
constructor(componentFactory: ComponentFactory<any>, injector: Injector);
|
||||||
|
protected callNgOnChanges(): void;
|
||||||
|
connect(element: HTMLElement): void;
|
||||||
|
protected detectChanges(): void;
|
||||||
|
disconnect(): void;
|
||||||
|
getPropertyValue(property: string): any;
|
||||||
|
protected initializeComponent(element: HTMLElement): void;
|
||||||
|
protected initializeInputs(): void;
|
||||||
|
protected initializeOutputs(): void;
|
||||||
|
protected recordInputChange(property: string, currentValue: any): void;
|
||||||
|
protected scheduleDetectChanges(): void;
|
||||||
|
setPropertyValue(property: string, value: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export declare class ComponentFactoryNgElementStrategyFactory implements NgElementStrategyFactory {
|
||||||
|
constructor(componentFactory: ComponentFactory<any>, injector: Injector);
|
||||||
|
create(): ComponentFactoryNgElementStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export declare function createNgElementConstructor<P>(config: NgElementConfig): NgElementConstructor<P>;
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export declare function getConfigFromComponentFactory(componentFactory: ComponentFactory<any>, injector: Injector): {
|
||||||
|
strategyFactory: ComponentFactoryNgElementStrategyFactory;
|
||||||
|
propertyInputs: string[];
|
||||||
|
attributeToPropertyInputs: Map<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export declare abstract class NgElement extends HTMLElement {
|
||||||
|
protected ngElementEventsSubscription: Subscription | null;
|
||||||
|
protected ngElementStrategy: NgElementStrategy;
|
||||||
|
abstract attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string, namespace?: string): void;
|
||||||
|
abstract connectedCallback(): void;
|
||||||
|
abstract disconnectedCallback(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export interface NgElementConfig {
|
||||||
|
attributeToPropertyInputs: Map<string, string>;
|
||||||
|
propertyInputs: string[];
|
||||||
|
strategyFactory: NgElementStrategyFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export interface NgElementConstructor<P> {
|
||||||
|
readonly observedAttributes: string[];
|
||||||
|
new (): NgElement & WithProperties<P>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export interface NgElementStrategy {
|
||||||
|
events: Observable<NgElementStrategyEvent>;
|
||||||
|
connect(element: HTMLElement): void;
|
||||||
|
disconnect(): void;
|
||||||
|
getPropertyValue(propName: string): any;
|
||||||
|
setPropertyValue(propName: string, value: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export interface NgElementStrategyEvent {
|
||||||
|
name: string;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export interface NgElementStrategyFactory {
|
||||||
|
create(): NgElementStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export declare const VERSION: Version;
|
|
@ -21,6 +21,7 @@
|
||||||
"compiler",
|
"compiler",
|
||||||
"compiler-cli",
|
"compiler-cli",
|
||||||
"core",
|
"core",
|
||||||
|
"elements",
|
||||||
"forms",
|
"forms",
|
||||||
"http",
|
"http",
|
||||||
"ivy",
|
"ivy",
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -156,6 +156,14 @@
|
||||||
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.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@webcomponents/custom-elements/-/custom-elements-1.0.8.tgz#b7b8ef7248f7681d1ad4286a0ada5fe3c2bc7228"
|
||||||
|
|
||||||
|
"@webcomponents/webcomponentsjs@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-1.1.0.tgz#1392799c266fca142622a720176f688beb74d181"
|
||||||
|
|
||||||
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"
|
||||||
|
@ -4909,6 +4917,10 @@ multipipe@^0.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
duplexer2 "0.0.2"
|
duplexer2 "0.0.2"
|
||||||
|
|
||||||
|
mutation-observer@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/mutation-observer/-/mutation-observer-1.0.3.tgz#42e9222b101bca82e5ba9d5a7acf4a14c0f263d0"
|
||||||
|
|
||||||
mute-stream@0.0.5:
|
mute-stream@0.0.5:
|
||||||
version "0.0.5"
|
version "0.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
|
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
|
||||||
|
|
Loading…
Reference in New Issue