@ -11,7 +11,7 @@
|
||||
* @description
|
||||
* Entry point for all public APIs of the `elements` package.
|
||||
*/
|
||||
export {NgElement, NgElementConfig, NgElementConstructor, WithProperties, createCustomElement} from './src/create-custom-element';
|
||||
export {createCustomElement, NgElement, NgElementConfig, NgElementConstructor, WithProperties} from './src/create-custom-element';
|
||||
export {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from './src/element-strategy';
|
||||
export {VERSION} from './src/version';
|
||||
|
||||
|
@ -5,9 +5,9 @@
|
||||
* 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 {Rule, SchematicContext, SchematicsException, Tree, chain, noop} from '@angular-devkit/schematics';
|
||||
import {chain, noop, Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||
import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks';
|
||||
import {NodeDependencyType, addPackageJsonDependency} from '@schematics/angular/utility/dependencies';
|
||||
import {addPackageJsonDependency, NodeDependencyType} from '@schematics/angular/utility/dependencies';
|
||||
import {getWorkspace} from '@schematics/angular/utility/workspace';
|
||||
|
||||
import {Schema} from './schema';
|
||||
@ -36,7 +36,7 @@ function addPolyfillDependency(): Rule {
|
||||
|
||||
/** Adds the document-register-element.js to the polyfills file. */
|
||||
function addPolyfill(options: Schema): Rule {
|
||||
return async(host: Tree, context: SchematicContext) => {
|
||||
return async (host: Tree, context: SchematicContext) => {
|
||||
const projectName = options.project;
|
||||
|
||||
if (!projectName) {
|
||||
|
@ -13,7 +13,9 @@ import {Schema as ElementsOptions} from './schema';
|
||||
// tslint:disable:max-line-length
|
||||
describe('Elements Schematics', () => {
|
||||
const schematicRunner = new SchematicTestRunner(
|
||||
'@angular/elements', path.join(__dirname, '../test-collection.json'), );
|
||||
'@angular/elements',
|
||||
path.join(__dirname, '../test-collection.json'),
|
||||
);
|
||||
const defaultOptions: ElementsOptions = {project: 'elements', skipPackageJson: false};
|
||||
|
||||
let appTree: UnitTestTree;
|
||||
@ -35,7 +37,7 @@ describe('Elements Schematics', () => {
|
||||
skipTests: false,
|
||||
};
|
||||
|
||||
beforeEach(async() => {
|
||||
beforeEach(async () => {
|
||||
appTree = await schematicRunner
|
||||
.runExternalSchematicAsync('@schematics/angular', 'workspace', workspaceOptions)
|
||||
.toPromise();
|
||||
@ -45,14 +47,14 @@ describe('Elements Schematics', () => {
|
||||
.toPromise();
|
||||
});
|
||||
|
||||
it('should run the ng-add schematic', async() => {
|
||||
it('should run the ng-add schematic', async () => {
|
||||
const tree =
|
||||
await schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise();
|
||||
expect(tree.readContent('/projects/elements/src/polyfills.ts'))
|
||||
.toContain(`import 'document-register-element';`);
|
||||
});
|
||||
|
||||
it('should add polyfill as a dependency in package.json', async() => {
|
||||
it('should add polyfill as a dependency in package.json', async () => {
|
||||
const tree =
|
||||
await schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise();
|
||||
const pkgJsonText = tree.readContent('/package.json');
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {ApplicationRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, Type} from '@angular/core';
|
||||
import {Observable, merge} from 'rxjs';
|
||||
import {merge, Observable} from 'rxjs';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from './element-strategy';
|
||||
@ -45,11 +45,11 @@ export class ComponentNgElementStrategyFactory implements NgElementStrategyFacto
|
||||
export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
/** Merged stream of the component's output events. */
|
||||
// TODO(issue/24571): remove '!'.
|
||||
events !: Observable<NgElementStrategyEvent>;
|
||||
events!: Observable<NgElementStrategyEvent>;
|
||||
|
||||
/** Reference to the component that was created on connect. */
|
||||
// TODO(issue/24571): remove '!'.
|
||||
private componentRef !: ComponentRef<any>| null;
|
||||
private componentRef!: ComponentRef<any>|null;
|
||||
|
||||
/** Changes that have been made to the component ref since the last time onChanges was called. */
|
||||
private inputChanges: SimpleChanges|null = null;
|
||||
@ -105,7 +105,7 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
// moved elsewhere in the DOM
|
||||
this.scheduledDestroyFn = scheduler.schedule(() => {
|
||||
if (this.componentRef) {
|
||||
this.componentRef !.destroy();
|
||||
this.componentRef!.destroy();
|
||||
this.componentRef = null;
|
||||
}
|
||||
}, DESTROY_DELAY);
|
||||
@ -190,7 +190,7 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
/** 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>;
|
||||
const emitter = (this.componentRef!.instance as any)[propName] as EventEmitter<any>;
|
||||
return emitter.pipe(map((value: any) => ({name: templateName, value})));
|
||||
});
|
||||
|
||||
@ -207,7 +207,7 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
// during ngOnChanges.
|
||||
const inputChanges = this.inputChanges;
|
||||
this.inputChanges = null;
|
||||
(this.componentRef !.instance as any as OnChanges).ngOnChanges(inputChanges);
|
||||
(this.componentRef!.instance as any as OnChanges).ngOnChanges(inputChanges);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -260,6 +260,6 @@ export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||
}
|
||||
|
||||
this.callNgOnChanges();
|
||||
this.componentRef !.changeDetectorRef.detectChanges();
|
||||
this.componentRef!.changeDetectorRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ export interface NgElementConstructor<P> {
|
||||
* Initializes a constructor instance.
|
||||
* @param injector If provided, overrides the configured injector.
|
||||
*/
|
||||
new (injector?: Injector): NgElement&WithProperties<P>;
|
||||
new(injector?: Injector): NgElement&WithProperties<P>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -44,20 +44,20 @@ export abstract class NgElement extends HTMLElement {
|
||||
* The strategy that controls how a component is transformed in a custom element.
|
||||
*/
|
||||
// TODO(issue/24571): remove '!'.
|
||||
protected ngElementStrategy !: NgElementStrategy;
|
||||
protected ngElementStrategy!: NgElementStrategy;
|
||||
/**
|
||||
* A subscription to change, connect, and disconnect events in the custom element.
|
||||
*/
|
||||
protected ngElementEventsSubscription: Subscription|null = null;
|
||||
|
||||
/**
|
||||
* Prototype for a handler that responds to a change in an observed attribute.
|
||||
* @param attrName The name of the attribute that has changed.
|
||||
* @param oldValue The previous value of the attribute.
|
||||
* @param newValue The new value of the attribute.
|
||||
* @param namespace The namespace in which the attribute is defined.
|
||||
* @returns Nothing.
|
||||
*/
|
||||
* Prototype for a handler that responds to a change in an observed attribute.
|
||||
* @param attrName The name of the attribute that has changed.
|
||||
* @param oldValue The previous value of the attribute.
|
||||
* @param newValue The new value of the attribute.
|
||||
* @param namespace The namespace in which the attribute is defined.
|
||||
* @returns Nothing.
|
||||
*/
|
||||
abstract attributeChangedCallback(
|
||||
attrName: string, oldValue: string|null, newValue: string, namespace?: string): void;
|
||||
/**
|
||||
@ -152,7 +152,7 @@ export function createCustomElement<P>(
|
||||
this.ngElementStrategy = strategyFactory.create(config.injector);
|
||||
}
|
||||
|
||||
const propName = attributeToPropertyInputs[attrName] !;
|
||||
const propName = attributeToPropertyInputs[attrName]!;
|
||||
this.ngElementStrategy.setInputValue(propName, newValue);
|
||||
}
|
||||
|
||||
@ -165,7 +165,7 @@ export function createCustomElement<P>(
|
||||
|
||||
// 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);
|
||||
const customEvent = createCustomEvent(this.ownerDocument!, e.name, e.value);
|
||||
this.dispatchEvent(customEvent);
|
||||
});
|
||||
}
|
||||
@ -186,8 +186,12 @@ export function createCustomElement<P>(
|
||||
// contain property inputs, use all inputs by default.
|
||||
inputs.map(({propName}) => propName).forEach(property => {
|
||||
Object.defineProperty(NgElementImpl.prototype, property, {
|
||||
get: function() { return this.ngElementStrategy.getInputValue(property); },
|
||||
set: function(newValue: any) { this.ngElementStrategy.setInputValue(property, newValue); },
|
||||
get: function() {
|
||||
return this.ngElementStrategy.getInputValue(property);
|
||||
},
|
||||
set: function(newValue: any) {
|
||||
this.ngElementStrategy.setInputValue(property, newValue);
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
|
@ -22,8 +22,10 @@ export const scheduler = {
|
||||
*
|
||||
* Returns a function that when executed will cancel the scheduled function.
|
||||
*/
|
||||
schedule(taskFn: () => void, delay: number): () =>
|
||||
void{const id = setTimeout(taskFn, delay); return () => clearTimeout(id);},
|
||||
schedule(taskFn: () => void, delay: number): () => void {
|
||||
const id = setTimeout(taskFn, delay);
|
||||
return () => clearTimeout(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Schedule a callback to be called before the next render.
|
||||
@ -31,7 +33,7 @@ export const scheduler = {
|
||||
*
|
||||
* Returns a function that when executed will cancel the scheduled function.
|
||||
*/
|
||||
scheduleBeforeRender(taskFn: () => void): () => void{
|
||||
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 === 'undefined') {
|
||||
@ -76,7 +78,7 @@ export function createCustomEvent(doc: Document, name: string, detail: any): Cus
|
||||
/**
|
||||
* Check whether the input is an `Element`.
|
||||
*/
|
||||
export function isElement(node: Node | null): node is Element {
|
||||
export function isElement(node: Node|null): node is Element {
|
||||
return !!node && node.nodeType === Node.ELEMENT_NODE;
|
||||
}
|
||||
|
||||
|
@ -53,11 +53,13 @@ describe('ComponentFactoryNgElementStrategy', () => {
|
||||
strategy.connect(document.createElement('div'));
|
||||
});
|
||||
|
||||
it('should attach the component to the view',
|
||||
() => { expect(applicationRef.attachView).toHaveBeenCalledWith(componentRef.hostView); });
|
||||
it('should attach the component to the view', () => {
|
||||
expect(applicationRef.attachView).toHaveBeenCalledWith(componentRef.hostView);
|
||||
});
|
||||
|
||||
it('should detect changes',
|
||||
() => { expect(componentRef.changeDetectorRef.detectChanges).toHaveBeenCalled(); });
|
||||
it('should detect changes', () => {
|
||||
expect(componentRef.changeDetectorRef.detectChanges).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should listen to output events', () => {
|
||||
const events: NgElementStrategyEvent[] = [];
|
||||
@ -140,7 +142,9 @@ describe('ComponentFactoryNgElementStrategy', () => {
|
||||
});
|
||||
|
||||
describe('when inputs change and is connected', () => {
|
||||
beforeEach(() => { strategy.connect(document.createElement('div')); });
|
||||
beforeEach(() => {
|
||||
strategy.connect(document.createElement('div'));
|
||||
});
|
||||
|
||||
it('should be set on the component instance', () => {
|
||||
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
||||
@ -249,7 +253,9 @@ export class FakeComponent {
|
||||
// Keep track of the simple changes passed to ngOnChanges
|
||||
simpleChanges: SimpleChanges[] = [];
|
||||
|
||||
ngOnChanges(simpleChanges: SimpleChanges) { this.simpleChanges.push(simpleChanges); }
|
||||
ngOnChanges(simpleChanges: SimpleChanges) {
|
||||
this.simpleChanges.push(simpleChanges);
|
||||
}
|
||||
}
|
||||
|
||||
export class FakeComponentFactory extends ComponentFactory<any> {
|
||||
@ -263,9 +269,15 @@ export class FakeComponentFactory extends ComponentFactory<any> {
|
||||
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 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: 'fooFoo', templateName: 'fooFoo'},
|
||||
@ -293,8 +305,9 @@ export class FakeComponentFactory extends ComponentFactory<any> {
|
||||
}
|
||||
|
||||
function expectSimpleChanges(actual: SimpleChanges, expected: SimpleChanges) {
|
||||
Object.keys(actual).forEach(
|
||||
key => { expect(expected[key]).toBeTruthy(`Change included additional key ${key}`); });
|
||||
Object.keys(actual).forEach(key => {
|
||||
expect(expected[key]).toBeTruthy(`Change included additional key ${key}`);
|
||||
});
|
||||
|
||||
Object.keys(expected).forEach(key => {
|
||||
expect(actual[key]).toBeTruthy(`Change should have included key ${key}`);
|
||||
|
@ -6,13 +6,13 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component, DoBootstrap, EventEmitter, Injector, Input, NgModule, Output, destroyPlatform} from '@angular/core';
|
||||
import {Component, destroyPlatform, DoBootstrap, EventEmitter, Injector, Input, NgModule, Output} from '@angular/core';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
|
||||
import {Subject} from 'rxjs';
|
||||
|
||||
import {NgElementConstructor, createCustomElement} from '../src/create-custom-element';
|
||||
import {createCustomElement, NgElementConstructor} from '../src/create-custom-element';
|
||||
import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from '../src/element-strategy';
|
||||
|
||||
type WithFooBar = {
|
||||
@ -105,7 +105,7 @@ if (browserDetection.supportsCustomElements) {
|
||||
class TestComponent {
|
||||
@Input() fooFoo: string = 'foo';
|
||||
// TODO(issue/24571): remove '!'.
|
||||
@Input('barbar') barBar !: string;
|
||||
@Input('barbar') barBar!: string;
|
||||
|
||||
@Output() bazBaz = new EventEmitter<boolean>();
|
||||
@Output('quxqux') quxQux = new EventEmitter<Object>();
|
||||
@ -126,17 +126,27 @@ export class TestStrategy implements NgElementStrategy {
|
||||
|
||||
events = new Subject<NgElementStrategyEvent>();
|
||||
|
||||
connect(element: HTMLElement): void { this.connectedElement = element; }
|
||||
connect(element: HTMLElement): void {
|
||||
this.connectedElement = element;
|
||||
}
|
||||
|
||||
disconnect(): void { this.disconnectCalled = true; }
|
||||
disconnect(): void {
|
||||
this.disconnectCalled = true;
|
||||
}
|
||||
|
||||
getInputValue(propName: string): any { return this.inputs.get(propName); }
|
||||
getInputValue(propName: string): any {
|
||||
return this.inputs.get(propName);
|
||||
}
|
||||
|
||||
setInputValue(propName: string, value: string): void { this.inputs.set(propName, value); }
|
||||
setInputValue(propName: string, value: string): void {
|
||||
this.inputs.set(propName, value);
|
||||
}
|
||||
}
|
||||
|
||||
export class TestStrategyFactory implements NgElementStrategyFactory {
|
||||
testStrategy = new TestStrategy();
|
||||
|
||||
create(): NgElementStrategy { return this.testStrategy; }
|
||||
create(): NgElementStrategy {
|
||||
return this.testStrategy;
|
||||
}
|
||||
}
|
||||
|
@ -6,12 +6,12 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component, ComponentFactoryResolver, EventEmitter, Input, NgModule, Output, ViewEncapsulation, destroyPlatform} from '@angular/core';
|
||||
import {Component, ComponentFactoryResolver, destroyPlatform, EventEmitter, Input, NgModule, Output, ViewEncapsulation} from '@angular/core';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
|
||||
|
||||
import {NgElement, createCustomElement} from '../src/create-custom-element';
|
||||
import {createCustomElement, NgElement} from '../src/create-custom-element';
|
||||
|
||||
|
||||
// we only run these tests in browsers that support Shadom DOM slots natively
|
||||
@ -46,9 +46,9 @@ if (browserDetection.supportsCustomElements && browserDetection.supportsShadowDo
|
||||
it('should use slots to project content', () => {
|
||||
const tpl = `<default-slot-el><span class="projected"></span></default-slot-el>`;
|
||||
testContainer.innerHTML = tpl;
|
||||
const testEl = testContainer.querySelector('default-slot-el') !;
|
||||
const content = testContainer.querySelector('span.projected') !;
|
||||
const slot = testEl.shadowRoot !.querySelector('slot') !;
|
||||
const testEl = testContainer.querySelector('default-slot-el')!;
|
||||
const content = testContainer.querySelector('span.projected')!;
|
||||
const slot = testEl.shadowRoot!.querySelector('slot')!;
|
||||
const assignedNodes = slot.assignedNodes();
|
||||
expect(assignedNodes[0]).toBe(content);
|
||||
});
|
||||
@ -56,9 +56,9 @@ if (browserDetection.supportsCustomElements && browserDetection.supportsShadowDo
|
||||
it('should use a named slot to project content', () => {
|
||||
const tpl = `<named-slot-el><span class="projected" slot="header"></span></named-slot-el>`;
|
||||
testContainer.innerHTML = tpl;
|
||||
const testEl = testContainer.querySelector('named-slot-el') !;
|
||||
const content = testContainer.querySelector('span.projected') !;
|
||||
const slot = testEl.shadowRoot !.querySelector('slot[name=header]') as HTMLSlotElement;
|
||||
const testEl = testContainer.querySelector('named-slot-el')!;
|
||||
const content = testContainer.querySelector('span.projected')!;
|
||||
const slot = testEl.shadowRoot!.querySelector('slot[name=header]') as HTMLSlotElement;
|
||||
const assignedNodes = slot.assignedNodes();
|
||||
expect(assignedNodes[0]).toBe(content);
|
||||
});
|
||||
@ -70,11 +70,11 @@ if (browserDetection.supportsCustomElements && browserDetection.supportsShadowDo
|
||||
<span class="projected-body" slot="body"></span>
|
||||
</named-slots-el>`;
|
||||
testContainer.innerHTML = tpl;
|
||||
const testEl = testContainer.querySelector('named-slots-el') !;
|
||||
const headerContent = testContainer.querySelector('span.projected-header') !;
|
||||
const bodyContent = testContainer.querySelector('span.projected-body') !;
|
||||
const headerSlot = testEl.shadowRoot !.querySelector('slot[name=header]') as HTMLSlotElement;
|
||||
const bodySlot = testEl.shadowRoot !.querySelector('slot[name=body]') as HTMLSlotElement;
|
||||
const testEl = testContainer.querySelector('named-slots-el')!;
|
||||
const headerContent = testContainer.querySelector('span.projected-header')!;
|
||||
const bodyContent = testContainer.querySelector('span.projected-body')!;
|
||||
const headerSlot = testEl.shadowRoot!.querySelector('slot[name=header]') as HTMLSlotElement;
|
||||
const bodySlot = testEl.shadowRoot!.querySelector('slot[name=body]') as HTMLSlotElement;
|
||||
|
||||
expect(headerContent.assignedSlot).toBe(headerSlot);
|
||||
expect(bodyContent.assignedSlot).toBe(bodySlot);
|
||||
@ -88,7 +88,7 @@ if (browserDetection.supportsCustomElements && browserDetection.supportsShadowDo
|
||||
</slot-events-el>`;
|
||||
templateEl.innerHTML = tpl;
|
||||
const template = templateEl.content.cloneNode(true) as DocumentFragment;
|
||||
const testEl = template.querySelector('slot-events-el') !as NgElement & SlotEventsComponent;
|
||||
const testEl = template.querySelector('slot-events-el')! as NgElement & SlotEventsComponent;
|
||||
testEl.addEventListener('slotEventsChange', e => {
|
||||
expect(testEl.slotEvents.length).toEqual(1);
|
||||
done();
|
||||
|
Reference in New Issue
Block a user