parent
46efd4b938
commit
87f60bccfd
|
@ -4,7 +4,7 @@
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"inline": 2062,
|
"inline": 2062,
|
||||||
"main": 467103,
|
"main": 467103,
|
||||||
"polyfills": 55349,
|
"polyfills": 54292,
|
||||||
"embedded": 71711,
|
"embedded": 71711,
|
||||||
"prettify": 14888
|
"prettify": 14888
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ describe('CodeExampleComponent', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to capture the code snippet provided in content', () => {
|
it('should be able to capture the code snippet provided in content', () => {
|
||||||
expect(codeExampleComponent.code.trim()).toBe(`const foo = "bar";`);
|
expect(codeExampleComponent.aioCode.code.trim()).toBe(`const foo = "bar";`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change aio-code classes based on title presence', () => {
|
it('should change aio-code classes based on title presence', () => {
|
||||||
|
|
|
@ -34,8 +34,6 @@ import { CodeComponent } from './code.component';
|
||||||
export class CodeExampleComponent implements AfterViewInit {
|
export class CodeExampleComponent implements AfterViewInit {
|
||||||
classes: {};
|
classes: {};
|
||||||
|
|
||||||
code: string;
|
|
||||||
|
|
||||||
@Input() language: string;
|
@Input() language: string;
|
||||||
|
|
||||||
@Input() linenums: string;
|
@Input() linenums: string;
|
||||||
|
|
|
@ -9,8 +9,6 @@ import {TestBed, fakeAsync, tick} from '@angular/core/testing';
|
||||||
import { ElementsLoader } from './elements-loader';
|
import { ElementsLoader } from './elements-loader';
|
||||||
import { ELEMENT_MODULE_PATHS_TOKEN, WithCustomElementComponent } from './element-registry';
|
import { ELEMENT_MODULE_PATHS_TOKEN, WithCustomElementComponent } from './element-registry';
|
||||||
|
|
||||||
const actualCustomElements = window.customElements;
|
|
||||||
|
|
||||||
class FakeComponentFactory extends ComponentFactory<any> {
|
class FakeComponentFactory extends ComponentFactory<any> {
|
||||||
selector: string;
|
selector: string;
|
||||||
componentType: Type<any>;
|
componentType: Type<any>;
|
||||||
|
@ -29,21 +27,26 @@ class FakeComponentFactory extends ComponentFactory<any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const FAKE_COMPONENT_FACTORIES = new Map([
|
const FAKE_COMPONENT_FACTORIES = new Map([
|
||||||
['element-a-module-path', new FakeComponentFactory('element-a-input')]
|
['element-a-module-path', new FakeComponentFactory('element-a-input')],
|
||||||
|
['element-b-module-path', new FakeComponentFactory('element-b-input')],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
fdescribe('ElementsLoader', () => {
|
describe('ElementsLoader', () => {
|
||||||
let elementsLoader: ElementsLoader;
|
let elementsLoader: ElementsLoader;
|
||||||
let injectedModuleRef: NgModuleRef<any>;
|
let injectedModuleRef: NgModuleRef<any>;
|
||||||
let fakeCustomElements;
|
let actualCustomElementsDefine;
|
||||||
|
let fakeCustomElementsDefine;
|
||||||
|
|
||||||
// ElementsLoader uses the window's customElements API. Provide a fake for this test.
|
// ElementsLoader uses the window's customElements API. Provide a fake for this test.
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fakeCustomElements = jasmine.createSpyObj('customElements', ['define', 'whenDefined']);
|
actualCustomElementsDefine = window.customElements.define;
|
||||||
window.customElements = fakeCustomElements;
|
|
||||||
|
fakeCustomElementsDefine = jasmine.createSpy('define');
|
||||||
|
|
||||||
|
window.customElements.define = fakeCustomElementsDefine;
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
window.customElements = actualCustomElements;
|
window.customElements.define = actualCustomElementsDefine;
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -52,7 +55,8 @@ fdescribe('ElementsLoader', () => {
|
||||||
ElementsLoader,
|
ElementsLoader,
|
||||||
{ provide: NgModuleFactoryLoader, useClass: FakeModuleFactoryLoader },
|
{ provide: NgModuleFactoryLoader, useClass: FakeModuleFactoryLoader },
|
||||||
{ provide: ELEMENT_MODULE_PATHS_TOKEN, useValue: new Map([
|
{ provide: ELEMENT_MODULE_PATHS_TOKEN, useValue: new Map([
|
||||||
['element-a-selector', 'element-a-module-path']
|
['element-a-selector', 'element-a-module-path'],
|
||||||
|
['element-b-selector', 'element-b-module-path']
|
||||||
])},
|
])},
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -71,7 +75,7 @@ fdescribe('ElementsLoader', () => {
|
||||||
elementsLoader.loadContainingCustomElements(hostEl);
|
elementsLoader.loadContainingCustomElements(hostEl);
|
||||||
tick();
|
tick();
|
||||||
|
|
||||||
const defineArgs = fakeCustomElements.define.calls.argsFor(0);
|
const defineArgs = fakeCustomElementsDefine.calls.argsFor(0);
|
||||||
expect(defineArgs[0]).toBe('element-a-selector');
|
expect(defineArgs[0]).toBe('element-a-selector');
|
||||||
|
|
||||||
// Verify the right component was loaded/created
|
// Verify the right component was loaded/created
|
||||||
|
@ -80,6 +84,30 @@ fdescribe('ElementsLoader', () => {
|
||||||
expect(elementsLoader.elementsToLoad.has('element-a-selector')).toBeFalsy();
|
expect(elementsLoader.elementsToLoad.has('element-a-selector')).toBeFalsy();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should be able to register multiple elements', fakeAsync(() => {
|
||||||
|
// Verify that the elements loader considered `element-a-selector` to be unregistered.
|
||||||
|
expect(elementsLoader.elementsToLoad.has('element-a-selector')).toBeTruthy();
|
||||||
|
|
||||||
|
const hostEl = document.createElement('div');
|
||||||
|
hostEl.innerHTML = `
|
||||||
|
<element-a-selector></element-a-selector>
|
||||||
|
<element-b-selector></element-b-selector>
|
||||||
|
`;
|
||||||
|
|
||||||
|
elementsLoader.loadContainingCustomElements(hostEl);
|
||||||
|
tick();
|
||||||
|
|
||||||
|
const defineElementA = fakeCustomElementsDefine.calls.argsFor(0);
|
||||||
|
expect(defineElementA[0]).toBe('element-a-selector');
|
||||||
|
expect(defineElementA[1].observedAttributes[0]).toBe('element-a-input');
|
||||||
|
expect(elementsLoader.elementsToLoad.has('element-a-selector')).toBeFalsy();
|
||||||
|
|
||||||
|
const defineElementB = fakeCustomElementsDefine.calls.argsFor(1);
|
||||||
|
expect(defineElementB[0]).toBe('element-b-selector');
|
||||||
|
expect(defineElementB[1].observedAttributes[0]).toBe('element-b-input');
|
||||||
|
expect(elementsLoader.elementsToLoad.has('element-b-selector')).toBeFalsy();
|
||||||
|
}));
|
||||||
|
|
||||||
it('should only register an element one time', fakeAsync(() => {
|
it('should only register an element one time', fakeAsync(() => {
|
||||||
const hostEl = document.createElement('div');
|
const hostEl = document.createElement('div');
|
||||||
hostEl.innerHTML = `<element-a-selector></element-a-selector>`;
|
hostEl.innerHTML = `<element-a-selector></element-a-selector>`;
|
||||||
|
|
|
@ -8,14 +8,14 @@ import { FILE_NOT_FOUND_ID, FETCHING_ERROR_ID } from 'app/documents/document.ser
|
||||||
import { Logger } from 'app/shared/logger.service';
|
import { Logger } from 'app/shared/logger.service';
|
||||||
import { CustomElementsModule } from 'app/custom-elements/custom-elements.module';
|
import { CustomElementsModule } from 'app/custom-elements/custom-elements.module';
|
||||||
import { TocService } from 'app/shared/toc.service';
|
import { TocService } from 'app/shared/toc.service';
|
||||||
|
import { ElementsLoader } from 'app/custom-elements/elements-loader';
|
||||||
import {
|
import {
|
||||||
MockTitle, MockTocService, ObservableWithSubscriptionSpies,
|
MockTitle, MockTocService, ObservableWithSubscriptionSpies,
|
||||||
TestDocViewerComponent, TestModule, TestParentComponent
|
TestDocViewerComponent, TestModule, TestParentComponent, MockElementsLoader
|
||||||
} from 'testing/doc-viewer-utils';
|
} from 'testing/doc-viewer-utils';
|
||||||
import { MockLogger } from 'testing/logger.service';
|
import { MockLogger } from 'testing/logger.service';
|
||||||
import { DocViewerComponent, NO_ANIMATIONS } from './doc-viewer.component';
|
import { DocViewerComponent, NO_ANIMATIONS } from './doc-viewer.component';
|
||||||
|
|
||||||
|
|
||||||
describe('DocViewerComponent', () => {
|
describe('DocViewerComponent', () => {
|
||||||
let parentFixture: ComponentFixture<TestParentComponent>;
|
let parentFixture: ComponentFixture<TestParentComponent>;
|
||||||
let parentComponent: TestParentComponent;
|
let parentComponent: TestParentComponent;
|
||||||
|
@ -24,7 +24,7 @@ describe('DocViewerComponent', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TestModule, CustomElementsModule],
|
imports: [CustomElementsModule, TestModule],
|
||||||
});
|
});
|
||||||
|
|
||||||
parentFixture = TestBed.createComponent(TestParentComponent);
|
parentFixture = TestBed.createComponent(TestParentComponent);
|
||||||
|
@ -294,12 +294,16 @@ describe('DocViewerComponent', () => {
|
||||||
describe('#render()', () => {
|
describe('#render()', () => {
|
||||||
let prepareTitleAndTocSpy: jasmine.Spy;
|
let prepareTitleAndTocSpy: jasmine.Spy;
|
||||||
let swapViewsSpy: jasmine.Spy;
|
let swapViewsSpy: jasmine.Spy;
|
||||||
|
let loadElementsSpy: jasmine.Spy;
|
||||||
|
|
||||||
const doRender = (contents: string | null, id = 'foo') =>
|
const doRender = (contents: string | null, id = 'foo') =>
|
||||||
new Promise<void>((resolve, reject) =>
|
new Promise<void>((resolve, reject) =>
|
||||||
docViewer.render({contents, id}).subscribe(resolve, reject));
|
docViewer.render({contents, id}).subscribe(resolve, reject));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
const elementsLoader = TestBed.get(ElementsLoader) as MockElementsLoader;
|
||||||
|
loadElementsSpy =
|
||||||
|
elementsLoader.loadContainingCustomElements.and.returnValue(of([]));
|
||||||
prepareTitleAndTocSpy = spyOn(docViewer, 'prepareTitleAndToc');
|
prepareTitleAndTocSpy = spyOn(docViewer, 'prepareTitleAndToc');
|
||||||
swapViewsSpy = spyOn(docViewer, 'swapViews').and.returnValue(of(undefined));
|
swapViewsSpy = spyOn(docViewer, 'swapViews').and.returnValue(of(undefined));
|
||||||
});
|
});
|
||||||
|
@ -333,7 +337,7 @@ describe('DocViewerComponent', () => {
|
||||||
expect(docViewerEl.textContent).toBe('');
|
expect(docViewerEl.textContent).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should prepare the title and ToC', async () => {
|
it('should prepare the title and ToC (before embedding components)', async () => {
|
||||||
prepareTitleAndTocSpy.and.callFake((targetEl: HTMLElement, docId: string) => {
|
prepareTitleAndTocSpy.and.callFake((targetEl: HTMLElement, docId: string) => {
|
||||||
expect(targetEl.innerHTML).toBe('Some content');
|
expect(targetEl.innerHTML).toBe('Some content');
|
||||||
expect(docId).toBe('foo');
|
expect(docId).toBe('foo');
|
||||||
|
@ -342,6 +346,7 @@ describe('DocViewerComponent', () => {
|
||||||
await doRender('Some content', 'foo');
|
await doRender('Some content', 'foo');
|
||||||
|
|
||||||
expect(prepareTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
expect(prepareTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(prepareTitleAndTocSpy).toHaveBeenCalledBefore(loadElementsSpy);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the title and ToC (after the content has been set)', async () => {
|
it('should set the title and ToC (after the content has been set)', async () => {
|
||||||
|
@ -384,6 +389,39 @@ describe('DocViewerComponent', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('(embedding components)', () => {
|
||||||
|
it('should embed components', async () => {
|
||||||
|
await doRender('Some content');
|
||||||
|
expect(loadElementsSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(loadElementsSpy).toHaveBeenCalledWith(docViewer.nextViewContainer);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should attempt to embed components even if the document is empty', async () => {
|
||||||
|
await doRender('');
|
||||||
|
await doRender(null);
|
||||||
|
|
||||||
|
expect(loadElementsSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(loadElementsSpy.calls.argsFor(0)).toEqual([docViewer.nextViewContainer]);
|
||||||
|
expect(loadElementsSpy.calls.argsFor(1)).toEqual([docViewer.nextViewContainer]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should unsubscribe from the previous "embed" observable when unsubscribed from', () => {
|
||||||
|
const obs = new ObservableWithSubscriptionSpies();
|
||||||
|
loadElementsSpy.and.returnValue(obs);
|
||||||
|
|
||||||
|
const renderObservable = docViewer.render({contents: 'Some content', id: 'foo'});
|
||||||
|
const subscription = renderObservable.subscribe();
|
||||||
|
|
||||||
|
expect(obs.subscribeSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(obs.unsubscribeSpies[0]).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
subscription.unsubscribe();
|
||||||
|
|
||||||
|
expect(obs.subscribeSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(obs.unsubscribeSpies[0]).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('(swapping views)', () => {
|
describe('(swapping views)', () => {
|
||||||
it('should still swap the views if the document is empty', async () => {
|
it('should still swap the views if the document is empty', async () => {
|
||||||
await doRender('');
|
await doRender('');
|
||||||
|
@ -444,6 +482,25 @@ describe('DocViewerComponent', () => {
|
||||||
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
|
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('when `EmbedComponentsService.embedInto()` fails', async () => {
|
||||||
|
const error = Error('Typical `embedInto()` error');
|
||||||
|
loadElementsSpy.and.callFake(() => {
|
||||||
|
expect(docViewer.nextViewContainer.innerHTML).not.toBe('');
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
await doRender('Some content', 'bar');
|
||||||
|
|
||||||
|
expect(prepareTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(loadElementsSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(swapViewsSpy).not.toHaveBeenCalled();
|
||||||
|
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
||||||
|
expect(logger.output.error).toEqual([
|
||||||
|
[`[DocViewer] Error preparing document 'bar': ${error.stack}`],
|
||||||
|
]);
|
||||||
|
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'googlebot', content: 'noindex' });
|
||||||
|
expect(TestBed.get(Meta).addTag).toHaveBeenCalledWith({ name: 'robots', content: 'noindex' });
|
||||||
|
});
|
||||||
|
|
||||||
it('when `swapViews()` fails', async () => {
|
it('when `swapViews()` fails', async () => {
|
||||||
const error = Error('Typical `swapViews()` error');
|
const error = Error('Typical `swapViews()` error');
|
||||||
|
@ -486,13 +543,24 @@ describe('DocViewerComponent', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('(events)', () => {
|
describe('(events)', () => {
|
||||||
it('should emit `docReady`', async () => {
|
it('should emit `docReady` after loading elements', async () => {
|
||||||
const onDocReadySpy = jasmine.createSpy('onDocReady');
|
const onDocReadySpy = jasmine.createSpy('onDocReady');
|
||||||
docViewer.docReady.subscribe(onDocReadySpy);
|
docViewer.docReady.subscribe(onDocReadySpy);
|
||||||
|
|
||||||
await doRender('Some content');
|
await doRender('Some content');
|
||||||
|
|
||||||
expect(onDocReadySpy).toHaveBeenCalledTimes(1);
|
expect(onDocReadySpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(loadElementsSpy).toHaveBeenCalledBefore(onDocReadySpy);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit `docReady` before swapping views', async () => {
|
||||||
|
const onDocReadySpy = jasmine.createSpy('onDocReady');
|
||||||
|
docViewer.docReady.subscribe(onDocReadySpy);
|
||||||
|
|
||||||
|
await doRender('Some content');
|
||||||
|
|
||||||
|
expect(onDocReadySpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onDocReadySpy).toHaveBeenCalledBefore(swapViewsSpy);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit `docRendered` after swapping views', async () => {
|
it('should emit `docRendered` after swapping views', async () => {
|
||||||
|
|
|
@ -67,12 +67,12 @@ export class DocViewerComponent implements OnDestroy {
|
||||||
@Output() docRendered = new EventEmitter<void>();
|
@Output() docRendered = new EventEmitter<void>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
elementRef: ElementRef,
|
elementRef: ElementRef,
|
||||||
private logger: Logger,
|
private logger: Logger,
|
||||||
private titleService: Title,
|
private titleService: Title,
|
||||||
private metaService: Meta,
|
private metaService: Meta,
|
||||||
private tocService: TocService,
|
private tocService: TocService,
|
||||||
private elementsLoader: ElementsLoader) {
|
private elementsLoader: ElementsLoader) {
|
||||||
this.hostElement = elementRef.nativeElement;
|
this.hostElement = elementRef.nativeElement;
|
||||||
// Security: the initialDocViewerContent comes from the prerendered DOM and is considered to be secure
|
// Security: the initialDocViewerContent comes from the prerendered DOM and is considered to be secure
|
||||||
this.hostElement.innerHTML = initialDocViewerContent;
|
this.hostElement.innerHTML = initialDocViewerContent;
|
||||||
|
|
|
@ -110,6 +110,15 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Custom elements should always rely on the polyfill to avoid having to include a shim that
|
||||||
|
// handles downleveled ES2015 classes. Especially since that shim would break on IE11 which
|
||||||
|
// can't even parse such code.
|
||||||
|
if (window.customElements) {
|
||||||
|
window.customElements['forcePolyfill'] = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
|
@ -33,9 +33,6 @@ import './environments/environment';
|
||||||
/** Add support for window.customElements */
|
/** Add support for window.customElements */
|
||||||
import '@webcomponents/custom-elements/custom-elements.min';
|
import '@webcomponents/custom-elements/custom-elements.min';
|
||||||
|
|
||||||
/** Required for custom elements for apps building to es5. */
|
|
||||||
import '@webcomponents/custom-elements/src/native-shim';
|
|
||||||
|
|
||||||
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
|
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
|
||||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||||
import { Logger } from 'app/shared/logger.service';
|
import { Logger } from 'app/shared/logger.service';
|
||||||
import { TocService } from 'app/shared/toc.service';
|
import { TocService } from 'app/shared/toc.service';
|
||||||
import { MockLogger } from 'testing/logger.service';
|
import { MockLogger } from 'testing/logger.service';
|
||||||
|
import { ElementsLoader } from 'app/custom-elements/elements-loader';
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -54,6 +55,11 @@ export class MockTocService {
|
||||||
reset = jasmine.createSpy('TocService#reset');
|
reset = jasmine.createSpy('TocService#reset');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MockElementsLoader {
|
||||||
|
loadContainingCustomElements =
|
||||||
|
jasmine.createSpy('MockElementsLoader#loadContainingCustomElements');
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
DocViewerComponent,
|
DocViewerComponent,
|
||||||
|
@ -64,6 +70,7 @@ export class MockTocService {
|
||||||
{ provide: Title, useClass: MockTitle },
|
{ provide: Title, useClass: MockTitle },
|
||||||
{ provide: Meta, useClass: MockMeta },
|
{ provide: Meta, useClass: MockMeta },
|
||||||
{ provide: TocService, useClass: MockTocService },
|
{ provide: TocService, useClass: MockTocService },
|
||||||
|
{ provide: ElementsLoader, useClass: MockElementsLoader },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TestModule { }
|
export class TestModule { }
|
||||||
|
|
|
@ -205,6 +205,13 @@ function extractProperties(
|
||||||
function extractName(type: Function): string|null {
|
function extractName(type: Function): string|null {
|
||||||
let name = type['name'];
|
let name = type['name'];
|
||||||
|
|
||||||
|
// The polyfill @webcomponents/custom-element/src/native-shim.js overrides the
|
||||||
|
// window.HTMLElement and does not have the name property. Check if this is the
|
||||||
|
// case and if so, set the name manually.
|
||||||
|
if (name === '' && type === HTMLElement) {
|
||||||
|
name = 'HTMLElement';
|
||||||
|
}
|
||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
// see https://www.w3.org/TR/html5/index.html
|
// see https://www.w3.org/TR/html5/index.html
|
||||||
// TODO(vicb): generate this map from all the element types
|
// TODO(vicb): generate this map from all the element types
|
||||||
|
|
|
@ -6,29 +6,35 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ApplicationRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges} from '@angular/core';
|
import {ApplicationRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, Type} from '@angular/core';
|
||||||
import {Observable} from 'rxjs/Observable';
|
import {Observable} from 'rxjs/Observable';
|
||||||
import {merge} from 'rxjs/observable/merge';
|
import {merge} from 'rxjs/observable/merge';
|
||||||
import {map} from 'rxjs/operator/map';
|
import {map} from 'rxjs/operator/map';
|
||||||
|
|
||||||
import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from './element-strategy';
|
import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from './element-strategy';
|
||||||
import {extractProjectableNodes} from './extract-projectable-nodes';
|
import {extractProjectableNodes} from './extract-projectable-nodes';
|
||||||
import {camelToDashCase, isFunction, scheduler, strictEquals} from './utils';
|
import {isFunction, scheduler, strictEquals} from './utils';
|
||||||
|
|
||||||
/** Time in milliseconds to wait before destroying the component ref when disconnected. */
|
/** Time in milliseconds to wait before destroying the component ref when disconnected. */
|
||||||
const DESTROY_DELAY = 10;
|
const DESTROY_DELAY = 10;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory that creates new ComponentFactoryNgElementStrategy instances with the strategy factory's
|
* Factory that creates new ComponentNgElementStrategy instance. Gets the component factory with the
|
||||||
* injector. A new strategy instance is created with the provided component factory which will
|
* constructor's injector's factory resolver and passes that factory to each strategy.
|
||||||
* create its components on connect.
|
|
||||||
*
|
*
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
export class ComponentFactoryNgElementStrategyFactory implements NgElementStrategyFactory {
|
export class ComponentNgElementStrategyFactory implements NgElementStrategyFactory {
|
||||||
constructor(private componentFactory: ComponentFactory<any>, private injector: Injector) {}
|
componentFactory: ComponentFactory<any>;
|
||||||
|
|
||||||
create() { return new ComponentFactoryNgElementStrategy(this.componentFactory, this.injector); }
|
constructor(private component: Type<any>, private injector: Injector) {
|
||||||
|
this.componentFactory =
|
||||||
|
injector.get(ComponentFactoryResolver).resolveComponentFactory(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(injector: Injector) {
|
||||||
|
return new ComponentNgElementStrategy(this.componentFactory, injector);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -37,12 +43,12 @@ export class ComponentFactoryNgElementStrategyFactory implements NgElementStrate
|
||||||
*
|
*
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
export class ComponentFactoryNgElementStrategy implements NgElementStrategy {
|
export class ComponentNgElementStrategy implements NgElementStrategy {
|
||||||
/** Merged stream of the component's output events. */
|
/** Merged stream of the component's output events. */
|
||||||
events: Observable<NgElementStrategyEvent>;
|
events: Observable<NgElementStrategyEvent>;
|
||||||
|
|
||||||
/** Reference to the component that was created on connect. */
|
/** Reference to the component that was created on connect. */
|
||||||
private componentRef: ComponentRef<any>;
|
private componentRef: ComponentRef<any>|null;
|
||||||
|
|
||||||
/** Changes that have been made to the component ref since the last time onChanges was called. */
|
/** Changes that have been made to the component ref since the last time onChanges was called. */
|
||||||
private inputChanges: SimpleChanges|null = null;
|
private inputChanges: SimpleChanges|null = null;
|
||||||
|
@ -96,6 +102,7 @@ export class ComponentFactoryNgElementStrategy implements NgElementStrategy {
|
||||||
this.scheduledDestroyFn = scheduler.schedule(() => {
|
this.scheduledDestroyFn = scheduler.schedule(() => {
|
||||||
if (this.componentRef) {
|
if (this.componentRef) {
|
||||||
this.componentRef !.destroy();
|
this.componentRef !.destroy();
|
||||||
|
this.componentRef = null;
|
||||||
}
|
}
|
||||||
}, DESTROY_DELAY);
|
}, DESTROY_DELAY);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {ComponentFactory} from '@angular/core';
|
import {ComponentFactory, Injector} from '@angular/core';
|
||||||
import {Observable} from 'rxjs/Observable';
|
import {Observable} from 'rxjs/Observable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,4 +38,7 @@ export interface NgElementStrategy {
|
||||||
*
|
*
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
export interface NgElementStrategyFactory { create(): NgElementStrategy; }
|
export interface NgElementStrategyFactory {
|
||||||
|
/** Creates a new instance to be used for an NgElement. */
|
||||||
|
create(injector: Injector): NgElementStrategy;
|
||||||
|
}
|
||||||
|
|
|
@ -6,12 +6,12 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ComponentFactoryResolver, Injector, Type} from '@angular/core';
|
import {Injector, Type} from '@angular/core';
|
||||||
import {Subscription} from 'rxjs/Subscription';
|
import {Subscription} from 'rxjs/Subscription';
|
||||||
|
|
||||||
import {ComponentFactoryNgElementStrategyFactory} from './component-factory-strategy';
|
import {ComponentNgElementStrategyFactory} from './component-factory-strategy';
|
||||||
import {NgElementStrategy, NgElementStrategyFactory} from './element-strategy';
|
import {NgElementStrategy, NgElementStrategyFactory} from './element-strategy';
|
||||||
import {camelToDashCase, createCustomEvent} from './utils';
|
import {createCustomEvent, getComponentInputs, getDefaultAttributeToPropertyInputs} from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class constructor based on an Angular Component to be used for custom element registration.
|
* Class constructor based on an Angular Component to be used for custom element registration.
|
||||||
|
@ -21,7 +21,7 @@ import {camelToDashCase, createCustomEvent} from './utils';
|
||||||
export interface NgElementConstructor<P> {
|
export interface NgElementConstructor<P> {
|
||||||
readonly observedAttributes: string[];
|
readonly observedAttributes: string[];
|
||||||
|
|
||||||
new (): NgElement&WithProperties<P>;
|
new (injector: Injector): NgElement&WithProperties<P>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,30 +50,19 @@ export type WithProperties<P> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialization configuration for the NgElementConstructor. Provides the strategy factory
|
* Initialization configuration for the NgElementConstructor which contains the injector to be used
|
||||||
* that produces a strategy for each instantiated element. Additionally, provides a function
|
* for retrieving the component's factory as well as the default context for the component. May
|
||||||
* that takes the component factory and provides a map of which attributes should be observed on
|
* provide a custom strategy factory to be used instead of the default. May provide a custom mapping
|
||||||
* the element and which property they are associated with.
|
* of attribute names to component inputs.
|
||||||
*
|
*
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
export interface NgElementConfig {
|
export interface NgElementConfig {
|
||||||
injector: Injector;
|
injector: Injector;
|
||||||
strategyFactory?: NgElementStrategyFactory;
|
strategyFactory?: NgElementStrategyFactory;
|
||||||
propertyInputs?: string[];
|
|
||||||
attributeToPropertyInputs?: {[key: string]: string};
|
attributeToPropertyInputs?: {[key: string]: string};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets a map of default set of attributes to observe and the properties they affect. */
|
|
||||||
function getDefaultAttributeToPropertyInputs(inputs: {propName: string, templateName: string}[]) {
|
|
||||||
const attributeToPropertyInputs: {[key: string]: string} = {};
|
|
||||||
inputs.forEach(({propName, templateName}) => {
|
|
||||||
attributeToPropertyInputs[camelToDashCase(templateName)] = propName;
|
|
||||||
});
|
|
||||||
|
|
||||||
return attributeToPropertyInputs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @whatItDoes Creates a custom element class based on an Angular Component. Takes a configuration
|
* @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
|
* that provides initialization information to the created class. E.g. the configuration's injector
|
||||||
|
@ -90,13 +79,10 @@ function getDefaultAttributeToPropertyInputs(inputs: {propName: string, template
|
||||||
*/
|
*/
|
||||||
export function createNgElementConstructor<P>(
|
export function createNgElementConstructor<P>(
|
||||||
component: Type<any>, config: NgElementConfig): NgElementConstructor<P> {
|
component: Type<any>, config: NgElementConfig): NgElementConstructor<P> {
|
||||||
const componentFactoryResolver =
|
const inputs = getComponentInputs(component, config.injector);
|
||||||
config.injector.get(ComponentFactoryResolver) as ComponentFactoryResolver;
|
|
||||||
const componentFactory = componentFactoryResolver.resolveComponentFactory(component);
|
|
||||||
const inputs = componentFactory.inputs;
|
|
||||||
|
|
||||||
const defaultStrategyFactory = config.strategyFactory ||
|
const strategyFactory =
|
||||||
new ComponentFactoryNgElementStrategyFactory(componentFactory, config.injector);
|
config.strategyFactory || new ComponentNgElementStrategyFactory(component, config.injector);
|
||||||
|
|
||||||
const attributeToPropertyInputs =
|
const attributeToPropertyInputs =
|
||||||
config.attributeToPropertyInputs || getDefaultAttributeToPropertyInputs(inputs);
|
config.attributeToPropertyInputs || getDefaultAttributeToPropertyInputs(inputs);
|
||||||
|
@ -104,13 +90,9 @@ export function createNgElementConstructor<P>(
|
||||||
class NgElementImpl extends NgElement {
|
class NgElementImpl extends NgElement {
|
||||||
static readonly observedAttributes = Object.keys(attributeToPropertyInputs);
|
static readonly observedAttributes = Object.keys(attributeToPropertyInputs);
|
||||||
|
|
||||||
constructor(strategyFactoryOverride?: NgElementStrategyFactory) {
|
constructor(injector?: Injector) {
|
||||||
super();
|
super();
|
||||||
|
this.ngElementStrategy = strategyFactory.create(injector || config.injector);
|
||||||
// Use the constructor's strategy factory override if it is present, otherwise default to
|
|
||||||
// the config's factory.
|
|
||||||
const strategyFactory = strategyFactoryOverride || defaultStrategyFactory;
|
|
||||||
this.ngElementStrategy = strategyFactory.create();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeChangedCallback(
|
attributeChangedCallback(
|
||||||
|
@ -120,14 +102,6 @@ export function createNgElementConstructor<P>(
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
// Take element attribute inputs and set them as inputs on the strategy
|
|
||||||
NgElementImpl.observedAttributes.forEach(attrName => {
|
|
||||||
const propName = attributeToPropertyInputs[attrName] !;
|
|
||||||
if (this.hasAttribute(attrName)) {
|
|
||||||
this.ngElementStrategy.setInputValue(propName, this.getAttribute(attrName) !);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ngElementStrategy.connect(this);
|
this.ngElementStrategy.connect(this);
|
||||||
|
|
||||||
// Listen for events from the strategy and dispatch them as custom events
|
// Listen for events from the strategy and dispatch them as custom events
|
||||||
|
@ -149,8 +123,7 @@ export function createNgElementConstructor<P>(
|
||||||
|
|
||||||
// Add getters and setters to the prototype for each property input. If the config does not
|
// Add getters and setters to the prototype for each property input. If the config does not
|
||||||
// contain property inputs, use all inputs by default.
|
// contain property inputs, use all inputs by default.
|
||||||
const propertyInputs = config.propertyInputs || inputs.map(({propName}) => propName);
|
inputs.map(({propName}) => propName).forEach(property => {
|
||||||
propertyInputs.forEach(property => {
|
|
||||||
Object.defineProperty(NgElementImpl.prototype, property, {
|
Object.defineProperty(NgElementImpl.prototype, property, {
|
||||||
get: function() { return this.ngElementStrategy.getInputValue(property); },
|
get: function() { return this.ngElementStrategy.getInputValue(property); },
|
||||||
set: function(newValue: any) { this.ngElementStrategy.setInputValue(property, newValue); },
|
set: function(newValue: any) { this.ngElementStrategy.setInputValue(property, newValue); },
|
||||||
|
|
|
@ -5,8 +5,7 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
import {ComponentFactoryResolver, Injector, Type} from '@angular/core';
|
||||||
import {Type} from '@angular/core';
|
|
||||||
|
|
||||||
const elProto = Element.prototype as any;
|
const elProto = Element.prototype as any;
|
||||||
const matches = elProto.matches || elProto.matchesSelector || elProto.mozMatchesSelector ||
|
const matches = elProto.matches || elProto.matchesSelector || elProto.mozMatchesSelector ||
|
||||||
|
@ -101,3 +100,25 @@ export function matchesSelector(element: Element, selector: string): boolean {
|
||||||
export function strictEquals(value1: any, value2: any): boolean {
|
export function strictEquals(value1: any, value2: any): boolean {
|
||||||
return value1 === value2 || (value1 !== value1 && value2 !== value2);
|
return value1 === value2 || (value1 !== value1 && value2 !== value2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets a map of default set of attributes to observe and the properties they affect. */
|
||||||
|
export function getDefaultAttributeToPropertyInputs(
|
||||||
|
inputs: {propName: string, templateName: string}[]) {
|
||||||
|
const attributeToPropertyInputs: {[key: string]: string} = {};
|
||||||
|
inputs.forEach(({propName, templateName}) => {
|
||||||
|
attributeToPropertyInputs[camelToDashCase(templateName)] = propName;
|
||||||
|
});
|
||||||
|
|
||||||
|
return attributeToPropertyInputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a component's set of inputs. Uses the injector to get the component factory where the inputs
|
||||||
|
* are defined.
|
||||||
|
*/
|
||||||
|
export function getComponentInputs(
|
||||||
|
component: Type<any>, injector: Injector): {propName: string, templateName: string}[] {
|
||||||
|
const componentFactoryResolver: ComponentFactoryResolver = injector.get(ComponentFactoryResolver);
|
||||||
|
const componentFactory = componentFactoryResolver.resolveComponentFactory(component);
|
||||||
|
return componentFactory.inputs;
|
||||||
|
}
|
||||||
|
|
|
@ -10,12 +10,12 @@ import {ComponentFactory, ComponentRef, Injector, NgModuleRef, SimpleChange, Sim
|
||||||
import {fakeAsync, tick} from '@angular/core/testing';
|
import {fakeAsync, tick} from '@angular/core/testing';
|
||||||
import {Subject} from 'rxjs/Subject';
|
import {Subject} from 'rxjs/Subject';
|
||||||
|
|
||||||
import {ComponentFactoryNgElementStrategy, ComponentFactoryNgElementStrategyFactory} from '../src/component-factory-strategy';
|
import {ComponentNgElementStrategy, ComponentNgElementStrategyFactory} from '../src/component-factory-strategy';
|
||||||
import {NgElementStrategyEvent} from '../src/element-strategy';
|
import {NgElementStrategyEvent} from '../src/element-strategy';
|
||||||
|
|
||||||
describe('ComponentFactoryNgElementStrategy', () => {
|
describe('ComponentFactoryNgElementStrategy', () => {
|
||||||
let factory: FakeComponentFactory;
|
let factory: FakeComponentFactory;
|
||||||
let strategy: ComponentFactoryNgElementStrategy;
|
let strategy: ComponentNgElementStrategy;
|
||||||
|
|
||||||
let injector: any;
|
let injector: any;
|
||||||
let componentRef: any;
|
let componentRef: any;
|
||||||
|
@ -25,22 +25,26 @@ describe('ComponentFactoryNgElementStrategy', () => {
|
||||||
factory = new FakeComponentFactory();
|
factory = new FakeComponentFactory();
|
||||||
componentRef = factory.componentRef;
|
componentRef = factory.componentRef;
|
||||||
|
|
||||||
injector = jasmine.createSpyObj('injector', ['get']);
|
|
||||||
applicationRef = jasmine.createSpyObj('applicationRef', ['attachView']);
|
applicationRef = jasmine.createSpyObj('applicationRef', ['attachView']);
|
||||||
injector.get.and.returnValue(applicationRef);
|
|
||||||
|
|
||||||
strategy = new ComponentFactoryNgElementStrategy(factory, injector);
|
strategy = new ComponentNgElementStrategy(factory, injector);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a new strategy from the factory', () => {
|
it('should create a new strategy from the factory', () => {
|
||||||
const strategyFactory = new ComponentFactoryNgElementStrategyFactory(factory, injector);
|
const factoryResolver = jasmine.createSpyObj('factoryResolver', ['resolveComponentFactory']);
|
||||||
expect(strategyFactory.create()).toBeTruthy();
|
factoryResolver.resolveComponentFactory.and.returnValue(factory);
|
||||||
|
injector = jasmine.createSpyObj('injector', ['get']);
|
||||||
|
injector.get.and.returnValue(factoryResolver);
|
||||||
|
|
||||||
|
const strategyFactory = new ComponentNgElementStrategyFactory(FakeComponent, injector);
|
||||||
|
expect(strategyFactory.create(injector)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('after connected', () => {
|
describe('after connected', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Set up an initial value to make sure it is passed to the component
|
// Set up an initial value to make sure it is passed to the component
|
||||||
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
||||||
|
injector.get.and.returnValue(applicationRef);
|
||||||
strategy.connect(document.createElement('div'));
|
strategy.connect(document.createElement('div'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -95,7 +99,7 @@ describe('ComponentFactoryNgElementStrategy', () => {
|
||||||
it('should not detect changes', fakeAsync(() => {
|
it('should not detect changes', fakeAsync(() => {
|
||||||
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
||||||
tick(16); // scheduler waits 16ms if RAF is unavailable
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
||||||
expect(componentRef.changeDetectorRef.detectChanges).toHaveBeenCalledTimes(0);
|
expect(componentRef.changeDetectorRef.detectChanges).not.toHaveBeenCalled();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ if (typeof customElements !== 'undefined') {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send input values from attributes when connected', () => {
|
it('should send input values from attributes when connected', () => {
|
||||||
const element = new NgElementCtor();
|
const element = new NgElementCtor(injector);
|
||||||
element.setAttribute('foo-foo', 'value-foo-foo');
|
element.setAttribute('foo-foo', 'value-foo-foo');
|
||||||
element.setAttribute('barbar', 'value-barbar');
|
element.setAttribute('barbar', 'value-barbar');
|
||||||
element.connectedCallback();
|
element.connectedCallback();
|
||||||
|
@ -62,7 +62,7 @@ if (typeof customElements !== 'undefined') {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should listen to output events after connected', () => {
|
it('should listen to output events after connected', () => {
|
||||||
const element = new NgElementCtor();
|
const element = new NgElementCtor(injector);
|
||||||
element.connectedCallback();
|
element.connectedCallback();
|
||||||
|
|
||||||
let eventValue: any = null;
|
let eventValue: any = null;
|
||||||
|
@ -73,7 +73,7 @@ if (typeof customElements !== 'undefined') {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not listen to output events after disconnected', () => {
|
it('should not listen to output events after disconnected', () => {
|
||||||
const element = new NgElementCtor();
|
const element = new NgElementCtor(injector);
|
||||||
element.connectedCallback();
|
element.connectedCallback();
|
||||||
element.disconnectedCallback();
|
element.disconnectedCallback();
|
||||||
expect(strategy.disconnectCalled).toBe(true);
|
expect(strategy.disconnectCalled).toBe(true);
|
||||||
|
@ -86,7 +86,7 @@ if (typeof customElements !== 'undefined') {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly set getters/setters on the element', () => {
|
it('should properly set getters/setters on the element', () => {
|
||||||
const element = new NgElementCtor();
|
const element = new NgElementCtor(injector);
|
||||||
element.fooFoo = 'foo-foo-value';
|
element.fooFoo = 'foo-foo-value';
|
||||||
element.barBar = 'barBar-value';
|
element.barBar = 'barBar-value';
|
||||||
|
|
||||||
|
@ -104,29 +104,26 @@ if (typeof customElements !== 'undefined') {
|
||||||
NgElementCtorWithChangedAttr = createNgElementConstructor(TestComponent, {
|
NgElementCtorWithChangedAttr = createNgElementConstructor(TestComponent, {
|
||||||
injector,
|
injector,
|
||||||
strategyFactory,
|
strategyFactory,
|
||||||
propertyInputs: ['prop1', 'prop2'],
|
attributeToPropertyInputs: {'attr-1': 'fooFoo', 'attr-2': 'barbar'}
|
||||||
attributeToPropertyInputs: {'attr-1': 'prop1', 'attr-2': 'prop2'}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
customElements.define('test-element-with-changed-attributes', NgElementCtorWithChangedAttr);
|
customElements.define('test-element-with-changed-attributes', NgElementCtorWithChangedAttr);
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => { element = new NgElementCtorWithChangedAttr(); });
|
beforeEach(() => { element = new NgElementCtorWithChangedAttr(injector); });
|
||||||
|
|
||||||
it('should affect which attributes are watched', () => {
|
it('should affect which attributes are watched', () => {
|
||||||
expect(NgElementCtorWithChangedAttr.observedAttributes).toEqual(['attr-1', 'attr-2']);
|
expect(NgElementCtorWithChangedAttr.observedAttributes).toEqual(['attr-1', 'attr-2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send attribute values as inputs when connected', () => {
|
it('should send attribute values as inputs when connected', () => {
|
||||||
const element = new NgElementCtorWithChangedAttr();
|
const element = new NgElementCtorWithChangedAttr(injector);
|
||||||
element.setAttribute('attr-1', 'value-1');
|
element.setAttribute('attr-1', 'value-1');
|
||||||
element.setAttribute('attr-2', 'value-2');
|
element.setAttribute('attr-2', 'value-2');
|
||||||
element.setAttribute('attr-3', 'value-3'); // Made-up attribute
|
|
||||||
element.connectedCallback();
|
element.connectedCallback();
|
||||||
|
|
||||||
expect(strategy.getInputValue('prop1')).toBe('value-1');
|
expect(strategy.getInputValue('fooFoo')).toBe('value-1');
|
||||||
expect(strategy.getInputValue('prop2')).toBe('value-2');
|
expect(strategy.getInputValue('barbar')).toBe('value-2');
|
||||||
expect(strategy.getInputValue('prop3')).not.toBe('value-3');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -56,7 +56,7 @@ if [[ ${TRAVIS} &&
|
||||||
travisFoldStart "yarn-install.aio"
|
travisFoldStart "yarn-install.aio"
|
||||||
(
|
(
|
||||||
# HACK (don't submit with this): Build Angular
|
# HACK (don't submit with this): Build Angular
|
||||||
./build.sh
|
./build.sh --packages=core,elements --examples=false
|
||||||
|
|
||||||
cd ${PROJECT_ROOT}/aio
|
cd ${PROJECT_ROOT}/aio
|
||||||
yarn install --frozen-lockfile --non-interactive
|
yarn install --frozen-lockfile --non-interactive
|
||||||
|
|
84
test-main.js
84
test-main.js
|
@ -76,7 +76,7 @@ Promise
|
||||||
.resolve()
|
.resolve()
|
||||||
|
|
||||||
// Load browser-specific polyfills for custom elements.
|
// Load browser-specific polyfills for custom elements.
|
||||||
// .then(function() { return loadCustomElementsPolyfills(); })
|
.then(function() { return loadCustomElementsPolyfills(); })
|
||||||
|
|
||||||
// Load necessary testing packages.
|
// Load necessary testing packages.
|
||||||
.then(function() {
|
.then(function() {
|
||||||
|
@ -151,29 +151,35 @@ function loadCustomElementsPolyfills() {
|
||||||
Object.setPrototypeOf = setPrototypeOf;
|
Object.setPrototypeOf = setPrototypeOf;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The custom elements polyfill will patch `(HTML)Element` properties, including `innerHTML`:
|
// The custom elements polyfill will patch properties and methods on `(HTML)Element` and `Node`
|
||||||
// https://github.com/webcomponents/custom-elements/blob/32f043c3a5e5fc3e035342c0ef10c6786fa416d7/src/Patch/Element.js#L28-L78
|
// (among others), including `(HTML)Element#innerHTML` and `Node#removeChild()`:
|
||||||
// The patched `innerHTML` setter will try to traverse the DOM (via `nextSibling`), which leads to
|
// https://github.com/webcomponents/custom-elements/blob/4f7072c0dbda4beb505d16967acfffd33337b325/src/Patch/Element.js#L28-L73
|
||||||
// infinite loops when testing `HtmlSanitizer` with cloberred elements on browsers that do not
|
// https://github.com/webcomponents/custom-elements/blob/4f7072c0dbda4beb505d16967acfffd33337b325/src/Patch/Node.js#L105-L120
|
||||||
// support the `<template>` element:
|
// The patched `innerHTML` setter and `removeChild()` method will try to traverse the DOM (via
|
||||||
|
// `nextSibling` and `parentNode` respectively), 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
|
// 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
|
// To avoid that, we "unpatch" these properties/methods and apply the patch only for the relevant
|
||||||
// `@angular/elements` tests.
|
// `@angular/elements` tests.
|
||||||
var patchTarget;
|
var patchConfig = {'innerHTML': ['Element', 'HTMLElement'], 'removeChild': ['Node']};
|
||||||
var originalDescriptor;
|
var patchTargets = {};
|
||||||
|
var originalDescriptors = {};
|
||||||
if (!window.customElements) {
|
if (!window.customElements) {
|
||||||
['Element', 'HTMLElement']
|
Object.keys(patchConfig).forEach(function(prop) {
|
||||||
.map(function(name) { return window[name].prototype; })
|
patchConfig[prop]
|
||||||
.some(function(candidatePatchTarget) {
|
.map(function(name) { return window[name].prototype; })
|
||||||
var candidateOriginalDescriptor =
|
.some(function(candidatePatchTarget) {
|
||||||
Object.getOwnPropertyDescriptor(candidatePatchTarget, 'innerHTML');
|
var candidateOriginalDescriptor =
|
||||||
|
Object.getOwnPropertyDescriptor(candidatePatchTarget, prop);
|
||||||
|
|
||||||
if (candidateOriginalDescriptor) {
|
if (candidateOriginalDescriptor) {
|
||||||
patchTarget = candidatePatchTarget;
|
patchTargets[prop] = candidatePatchTarget;
|
||||||
originalDescriptor = candidateOriginalDescriptor;
|
originalDescriptors[prop] = candidateOriginalDescriptor;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var polyfillPath = !window.customElements ?
|
var polyfillPath = !window.customElements ?
|
||||||
|
@ -188,23 +194,31 @@ function loadCustomElementsPolyfills() {
|
||||||
// but custom element polyfills will replace `HTMLElement` with an anonymous function.
|
// but custom element polyfills will replace `HTMLElement` with an anonymous function.
|
||||||
Object.defineProperty(HTMLElement, 'name', {value: 'HTMLElement'});
|
Object.defineProperty(HTMLElement, 'name', {value: 'HTMLElement'});
|
||||||
|
|
||||||
// Create helper functions on `window` for patching/restoring `(HTML)Element#innerHTML`.
|
// Create helper functions on `window` for patching/restoring properties/methods.
|
||||||
if (!patchTarget) {
|
Object.keys(patchConfig).forEach(function(prop) {
|
||||||
window.$$patchInnerHtmlProp = window.$$restoreInnerHtmlProp = function() {};
|
var patchMethod = '$$patch_' + prop;
|
||||||
} else {
|
var restoreMethod = '$$restore_' + prop;
|
||||||
var patchedDescriptor = Object.getOwnPropertyDescriptor(patchTarget, 'innerHTML');
|
|
||||||
|
|
||||||
window.$$patchInnerHtmlProp = function() {
|
if (!patchTargets[prop]) {
|
||||||
Object.defineProperty(patchTarget, 'innerHTML', patchedDescriptor);
|
// No patching detected. Create no-op functions.
|
||||||
};
|
window[patchMethod] = window[restoreMethod] = function() {};
|
||||||
window.$$restoreInnerHtmlProp = function() {
|
} else {
|
||||||
Object.defineProperty(patchTarget, 'innerHTML', originalDescriptor);
|
var patchTarget = patchTargets[prop];
|
||||||
};
|
var originalDescriptor = originalDescriptors[prop];
|
||||||
|
var patchedDescriptor = Object.getOwnPropertyDescriptor(patchTarget, prop);
|
||||||
|
|
||||||
// Restore `innerHTML`. The patch will be manually applied only during the
|
window[patchMethod] = function() {
|
||||||
// `@angular/elements` tests that need it.
|
Object.defineProperty(patchTarget, prop, patchedDescriptor);
|
||||||
window.$$restoreInnerHtmlProp();
|
};
|
||||||
}
|
window[restoreMethod] = function() {
|
||||||
|
Object.defineProperty(patchTarget, prop, originalDescriptor);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Restore `prop`. The patch will be manually applied only during the
|
||||||
|
// `@angular/elements` tests that need it.
|
||||||
|
window[restoreMethod]();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return loadedPromise;
|
return loadedPromise;
|
||||||
|
|
|
@ -16,14 +16,13 @@ export interface NgElementConfig {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
injector: Injector;
|
injector: Injector;
|
||||||
propertyInputs?: string[];
|
|
||||||
strategyFactory?: NgElementStrategyFactory;
|
strategyFactory?: NgElementStrategyFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export interface NgElementConstructor<P> {
|
export interface NgElementConstructor<P> {
|
||||||
readonly observedAttributes: string[];
|
readonly observedAttributes: string[];
|
||||||
new (): NgElement & WithProperties<P>;
|
new (injector: Injector): NgElement & WithProperties<P>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
|
@ -43,7 +42,7 @@ export interface NgElementStrategyEvent {
|
||||||
|
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export interface NgElementStrategyFactory {
|
export interface NgElementStrategyFactory {
|
||||||
create(): NgElementStrategy;
|
create(injector: Injector): NgElementStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
|
|
Loading…
Reference in New Issue