
committed by
Miško Hevery

parent
22b96b9690
commit
7c9b411777
@ -75,6 +75,7 @@
|
|||||||
"@angular/common": "^5.2.0",
|
"@angular/common": "^5.2.0",
|
||||||
"@angular/compiler": "^5.2.0",
|
"@angular/compiler": "^5.2.0",
|
||||||
"@angular/core": "^5.2.0",
|
"@angular/core": "^5.2.0",
|
||||||
|
"@angular/elements": "file:../dist/packages-dist/elements",
|
||||||
"@angular/forms": "^5.2.0",
|
"@angular/forms": "^5.2.0",
|
||||||
"@angular/http": "^5.2.0",
|
"@angular/http": "^5.2.0",
|
||||||
"@angular/material": "^5.0.0-rc.1",
|
"@angular/material": "^5.0.0-rc.1",
|
||||||
@ -83,6 +84,7 @@
|
|||||||
"@angular/platform-server": "^5.2.0",
|
"@angular/platform-server": "^5.2.0",
|
||||||
"@angular/router": "^5.2.0",
|
"@angular/router": "^5.2.0",
|
||||||
"@angular/service-worker": "^1.0.0-beta.16",
|
"@angular/service-worker": "^1.0.0-beta.16",
|
||||||
|
"@webcomponents/custom-elements": "^1.0.8",
|
||||||
"classlist.js": "^1.1.20150312",
|
"classlist.js": "^1.1.20150312",
|
||||||
"core-js": "^2.4.1",
|
"core-js": "^2.4.1",
|
||||||
"jasmine": "^2.6.0",
|
"jasmine": "^2.6.0",
|
||||||
|
@ -7,7 +7,6 @@ import { MatProgressBar, MatSidenav } from '@angular/material';
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { of } from 'rxjs/observable/of';
|
|
||||||
import { timer } from 'rxjs/observable/timer';
|
import { timer } from 'rxjs/observable/timer';
|
||||||
import 'rxjs/add/operator/mapTo';
|
import 'rxjs/add/operator/mapTo';
|
||||||
|
|
||||||
@ -16,7 +15,6 @@ import { AppModule } from './app.module';
|
|||||||
import { DocumentService } from 'app/documents/document.service';
|
import { DocumentService } from 'app/documents/document.service';
|
||||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||||
import { Deployment } from 'app/shared/deployment.service';
|
import { Deployment } from 'app/shared/deployment.service';
|
||||||
import { EmbedComponentsService } from 'app/embed-components/embed-components.service';
|
|
||||||
import { GaService } from 'app/shared/ga.service';
|
import { GaService } from 'app/shared/ga.service';
|
||||||
import { LocationService } from 'app/shared/location.service';
|
import { LocationService } from 'app/shared/location.service';
|
||||||
import { Logger } from 'app/shared/logger.service';
|
import { Logger } from 'app/shared/logger.service';
|
||||||
@ -1280,7 +1278,6 @@ function createTestingModule(initialUrl: string, mode: string = 'stable') {
|
|||||||
imports: [ AppModule ],
|
imports: [ AppModule ],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: APP_BASE_HREF, useValue: '/' },
|
{ provide: APP_BASE_HREF, useValue: '/' },
|
||||||
{ provide: EmbedComponentsService, useClass: TestEmbedComponentsService },
|
|
||||||
{ provide: GaService, useClass: TestGaService },
|
{ provide: GaService, useClass: TestGaService },
|
||||||
{ provide: HttpClient, useClass: TestHttpClient },
|
{ provide: HttpClient, useClass: TestHttpClient },
|
||||||
{ provide: LocationService, useFactory: () => mockLocationService },
|
{ provide: LocationService, useFactory: () => mockLocationService },
|
||||||
@ -1295,10 +1292,6 @@ function createTestingModule(initialUrl: string, mode: string = 'stable') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestEmbedComponentsService {
|
|
||||||
embedInto = jasmine.createSpy('embedInto').and.returnValue(of([]));
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestGaService {
|
class TestGaService {
|
||||||
locationChanged = jasmine.createSpy('locationChanged');
|
locationChanged = jasmine.createSpy('locationChanged');
|
||||||
}
|
}
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
import { AppModule } from 'app/app.module';
|
|
||||||
import { ComponentsOrModulePath, EMBEDDED_COMPONENTS } from 'app/embed-components/embed-components.service';
|
|
||||||
import { embeddedComponents } from 'app/embedded/embedded.module';
|
|
||||||
|
|
||||||
describe('AppModule', () => {
|
|
||||||
let componentsMap: {[multiSelectorstring: string]: ComponentsOrModulePath};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({imports: [AppModule]});
|
|
||||||
componentsMap = TestBed.get(EMBEDDED_COMPONENTS);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should provide a map of selectors to embedded components (or module)', () => {
|
|
||||||
const allSelectors = Object.keys(componentsMap);
|
|
||||||
|
|
||||||
expect(allSelectors.length).toBeGreaterThan(1);
|
|
||||||
allSelectors.forEach(selector => {
|
|
||||||
const value = componentsMap[selector];
|
|
||||||
const isArrayOrString = Array.isArray(value) || (typeof value === 'string');
|
|
||||||
expect(isArrayOrString).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should provide a list of eagerly-loaded embedded components', () => {
|
|
||||||
|
|
||||||
const eagerConfig = Object.keys(componentsMap).filter(selector => Array.isArray(componentsMap[selector]));
|
|
||||||
expect(eagerConfig.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
const eagerSelectors = eagerConfig.reduce<string[]>((selectors, config) => selectors.concat(config.split(',')), []);
|
|
||||||
expect(eagerSelectors.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// For example...
|
|
||||||
expect(eagerSelectors).toContain('aio-toc');
|
|
||||||
expect(eagerSelectors).toContain('aio-announcement-bar');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should provide a list of lazy-loaded embedded components', () => {
|
|
||||||
const lazySelector = Object.keys(componentsMap).find(selector => selector.includes('code-example'))!;
|
|
||||||
const selectorCount = lazySelector.split(',').length;
|
|
||||||
|
|
||||||
expect(lazySelector).not.toBeNull();
|
|
||||||
expect(selectorCount).toBe(embeddedComponents.length);
|
|
||||||
|
|
||||||
// For example...
|
|
||||||
expect(lazySelector).toContain('code-example');
|
|
||||||
expect(lazySelector).toContain('code-tabs');
|
|
||||||
expect(lazySelector).toContain('live-example');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
@ -11,12 +11,7 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
|
|
||||||
import { ROUTES } from '@angular/router';
|
|
||||||
|
|
||||||
|
|
||||||
import { AnnouncementBarComponent } from 'app/embedded/announcement-bar/announcement-bar.component';
|
|
||||||
import { AppComponent } from 'app/app.component';
|
import { AppComponent } from 'app/app.component';
|
||||||
import { EMBEDDED_COMPONENTS, EmbeddedComponentsMap } from 'app/embed-components/embed-components.service';
|
|
||||||
import { CustomIconRegistry, SVG_ICONS } from 'app/shared/custom-icon-registry';
|
import { CustomIconRegistry, SVG_ICONS } from 'app/shared/custom-icon-registry';
|
||||||
import { Deployment } from 'app/shared/deployment.service';
|
import { Deployment } from 'app/shared/deployment.service';
|
||||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||||
@ -42,14 +37,10 @@ import { TocService } from 'app/shared/toc.service';
|
|||||||
import { CurrentDateToken, currentDateProvider } from 'app/shared/current-date';
|
import { CurrentDateToken, currentDateProvider } from 'app/shared/current-date';
|
||||||
import { WindowToken, windowProvider } from 'app/shared/window';
|
import { WindowToken, windowProvider } from 'app/shared/window';
|
||||||
|
|
||||||
import { EmbedComponentsModule } from 'app/embed-components/embed-components.module';
|
import { CustomElementsModule } from 'app/custom-elements/custom-elements.module';
|
||||||
import { SharedModule } from 'app/shared/shared.module';
|
import { SharedModule } from 'app/shared/shared.module';
|
||||||
import { SwUpdatesModule } from 'app/sw-updates/sw-updates.module';
|
import { SwUpdatesModule } from 'app/sw-updates/sw-updates.module';
|
||||||
|
|
||||||
|
|
||||||
// The path to the `EmbeddedModule`.
|
|
||||||
const embeddedModulePath = 'app/embedded/embedded.module#EmbeddedModule';
|
|
||||||
|
|
||||||
// These are the hardcoded inline svg sources to be used by the `<mat-icon>` component
|
// These are the hardcoded inline svg sources to be used by the `<mat-icon>` component
|
||||||
export const svgIconProviders = [
|
export const svgIconProviders = [
|
||||||
{
|
{
|
||||||
@ -100,7 +91,7 @@ export const svgIconProviders = [
|
|||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
EmbedComponentsModule,
|
CustomElementsModule,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
@ -108,10 +99,9 @@ export const svgIconProviders = [
|
|||||||
MatSidenavModule,
|
MatSidenavModule,
|
||||||
MatToolbarModule,
|
MatToolbarModule,
|
||||||
SwUpdatesModule,
|
SwUpdatesModule,
|
||||||
SharedModule
|
SharedModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AnnouncementBarComponent,
|
|
||||||
AppComponent,
|
AppComponent,
|
||||||
DocViewerComponent,
|
DocViewerComponent,
|
||||||
DtComponent,
|
DtComponent,
|
||||||
@ -142,27 +132,8 @@ export const svgIconProviders = [
|
|||||||
TocService,
|
TocService,
|
||||||
{ provide: CurrentDateToken, useFactory: currentDateProvider },
|
{ provide: CurrentDateToken, useFactory: currentDateProvider },
|
||||||
{ provide: WindowToken, useFactory: windowProvider },
|
{ provide: WindowToken, useFactory: windowProvider },
|
||||||
|
|
||||||
{
|
|
||||||
provide: EMBEDDED_COMPONENTS,
|
|
||||||
useValue: {
|
|
||||||
/* tslint:disable: max-line-length */
|
|
||||||
'aio-announcement-bar': [AnnouncementBarComponent],
|
|
||||||
'aio-toc': [TocComponent],
|
|
||||||
'aio-api-list, aio-contributor-list, aio-file-not-found-search, aio-resource-list, code-example, code-tabs, current-location, live-example': embeddedModulePath,
|
|
||||||
/* tslint:enable: max-line-length */
|
|
||||||
} as EmbeddedComponentsMap,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// This is currently the only way to get `@angular/cli`
|
|
||||||
// to split `EmbeddedModule` into a separate chunk :(
|
|
||||||
provide: ROUTES,
|
|
||||||
useValue: [{ path: '/embedded', loadChildren: embeddedModulePath }],
|
|
||||||
multi: true,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
entryComponents: [ AnnouncementBarComponent, TocComponent ],
|
entryComponents: [ TocComponent ],
|
||||||
bootstrap: [ AppComponent ]
|
bootstrap: [ AppComponent ]
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule { }
|
||||||
}
|
|
||||||
|
@ -66,10 +66,7 @@ describe('AnnouncementBarComponent', () => {
|
|||||||
const request = httpMock.expectOne('generated/announcements.json');
|
const request = httpMock.expectOne('generated/announcements.json');
|
||||||
request.flush('some random response');
|
request.flush('some random response');
|
||||||
expect(component.announcement).toBeUndefined();
|
expect(component.announcement).toBeUndefined();
|
||||||
expect(mockLogger.output.error).toEqual([
|
expect(mockLogger.output.error[0][0]).toContain('generated/announcements.json contains invalid data:');
|
||||||
[jasmine.any(Error)]
|
|
||||||
]);
|
|
||||||
expect(mockLogger.output.error[0][0].message).toMatch(/^generated\/announcements\.json contains invalid data:/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a failed request for `announcements.json`', () => {
|
it('should handle a failed request for `announcements.json`', () => {
|
||||||
@ -77,10 +74,7 @@ describe('AnnouncementBarComponent', () => {
|
|||||||
const request = httpMock.expectOne('generated/announcements.json');
|
const request = httpMock.expectOne('generated/announcements.json');
|
||||||
request.error(new ErrorEvent('404'));
|
request.error(new ErrorEvent('404'));
|
||||||
expect(component.announcement).toBeUndefined();
|
expect(component.announcement).toBeUndefined();
|
||||||
expect(mockLogger.output.error).toEqual([
|
expect(mockLogger.output.error[0][0]).toContain('generated/announcements.json request failed:');
|
||||||
[jasmine.any(Error)]
|
|
||||||
]);
|
|
||||||
expect(mockLogger.output.error[0][0].message).toMatch(/^generated\/announcements\.json request failed:/);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -59,12 +59,12 @@ export class AnnouncementBarComponent implements OnInit {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.http.get<Announcement[]>(announcementsPath)
|
this.http.get<Announcement[]>(announcementsPath)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.logger.error(new Error(`${announcementsPath} request failed: ${error.message}`));
|
this.logger.error(`${announcementsPath} request failed: ${error.message}`);
|
||||||
return [];
|
return [];
|
||||||
})
|
})
|
||||||
.map(announcements => this.findCurrentAnnouncement(announcements))
|
.map(announcements => this.findCurrentAnnouncement(announcements))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.logger.error(new Error(`${announcementsPath} contains invalid data: ${error.message}`));
|
this.logger.error(`${announcementsPath} contains invalid data: ${error.message}`);
|
||||||
return [];
|
return [];
|
||||||
})
|
})
|
||||||
.subscribe(announcement => this.announcement = announcement);
|
.subscribe(announcement => this.announcement = announcement);
|
@ -0,0 +1,15 @@
|
|||||||
|
import { NgModule, Type } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { AnnouncementBarComponent } from './announcement-bar.component';
|
||||||
|
import { WithCustomElementComponent } from '../element-registry';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ CommonModule, SharedModule, HttpClientModule ],
|
||||||
|
declarations: [ AnnouncementBarComponent ],
|
||||||
|
entryComponents: [ AnnouncementBarComponent ],
|
||||||
|
})
|
||||||
|
export class AnnouncementBarModule implements WithCustomElementComponent {
|
||||||
|
customElementComponent: Type<any> = AnnouncementBarComponent;
|
||||||
|
}
|
@ -4,7 +4,9 @@ import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
|||||||
import { ApiListComponent } from './api-list.component';
|
import { ApiListComponent } from './api-list.component';
|
||||||
import { ApiItem, ApiSection, ApiService } from './api.service';
|
import { ApiItem, ApiSection, ApiService } from './api.service';
|
||||||
import { LocationService } from 'app/shared/location.service';
|
import { LocationService } from 'app/shared/location.service';
|
||||||
import { SharedModule } from 'app/shared/shared.module';
|
import { Logger } from 'app/shared/logger.service';
|
||||||
|
import { MockLogger } from 'testing/logger.service';
|
||||||
|
import { ApiListModule } from './api-list.module';
|
||||||
|
|
||||||
describe('ApiListComponent', () => {
|
describe('ApiListComponent', () => {
|
||||||
let component: ApiListComponent;
|
let component: ApiListComponent;
|
||||||
@ -13,10 +15,10 @@ describe('ApiListComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [ SharedModule ],
|
imports: [ ApiListModule ],
|
||||||
declarations: [ ApiListComponent ],
|
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ApiService, useClass: TestApiService },
|
{ provide: ApiService, useClass: TestApiService },
|
||||||
|
{ provide: Logger, useClass: MockLogger },
|
||||||
{ provide: LocationService, useClass: TestLocationService }
|
{ provide: LocationService, useClass: TestLocationService }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@ -37,11 +39,11 @@ describe('ApiListComponent', () => {
|
|||||||
let badItem: ApiItem|undefined;
|
let badItem: ApiItem|undefined;
|
||||||
expect(filtered.length).toBeGreaterThan(0, 'expected something');
|
expect(filtered.length).toBeGreaterThan(0, 'expected something');
|
||||||
expect(filtered.every(section => section.items.every(
|
expect(filtered.every(section => section.items.every(
|
||||||
item => {
|
item => {
|
||||||
const ok = item.show === itemTest(item);
|
const ok = item.show === itemTest(item);
|
||||||
if (!ok) { badItem = item; }
|
if (!ok) { badItem = item; }
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
))).toBe(true, `${label} fail: ${JSON.stringify(badItem, null, 2)}`);
|
))).toBe(true, `${label} fail: ${JSON.stringify(badItem, null, 2)}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -25,7 +25,7 @@ class SearchCriteria {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'aio-api-list',
|
selector: 'aio-api-list',
|
||||||
templateUrl: './api-list.component.html'
|
templateUrl: './api-list.component.html',
|
||||||
})
|
})
|
||||||
export class ApiListComponent implements OnInit {
|
export class ApiListComponent implements OnInit {
|
||||||
|
|
||||||
@ -69,7 +69,6 @@ export class ApiListComponent implements OnInit {
|
|||||||
private locationService: LocationService) { }
|
private locationService: LocationService) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
|
||||||
this.filteredSections = combineLatest(
|
this.filteredSections = combineLatest(
|
||||||
this.apiService.sections,
|
this.apiService.sections,
|
||||||
this.criteriaSubject,
|
this.criteriaSubject,
|
17
aio/src/app/custom-elements/api/api-list.module.ts
Normal file
17
aio/src/app/custom-elements/api/api-list.module.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { NgModule, Type } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { ApiListComponent } from './api-list.component';
|
||||||
|
import { ApiService } from './api.service';
|
||||||
|
import { WithCustomElementComponent } from '../element-registry';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ CommonModule, SharedModule, HttpClientModule ],
|
||||||
|
declarations: [ ApiListComponent ],
|
||||||
|
entryComponents: [ ApiListComponent ],
|
||||||
|
providers: [ ApiService ]
|
||||||
|
})
|
||||||
|
export class ApiListModule implements WithCustomElementComponent {
|
||||||
|
customElementComponent: Type<any> = ApiListComponent;
|
||||||
|
}
|
100
aio/src/app/custom-elements/code/code-example.component.spec.ts
Normal file
100
aio/src/app/custom-elements/code/code-example.component.spec.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { Component, ViewChild } from '@angular/core';
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CodeExampleComponent } from './code-example.component';
|
||||||
|
import { CodeExampleModule } from './code-example.module';
|
||||||
|
import { Logger } from 'app/shared/logger.service';
|
||||||
|
import { MockLogger } from 'testing/logger.service';
|
||||||
|
|
||||||
|
describe('CodeExampleComponent', () => {
|
||||||
|
let hostComponent: HostComponent;
|
||||||
|
let codeExampleComponent: CodeExampleComponent;
|
||||||
|
let fixture: ComponentFixture<HostComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ CodeExampleModule ],
|
||||||
|
declarations: [
|
||||||
|
HostComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: Logger, useClass: MockLogger },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(HostComponent);
|
||||||
|
hostComponent = fixture.componentInstance;
|
||||||
|
codeExampleComponent = hostComponent.codeExampleComponent;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to capture the code snippet provided in content', () => {
|
||||||
|
expect(codeExampleComponent.code.trim()).toBe(`const foo = "bar";`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change aio-code classes based on title presence', () => {
|
||||||
|
expect(codeExampleComponent.title).toBe('Great Example');
|
||||||
|
expect(fixture.nativeElement.querySelector('header')).toBeTruthy();
|
||||||
|
expect(codeExampleComponent.classes).toEqual({
|
||||||
|
'headed-code': true,
|
||||||
|
'simple-code': false
|
||||||
|
});
|
||||||
|
|
||||||
|
codeExampleComponent.title = '';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(codeExampleComponent.title).toBe('');
|
||||||
|
expect(fixture.nativeElement.querySelector('header')).toBeFalsy();
|
||||||
|
expect(codeExampleComponent.classes).toEqual({
|
||||||
|
'headed-code': false,
|
||||||
|
'simple-code': true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set avoidFile class if path has .avoid.', () => {
|
||||||
|
const codeExampleComponentElement: HTMLElement =
|
||||||
|
fixture.nativeElement.querySelector('code-example');
|
||||||
|
|
||||||
|
expect(codeExampleComponent.path).toBe('code-path');
|
||||||
|
expect(codeExampleComponentElement.className.indexOf('avoidFile') === -1).toBe(true);
|
||||||
|
|
||||||
|
codeExampleComponent.path = 'code-path.avoid.';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(codeExampleComponentElement.className.indexOf('avoidFile') === -1).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should coerce hidecopy', () => {
|
||||||
|
expect(codeExampleComponent.hidecopy).toBe(false);
|
||||||
|
|
||||||
|
hostComponent.hidecopy = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(codeExampleComponent.hidecopy).toBe(true);
|
||||||
|
|
||||||
|
hostComponent.hidecopy = 'false';
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(codeExampleComponent.hidecopy).toBe(false);
|
||||||
|
|
||||||
|
hostComponent.hidecopy = 'true';
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(codeExampleComponent.hidecopy).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'aio-host-comp',
|
||||||
|
template: `
|
||||||
|
<code-example [title]="title" [path]="path" [hidecopy]="hidecopy">
|
||||||
|
{{code}}
|
||||||
|
</code-example>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class HostComponent {
|
||||||
|
code = `const foo = "bar";`;
|
||||||
|
title = 'Great Example';
|
||||||
|
path = 'code-path';
|
||||||
|
hidecopy: boolean | string = false;
|
||||||
|
|
||||||
|
@ViewChild(CodeExampleComponent) codeExampleComponent: CodeExampleComponent;
|
||||||
|
}
|
91
aio/src/app/custom-elements/code/code-example.component.ts
Normal file
91
aio/src/app/custom-elements/code/code-example.component.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/* tslint:disable component-selector */
|
||||||
|
import { Component, HostBinding, ElementRef, ViewChild, Input, AfterViewInit } from '@angular/core';
|
||||||
|
import { CodeComponent } from './code.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An embeddable code block that displays nicely formatted code.
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* <code-example language="ts" linenums="2" class="special" title="Do Stuff">
|
||||||
|
* // a code block
|
||||||
|
* console.log('do stuff');
|
||||||
|
* </code-example>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'code-example',
|
||||||
|
template: `
|
||||||
|
<!-- Content projection is used to get the content HTML provided to this component -->
|
||||||
|
<div #content style="display: none"><ng-content></ng-content></div>
|
||||||
|
|
||||||
|
<header *ngIf="title">{{title}}</header>
|
||||||
|
|
||||||
|
<aio-code [ngClass]="classes"
|
||||||
|
[language]="language"
|
||||||
|
[linenums]="linenums"
|
||||||
|
[path]="path"
|
||||||
|
[region]="region"
|
||||||
|
[hideCopy]="hidecopy"
|
||||||
|
[title]="title">
|
||||||
|
</aio-code>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class CodeExampleComponent implements AfterViewInit {
|
||||||
|
classes: {};
|
||||||
|
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
@Input() language: string;
|
||||||
|
|
||||||
|
@Input() linenums: string;
|
||||||
|
|
||||||
|
@Input() region: string;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set title(title: string) {
|
||||||
|
this._title = title;
|
||||||
|
this.classes = {
|
||||||
|
'headed-code': !!this.title,
|
||||||
|
'simple-code': !this.title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
get title(): string { return this._title; }
|
||||||
|
private _title: string;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set path(path: string) {
|
||||||
|
this._path = path;
|
||||||
|
this.isAvoid = this.path.indexOf('.avoid.') !== -1;
|
||||||
|
}
|
||||||
|
get path(): string { return this._path; }
|
||||||
|
private _path = '';
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set hidecopy(hidecopy: boolean) {
|
||||||
|
// Coerce the boolean value.
|
||||||
|
this._hidecopy = hidecopy != null && `${hidecopy}` !== 'false';
|
||||||
|
}
|
||||||
|
get hidecopy(): boolean { return this._hidecopy; }
|
||||||
|
private _hidecopy: boolean;
|
||||||
|
|
||||||
|
@Input('hide-copy')
|
||||||
|
set hyphenatedHideCopy(hidecopy: boolean) {
|
||||||
|
this.hidecopy = hidecopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input('hideCopy')
|
||||||
|
set capitalizedHideCopy(hidecopy: boolean) {
|
||||||
|
this.hidecopy = hidecopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostBinding('class.avoidFile') isAvoid = false;
|
||||||
|
|
||||||
|
@ViewChild('content') content: ElementRef;
|
||||||
|
|
||||||
|
@ViewChild(CodeComponent) aioCode: CodeComponent;
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.aioCode.code = this.content.nativeElement.innerHTML;
|
||||||
|
}
|
||||||
|
}
|
15
aio/src/app/custom-elements/code/code-example.module.ts
Normal file
15
aio/src/app/custom-elements/code/code-example.module.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { NgModule, Type } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CodeExampleComponent } from './code-example.component';
|
||||||
|
import { CodeModule } from './code.module';
|
||||||
|
import { WithCustomElementComponent } from '../element-registry';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ CommonModule, CodeModule ],
|
||||||
|
declarations: [ CodeExampleComponent ],
|
||||||
|
exports: [ CodeExampleComponent ],
|
||||||
|
entryComponents: [ CodeExampleComponent ]
|
||||||
|
})
|
||||||
|
export class CodeExampleModule implements WithCustomElementComponent {
|
||||||
|
customElementComponent: Type<any> = CodeExampleComponent;
|
||||||
|
}
|
96
aio/src/app/custom-elements/code/code-tabs.component.spec.ts
Normal file
96
aio/src/app/custom-elements/code/code-tabs.component.spec.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { Component, ViewChild, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { Logger } from 'app/shared/logger.service';
|
||||||
|
import { MockLogger } from 'testing/logger.service';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
import { CodeTabsComponent } from './code-tabs.component';
|
||||||
|
import { CodeTabsModule } from './code-tabs.module';
|
||||||
|
|
||||||
|
describe('CodeTabsComponent', () => {
|
||||||
|
let fixture: ComponentFixture<HostComponent>;
|
||||||
|
let hostComponent: HostComponent;
|
||||||
|
let codeTabsComponent: CodeTabsComponent;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ HostComponent ],
|
||||||
|
imports: [ CodeTabsModule, NoopAnimationsModule ],
|
||||||
|
schemas: [ NO_ERRORS_SCHEMA ],
|
||||||
|
providers: [
|
||||||
|
{ provide: Logger, useClass: MockLogger },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(HostComponent);
|
||||||
|
hostComponent = fixture.componentInstance;
|
||||||
|
codeTabsComponent = hostComponent.codeTabsComponent;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get correct tab info', () => {
|
||||||
|
const tabs = codeTabsComponent.tabs;
|
||||||
|
expect(tabs.length).toBe(2);
|
||||||
|
|
||||||
|
// First code pane expectations
|
||||||
|
expect(tabs[0].class).toBe('class-A');
|
||||||
|
expect(tabs[0].language).toBe('language-A');
|
||||||
|
expect(tabs[0].linenums).toBe('linenums-A');
|
||||||
|
expect(tabs[0].path).toBe('path-A');
|
||||||
|
expect(tabs[0].region).toBe('region-A');
|
||||||
|
expect(tabs[0].title).toBe('title-A');
|
||||||
|
expect(tabs[0].code.trim()).toBe('Code example 1');
|
||||||
|
|
||||||
|
// Second code pane expectations
|
||||||
|
expect(tabs[1].class).toBe('class-B');
|
||||||
|
expect(tabs[1].language).toBe('language-B');
|
||||||
|
expect(tabs[1].linenums).toBe('default-linenums', 'Default linenums should have been used');
|
||||||
|
expect(tabs[1].path).toBe('path-B');
|
||||||
|
expect(tabs[1].region).toBe('region-B');
|
||||||
|
expect(tabs[1].title).toBe('title-B');
|
||||||
|
expect(tabs[1].code.trim()).toBe('Code example 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the right number of tabs with the right labels and classes', () => {
|
||||||
|
const matTabs = fixture.nativeElement.querySelectorAll('.mat-tab-label');
|
||||||
|
expect(matTabs.length).toBe(2);
|
||||||
|
|
||||||
|
expect(matTabs[0].textContent.trim()).toBe('title-A');
|
||||||
|
expect(matTabs[0].querySelector('.class-A')).toBeTruthy();
|
||||||
|
|
||||||
|
expect(matTabs[1].textContent.trim()).toBe('title-B');
|
||||||
|
expect(matTabs[1].querySelector('.class-B')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the first tab with the right code', () => {
|
||||||
|
const codeContent = fixture.nativeElement.querySelector('aio-code').textContent;
|
||||||
|
expect(codeContent.indexOf('Code example 1') !== -1).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'aio-host-comp',
|
||||||
|
template: `
|
||||||
|
<code-tabs linenums="default-linenums">
|
||||||
|
<code-pane class="class-A"
|
||||||
|
language="language-A"
|
||||||
|
linenums="linenums-A"
|
||||||
|
path="path-A"
|
||||||
|
region="region-A"
|
||||||
|
title="title-A">
|
||||||
|
Code example 1
|
||||||
|
</code-pane>
|
||||||
|
<code-pane class="class-B"
|
||||||
|
language="language-B"
|
||||||
|
path="path-B"
|
||||||
|
region="region-B"
|
||||||
|
title="title-B">
|
||||||
|
Code example 2
|
||||||
|
</code-pane>
|
||||||
|
</code-tabs>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class HostComponent {
|
||||||
|
@ViewChild(CodeTabsComponent) codeTabsComponent: CodeTabsComponent;
|
||||||
|
}
|
81
aio/src/app/custom-elements/code/code-tabs.component.ts
Normal file
81
aio/src/app/custom-elements/code/code-tabs.component.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/* tslint:disable component-selector */
|
||||||
|
import { Component, AfterViewInit, ViewChild, Input, ViewChildren, QueryList, OnInit } from '@angular/core';
|
||||||
|
import { CodeComponent } from './code.component';
|
||||||
|
|
||||||
|
export interface TabInfo {
|
||||||
|
class: string|null;
|
||||||
|
code: string;
|
||||||
|
language: string|null;
|
||||||
|
linenums: any;
|
||||||
|
path: string;
|
||||||
|
region: string;
|
||||||
|
title: string|null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a set of tab group of code snippets.
|
||||||
|
*
|
||||||
|
* The innerHTML of the `<code-tabs>` component should contain `<code-pane>` elements.
|
||||||
|
* Each `<code-pane>` has the same interface as the embedded `<code-example>` component.
|
||||||
|
* The optional `linenums` attribute is the default `linenums` for each code pane.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'code-tabs',
|
||||||
|
template: `
|
||||||
|
<!-- Use content projection so that the provided HTML's code-panes can be split into tabs -->
|
||||||
|
<div #content style="display: none"><ng-content></ng-content></div>
|
||||||
|
|
||||||
|
<mat-tab-group class="code-tab-group" disableRipple>
|
||||||
|
<mat-tab style="overflow-y: hidden;" *ngFor="let tab of tabs">
|
||||||
|
<ng-template mat-tab-label>
|
||||||
|
<span class="{{ tab.class }}">{{ tab.title }}</span>
|
||||||
|
</ng-template>
|
||||||
|
<aio-code class="{{ tab.class }}"
|
||||||
|
[language]="tab.language"
|
||||||
|
[linenums]="tab.linenums"
|
||||||
|
[path]="tab.path"
|
||||||
|
[region]="tab.region"
|
||||||
|
[title]="tab.title">
|
||||||
|
</aio-code>
|
||||||
|
</mat-tab>
|
||||||
|
</mat-tab-group>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class CodeTabsComponent implements OnInit, AfterViewInit {
|
||||||
|
tabs: TabInfo[];
|
||||||
|
|
||||||
|
@Input('linenums') linenums: string;
|
||||||
|
|
||||||
|
@ViewChild('content') content;
|
||||||
|
|
||||||
|
@ViewChildren(CodeComponent) codeComponents: QueryList<CodeComponent>;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.tabs = [];
|
||||||
|
const codeExamples = this.content.nativeElement.querySelectorAll('code-pane');
|
||||||
|
|
||||||
|
for (let i = 0; i < codeExamples.length; i++) {
|
||||||
|
const tabContent = codeExamples[i];
|
||||||
|
this.tabs.push(this.getTabInfo(tabContent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.codeComponents.toArray().forEach((codeComponent, i) => {
|
||||||
|
codeComponent.code = this.tabs[i].code;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the extracted TabInfo data from the provided code-pane element. */
|
||||||
|
private getTabInfo(tabContent: HTMLElement): TabInfo {
|
||||||
|
return {
|
||||||
|
class: tabContent.getAttribute('class'),
|
||||||
|
code: tabContent.innerHTML,
|
||||||
|
language: tabContent.getAttribute('language'),
|
||||||
|
linenums: tabContent.getAttribute('linenums') || this.linenums,
|
||||||
|
path: tabContent.getAttribute('path') || '',
|
||||||
|
region: tabContent.getAttribute('region') || '',
|
||||||
|
title: tabContent.getAttribute('title')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
16
aio/src/app/custom-elements/code/code-tabs.module.ts
Normal file
16
aio/src/app/custom-elements/code/code-tabs.module.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { NgModule, Type } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CodeTabsComponent } from './code-tabs.component';
|
||||||
|
import { MatTabsModule } from '@angular/material';
|
||||||
|
import { CodeModule } from './code.module';
|
||||||
|
import { WithCustomElementComponent } from '../element-registry';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ CommonModule, MatTabsModule, CodeModule ],
|
||||||
|
declarations: [ CodeTabsComponent ],
|
||||||
|
exports: [ CodeTabsComponent ],
|
||||||
|
entryComponents: [ CodeTabsComponent ]
|
||||||
|
})
|
||||||
|
export class CodeTabsModule implements WithCustomElementComponent {
|
||||||
|
customElementComponent: Type<any> = CodeTabsComponent;
|
||||||
|
}
|
@ -1,10 +1,11 @@
|
|||||||
import { Component, DebugElement } from '@angular/core';
|
import { Component, ViewChild, AfterViewInit } from '@angular/core';
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { MatSnackBarModule, MatSnackBar } from '@angular/material';
|
import { MatSnackBar } from '@angular/material';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
import { CodeComponent } from './code.component';
|
import { CodeComponent } from './code.component';
|
||||||
|
import { CodeModule } from './code.module';
|
||||||
import { CopierService } from 'app/shared//copier.service';
|
import { CopierService } from 'app/shared//copier.service';
|
||||||
import { Logger } from 'app/shared/logger.service';
|
import { Logger } from 'app/shared/logger.service';
|
||||||
import { PrettyPrinter } from './pretty-printer.service';
|
import { PrettyPrinter } from './pretty-printer.service';
|
||||||
@ -22,12 +23,9 @@ const smallMultiLineCode = `
|
|||||||
const bigMultiLineCode = smallMultiLineCode + smallMultiLineCode + smallMultiLineCode;
|
const bigMultiLineCode = smallMultiLineCode + smallMultiLineCode + smallMultiLineCode;
|
||||||
|
|
||||||
describe('CodeComponent', () => {
|
describe('CodeComponent', () => {
|
||||||
let codeComponentDe: DebugElement;
|
|
||||||
let codeComponent: CodeComponent;
|
|
||||||
let hostComponent: HostComponent;
|
let hostComponent: HostComponent;
|
||||||
let fixture: ComponentFixture<HostComponent>;
|
let fixture: ComponentFixture<HostComponent>;
|
||||||
|
|
||||||
|
|
||||||
// WARNING: Chance of cross-test pollution
|
// WARNING: Chance of cross-test pollution
|
||||||
// CodeComponent injects PrettyPrintService
|
// CodeComponent injects PrettyPrintService
|
||||||
// Once PrettyPrintService runs once _anywhere_, its ctor loads `prettify.js`
|
// Once PrettyPrintService runs once _anywhere_, its ctor loads `prettify.js`
|
||||||
@ -42,14 +40,14 @@ describe('CodeComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [ MatSnackBarModule, NoopAnimationsModule ],
|
imports: [ NoopAnimationsModule, CodeModule ],
|
||||||
declarations: [ CodeComponent, HostComponent ],
|
declarations: [ HostComponent ],
|
||||||
providers: [
|
providers: [
|
||||||
PrettyPrinter,
|
PrettyPrinter,
|
||||||
CopierService,
|
CopierService,
|
||||||
{provide: Logger, useClass: TestLogger }
|
{provide: Logger, useClass: TestLogger }
|
||||||
]
|
]
|
||||||
});
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Must be async because
|
// Must be async because
|
||||||
@ -58,26 +56,20 @@ describe('CodeComponent', () => {
|
|||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
fixture = TestBed.createComponent(HostComponent);
|
fixture = TestBed.createComponent(HostComponent);
|
||||||
hostComponent = fixture.componentInstance;
|
hostComponent = fixture.componentInstance;
|
||||||
codeComponentDe = fixture.debugElement.children[0];
|
|
||||||
codeComponent = codeComponentDe.componentInstance;
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should create CodeComponent', () => {
|
|
||||||
expect(codeComponentDe.name).toBe('aio-code', 'selector');
|
|
||||||
expect(codeComponent).toBeTruthy('CodeComponent');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('pretty printing', () => {
|
describe('pretty printing', () => {
|
||||||
it('should format a one-line code sample', () => {
|
it('should format a one-line code sample', () => {
|
||||||
// 'pln' spans are a tell-tale for syntax highlighing
|
// 'pln' spans are a tell-tale for syntax highlighing
|
||||||
const spans = codeComponentDe.nativeElement.querySelectorAll('span.pln');
|
const spans = fixture.nativeElement.querySelectorAll('span.pln');
|
||||||
expect(spans.length).toBeGreaterThan(0, 'formatted spans');
|
expect(spans.length).toBeGreaterThan(0, 'formatted spans');
|
||||||
});
|
});
|
||||||
|
|
||||||
function hasLineNumbers() {
|
function hasLineNumbers() {
|
||||||
// presence of `<li>`s are a tell-tale for line numbers
|
// presence of `<li>`s are a tell-tale for line numbers
|
||||||
return 0 < codeComponentDe.nativeElement.querySelectorAll('li').length;
|
return 0 < fixture.nativeElement.querySelectorAll('li').length;
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should format a one-line code sample without linenums by default', () => {
|
it('should format a one-line code sample without linenums by default', () => {
|
||||||
@ -87,25 +79,25 @@ describe('CodeComponent', () => {
|
|||||||
it('should add line numbers to one-line code sample when linenums set true', () => {
|
it('should add line numbers to one-line code sample when linenums set true', () => {
|
||||||
hostComponent.linenums = 'true';
|
hostComponent.linenums = 'true';
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(hasLineNumbers()).toBe(true);
|
expect(hasLineNumbers()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format a small multi-line code without linenums by default', () => {
|
it('should format a small multi-line code without linenums by default', () => {
|
||||||
hostComponent.code = smallMultiLineCode;
|
hostComponent.setCode(smallMultiLineCode);
|
||||||
fixture.detectChanges();
|
|
||||||
expect(hasLineNumbers()).toBe(false);
|
expect(hasLineNumbers()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add line numbers to a big multi-line code by default', () => {
|
it('should add line numbers to a big multi-line code by default', () => {
|
||||||
hostComponent.code = bigMultiLineCode;
|
hostComponent.setCode(bigMultiLineCode);
|
||||||
fixture.detectChanges();
|
|
||||||
expect(hasLineNumbers()).toBe(true);
|
expect(hasLineNumbers()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format big multi-line code without linenums when linenums set false', () => {
|
it('should format big multi-line code without linenums when linenums set false', () => {
|
||||||
hostComponent.linenums = false;
|
hostComponent.linenums = false;
|
||||||
hostComponent.code = bigMultiLineCode;
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
hostComponent.setCode(bigMultiLineCode);
|
||||||
expect(hasLineNumbers()).toBe(false);
|
expect(hasLineNumbers()).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -113,25 +105,27 @@ describe('CodeComponent', () => {
|
|||||||
describe('whitespace handling', () => {
|
describe('whitespace handling', () => {
|
||||||
it('should remove common indentation from the code before rendering', () => {
|
it('should remove common indentation from the code before rendering', () => {
|
||||||
hostComponent.linenums = false;
|
hostComponent.linenums = false;
|
||||||
hostComponent.code = ' abc\n let x = text.split(\'\\n\');\n ghi\n\n jkl\n';
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const codeContent = codeComponentDe.nativeElement.querySelector('code').textContent;
|
|
||||||
|
hostComponent.setCode(' abc\n let x = text.split(\'\\n\');\n ghi\n\n jkl\n');
|
||||||
|
const codeContent = fixture.nativeElement.querySelector('code').textContent;
|
||||||
expect(codeContent).toEqual('abc\n let x = text.split(\'\\n\');\nghi\n\njkl');
|
expect(codeContent).toEqual('abc\n let x = text.split(\'\\n\');\nghi\n\njkl');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should trim whitespace from the code before rendering', () => {
|
it('should trim whitespace from the code before rendering', () => {
|
||||||
hostComponent.linenums = false;
|
hostComponent.linenums = false;
|
||||||
hostComponent.code = '\n\n\n' + smallMultiLineCode + '\n\n\n';
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const codeContent = codeComponentDe.nativeElement.querySelector('code').textContent;
|
|
||||||
|
hostComponent.setCode('\n\n\n' + smallMultiLineCode + '\n\n\n');
|
||||||
|
const codeContent = fixture.nativeElement.querySelector('code').textContent;
|
||||||
expect(codeContent).toEqual(codeContent.trim());
|
expect(codeContent).toEqual(codeContent.trim());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should trim whitespace from code before computing whether to format linenums', () => {
|
it('should trim whitespace from code before computing whether to format linenums', () => {
|
||||||
hostComponent.code = '\n\n\n' + hostComponent.code + '\n\n\n';
|
hostComponent.setCode('\n\n\n' + oneLineCode + '\n\n\n');
|
||||||
fixture.detectChanges();
|
|
||||||
// `<li>`s are a tell-tale for line numbers
|
// `<li>`s are a tell-tale for line numbers
|
||||||
const lis = codeComponentDe.nativeElement.querySelectorAll('li');
|
const lis = fixture.nativeElement.querySelectorAll('li');
|
||||||
expect(lis.length).toBe(0, 'should be no linenums');
|
expect(lis.length).toBe(0, 'should be no linenums');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -139,39 +133,38 @@ describe('CodeComponent', () => {
|
|||||||
describe('error message', () => {
|
describe('error message', () => {
|
||||||
|
|
||||||
function getErrorMessage() {
|
function getErrorMessage() {
|
||||||
const missing: HTMLElement = codeComponentDe.nativeElement.querySelector('.code-missing');
|
const missing: HTMLElement = fixture.nativeElement.querySelector('.code-missing');
|
||||||
return missing ? missing.textContent : null;
|
return missing ? missing.textContent : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should not display "code-missing" class when there is some code', () => {
|
it('should not display "code-missing" class when there is some code', () => {
|
||||||
fixture.detectChanges();
|
|
||||||
expect(getErrorMessage()).toBeNull('should not have element with "code-missing" class');
|
expect(getErrorMessage()).toBeNull('should not have element with "code-missing" class');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display error message when there is no code (after trimming)', () => {
|
it('should display error message when there is no code (after trimming)', () => {
|
||||||
hostComponent.code = ' \n ';
|
hostComponent.setCode(' \n ');
|
||||||
fixture.detectChanges();
|
|
||||||
expect(getErrorMessage()).toContain('missing');
|
expect(getErrorMessage()).toContain('missing');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show path and region in missing-code error message', () => {
|
it('should show path and region in missing-code error message', () => {
|
||||||
hostComponent.code = ' \n ';
|
|
||||||
hostComponent.path = 'fizz/buzz/foo.html';
|
hostComponent.path = 'fizz/buzz/foo.html';
|
||||||
hostComponent.region = 'something';
|
hostComponent.region = 'something';
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
hostComponent.setCode(' \n ');
|
||||||
expect(getErrorMessage()).toMatch(/for[\s\S]fizz\/buzz\/foo\.html#something$/);
|
expect(getErrorMessage()).toMatch(/for[\s\S]fizz\/buzz\/foo\.html#something$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show path only in missing-code error message when no region', () => {
|
it('should show path only in missing-code error message when no region', () => {
|
||||||
hostComponent.code = ' \n ';
|
|
||||||
hostComponent.path = 'fizz/buzz/foo.html';
|
hostComponent.path = 'fizz/buzz/foo.html';
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
hostComponent.setCode(' \n ');
|
||||||
expect(getErrorMessage()).toMatch(/for[\s\S]fizz\/buzz\/foo\.html$/);
|
expect(getErrorMessage()).toMatch(/for[\s\S]fizz\/buzz\/foo\.html$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show simple missing-code error message when no path/region', () => {
|
it('should show simple missing-code error message when no path/region', () => {
|
||||||
hostComponent.code = ' \n ';
|
hostComponent.setCode(' \n ');
|
||||||
fixture.detectChanges();
|
|
||||||
expect(getErrorMessage()).toMatch(/missing.$/);
|
expect(getErrorMessage()).toMatch(/missing.$/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -190,12 +183,10 @@ describe('CodeComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have title', () => {
|
it('should have title', () => {
|
||||||
fixture.detectChanges();
|
|
||||||
expect(getButton().title).toBe('Copy code snippet');
|
expect(getButton().title).toBe('Copy code snippet');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have no aria-label by default', () => {
|
it('should have no aria-label by default', () => {
|
||||||
fixture.detectChanges();
|
|
||||||
expect(getButton().getAttribute('aria-label')).toBe('');
|
expect(getButton().getAttribute('aria-label')).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -226,12 +217,11 @@ describe('CodeComponent', () => {
|
|||||||
const expectedCode = smallMultiLineCode.trim().replace(/</g, '<').replace(/>/g, '>');
|
const expectedCode = smallMultiLineCode.trim().replace(/</g, '<').replace(/>/g, '>');
|
||||||
let actualCode;
|
let actualCode;
|
||||||
|
|
||||||
hostComponent.code = smallMultiLineCode;
|
hostComponent.setCode(smallMultiLineCode);
|
||||||
|
|
||||||
[false, true, 42].forEach(linenums => {
|
[false, true, 42].forEach(linenums => {
|
||||||
hostComponent.linenums = linenums;
|
hostComponent.linenums = linenums;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
codeComponent.ngOnChanges();
|
|
||||||
getButton().click();
|
getButton().click();
|
||||||
actualCode = spy.calls.mostRecent().args[0];
|
actualCode = spy.calls.mostRecent().args[0];
|
||||||
|
|
||||||
@ -271,19 +261,29 @@ describe('CodeComponent', () => {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'aio-host-comp',
|
selector: 'aio-host-comp',
|
||||||
template: `
|
template: `
|
||||||
<aio-code [code]="code" [language]="language"
|
<aio-code [language]="language"
|
||||||
[linenums]="linenums" [path]="path" [region]="region"
|
[linenums]="linenums" [path]="path" [region]="region"
|
||||||
[hideCopy]="hideCopy" [title]="title"></aio-code>
|
[hideCopy]="hideCopy" [title]="title"></aio-code>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
class HostComponent {
|
class HostComponent implements AfterViewInit {
|
||||||
code = oneLineCode;
|
|
||||||
hideCopy: boolean;
|
hideCopy: boolean;
|
||||||
language: string;
|
language: string;
|
||||||
linenums: boolean | number | string;
|
linenums: boolean | number | string;
|
||||||
path: string;
|
path: string;
|
||||||
region: string;
|
region: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
@ViewChild(CodeComponent) codeComponent: CodeComponent;
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.setCode(oneLineCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Changes the displayed code on the code component. */
|
||||||
|
setCode(code: string) {
|
||||||
|
this.codeComponent.code = code;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestLogger {
|
class TestLogger {
|
@ -1,10 +1,14 @@
|
|||||||
import { Component, ElementRef, ViewChild, OnChanges, Input } from '@angular/core';
|
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
|
||||||
import { Logger } from 'app/shared/logger.service';
|
import { Logger } from 'app/shared/logger.service';
|
||||||
import { PrettyPrinter } from './pretty-printer.service';
|
import { PrettyPrinter } from './pretty-printer.service';
|
||||||
import { CopierService } from 'app/shared/copier.service';
|
import { CopierService } from 'app/shared/copier.service';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
|
||||||
const defaultLineNumsCount = 10; // by default, show linenums over this number
|
/**
|
||||||
|
* If linenums is not set, this is the default maximum number of lines that
|
||||||
|
* an example can display without line numbers.
|
||||||
|
*/
|
||||||
|
const DEFAULT_LINE_NUMS_COUNT = 10;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formatted Code Block
|
* Formatted Code Block
|
||||||
@ -17,13 +21,15 @@ const defaultLineNumsCount = 10; // by default, show linenums over this number
|
|||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
* <aio-code
|
* <aio-code
|
||||||
* [code]="variableContainingCode"
|
|
||||||
* [language]="ts"
|
* [language]="ts"
|
||||||
* [linenums]="true"
|
* [linenums]="true"
|
||||||
* [path]="router/src/app/app.module.ts"
|
* [path]="router/src/app/app.module.ts"
|
||||||
* [region]="animations-module">
|
* [region]="animations-module">
|
||||||
* </aio-code>
|
* </aio-code>
|
||||||
* ```
|
* ```
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Renders code provided through the `updateCode` method.
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'aio-code',
|
selector: 'aio-code',
|
||||||
@ -40,63 +46,54 @@ const defaultLineNumsCount = 10; // by default, show linenums over this number
|
|||||||
`
|
`
|
||||||
})
|
})
|
||||||
export class CodeComponent implements OnChanges {
|
export class CodeComponent implements OnChanges {
|
||||||
|
|
||||||
ariaLabel = '';
|
ariaLabel = '';
|
||||||
|
|
||||||
/**
|
/** The code to be copied when clicking the copy button, this should not be HTML encoded */
|
||||||
* The code to be formatted, this should already be HTML encoded
|
|
||||||
*/
|
|
||||||
@Input()
|
|
||||||
code: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The code to be copied when clicking the copy button, this should not be HTML encoded
|
|
||||||
*/
|
|
||||||
private codeText: string;
|
private codeText: string;
|
||||||
|
|
||||||
/**
|
/** Code that should be formatted with current inputs and displayed in the view. */
|
||||||
* set to true if the copy button is not to be shown
|
set code(code: string) {
|
||||||
*/
|
this._code = code;
|
||||||
@Input()
|
|
||||||
hideCopy: boolean;
|
|
||||||
|
|
||||||
/**
|
if (!this._code || !this._code.trim()) {
|
||||||
* The language of the code to render
|
this.showMissingCodeMessage();
|
||||||
* (could be javascript, dart, typescript, etc)
|
} else {
|
||||||
*/
|
this.formatDisplayedCode();
|
||||||
@Input()
|
}
|
||||||
language: string;
|
}
|
||||||
|
get code(): string { return this._code; }
|
||||||
|
_code: string;
|
||||||
|
|
||||||
|
/** Whether the copy button should be shown. */
|
||||||
|
@Input() hideCopy: boolean;
|
||||||
|
|
||||||
|
/** Language to render the code (e.g. javascript, dart, typescript). */
|
||||||
|
@Input() language: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to display line numbers:
|
* Whether to display line numbers:
|
||||||
* - false: don't display
|
* - If false: hide
|
||||||
* - true: do display
|
* - If true: show
|
||||||
* - number: do display but start at the given number
|
* - If number: show but start at that number
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input() linenums: boolean | number | string;
|
||||||
linenums: boolean | number | string;
|
|
||||||
|
|
||||||
/**
|
/** Path to the source of the code. */
|
||||||
* path to the source of the code being displayed
|
@Input() path: string;
|
||||||
*/
|
|
||||||
@Input()
|
|
||||||
path: string;
|
|
||||||
|
|
||||||
/**
|
/** Region of the source of the code being displayed. */
|
||||||
* region of the source of the code being displayed
|
@Input() region: string;
|
||||||
*/
|
|
||||||
@Input()
|
|
||||||
region: string;
|
|
||||||
|
|
||||||
/**
|
/** Optional title to be displayed above the code. */
|
||||||
* title for this snippet (optional)
|
|
||||||
*/
|
|
||||||
@Input()
|
@Input()
|
||||||
title: string;
|
set title(title: string) {
|
||||||
|
this._title = title;
|
||||||
|
this.ariaLabel = this.title ? `Copy code snippet from ${this.title}` : '';
|
||||||
|
}
|
||||||
|
get title(): string { return this._title; }
|
||||||
|
private _title: string;
|
||||||
|
|
||||||
/**
|
/** The element in the template that will display the formatted code. */
|
||||||
* The element in the template that will display the formatted code
|
|
||||||
*/
|
|
||||||
@ViewChild('codeContainer') codeContainer: ElementRef;
|
@ViewChild('codeContainer') codeContainer: ElementRef;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -106,32 +103,38 @@ export class CodeComponent implements OnChanges {
|
|||||||
private logger: Logger) {}
|
private logger: Logger) {}
|
||||||
|
|
||||||
ngOnChanges() {
|
ngOnChanges() {
|
||||||
this.code = this.code && leftAlign(this.code);
|
// If some inputs have changed and there is code displayed, update the view with the latest
|
||||||
this.ariaLabel = this.title ? `Copy code snippet from ${this.title}` : '';
|
// formatted code.
|
||||||
|
if (this.code) {
|
||||||
if (!this.code) {
|
this.formatDisplayedCode();
|
||||||
const src = this.path ? this.path + (this.region ? '#' + this.region : '') : '';
|
|
||||||
const srcMsg = src ? ` for\n${src}` : '.';
|
|
||||||
this.setCodeHtml(`<p class="code-missing">The code sample is missing${srcMsg}</p>`);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const linenums = this.getLinenums();
|
private formatDisplayedCode() {
|
||||||
|
const leftAlignedCode = leftAlign(this.code);
|
||||||
this.setCodeHtml(this.code); // start with unformatted code
|
this.setCodeHtml(leftAlignedCode); // start with unformatted code
|
||||||
this.codeText = this.getCodeText(); // store the unformatted code as text (for copying)
|
this.codeText = this.getCodeText(); // store the unformatted code as text (for copying)
|
||||||
this.pretty.formatCode(this.code, this.language, linenums).subscribe(
|
|
||||||
formattedCode => this.setCodeHtml(formattedCode),
|
this.pretty.formatCode(leftAlignedCode, this.language, this.getLinenums(leftAlignedCode))
|
||||||
err => { /* ignore failure to format */ }
|
.subscribe(c => this.setCodeHtml(c), err => { /* ignore failure to format */ }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sets the message showing that the code could not be found. */
|
||||||
|
private showMissingCodeMessage() {
|
||||||
|
const src = this.path ? this.path + (this.region ? '#' + this.region : '') : '';
|
||||||
|
const srcMsg = src ? ` for\n${src}` : '.';
|
||||||
|
this.setCodeHtml(`<p class="code-missing">The code sample is missing${srcMsg}</p>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the innerHTML of the code container to the provided code string. */
|
||||||
private setCodeHtml(formattedCode: string) {
|
private setCodeHtml(formattedCode: string) {
|
||||||
// **Security:** `codeExampleContent` is provided by docs authors and as such its considered to
|
// **Security:** Code example content is provided by docs authors and as such its considered to
|
||||||
// be safe for innerHTML purposes.
|
// be safe for innerHTML purposes.
|
||||||
this.codeContainer.nativeElement.innerHTML = formattedCode;
|
this.codeContainer.nativeElement.innerHTML = formattedCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets the textContent of the displayed code element. */
|
||||||
private getCodeText() {
|
private getCodeText() {
|
||||||
// `prettify` may remove newlines, e.g. when `linenums` are on. Retrieve the content of the
|
// `prettify` may remove newlines, e.g. when `linenums` are on. Retrieve the content of the
|
||||||
// container as text, before prettifying it.
|
// container as text, before prettifying it.
|
||||||
@ -139,24 +142,22 @@ export class CodeComponent implements OnChanges {
|
|||||||
return this.codeContainer.nativeElement.textContent;
|
return this.codeContainer.nativeElement.textContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Copies the code snippet to the user's clipboard. */
|
||||||
doCopy() {
|
doCopy() {
|
||||||
const code = this.codeText;
|
const code = this.codeText;
|
||||||
if (this.copier.copyText(code)) {
|
const successfullyCopied = this.copier.copyText(code);
|
||||||
|
|
||||||
|
if (successfullyCopied) {
|
||||||
this.logger.log('Copied code to clipboard:', code);
|
this.logger.log('Copied code to clipboard:', code);
|
||||||
// success snackbar alert
|
this.snackbar.open('Code Copied', '', { duration: 800 });
|
||||||
this.snackbar.open('Code Copied', '', {
|
|
||||||
duration: 800,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.logger.error(new Error(`ERROR copying code to clipboard: "${code}"`));
|
this.logger.error(new Error(`ERROR copying code to clipboard: "${code}"`));
|
||||||
// failure snackbar alert
|
this.snackbar.open('Copy failed. Please try again!', '', { duration: 800 });
|
||||||
this.snackbar.open('Copy failed. Please try again!', '', {
|
|
||||||
duration: 800,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getLinenums() {
|
/** Gets the calculated value of linenums (boolean/number). */
|
||||||
|
getLinenums(code: string) {
|
||||||
const linenums =
|
const linenums =
|
||||||
typeof this.linenums === 'boolean' ? this.linenums :
|
typeof this.linenums === 'boolean' ? this.linenums :
|
||||||
this.linenums === 'true' ? true :
|
this.linenums === 'true' ? true :
|
||||||
@ -165,13 +166,14 @@ export class CodeComponent implements OnChanges {
|
|||||||
this.linenums;
|
this.linenums;
|
||||||
|
|
||||||
// if no linenums, enable line numbers if more than one line
|
// if no linenums, enable line numbers if more than one line
|
||||||
return linenums == null || linenums === NaN ?
|
return linenums == null || isNaN(linenums as number) ?
|
||||||
(this.code.match(/\n/g) || []).length > defaultLineNumsCount : linenums;
|
(code.match(/\n/g) || []).length > DEFAULT_LINE_NUMS_COUNT : linenums;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function leftAlign(text: string) {
|
function leftAlign(text: string): string {
|
||||||
let indent = Number.MAX_VALUE;
|
let indent = Number.MAX_VALUE;
|
||||||
|
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
lines.forEach(line => {
|
lines.forEach(line => {
|
||||||
const lineIndent = line.search(/\S/);
|
const lineIndent = line.search(/\S/);
|
||||||
@ -179,5 +181,6 @@ function leftAlign(text: string) {
|
|||||||
indent = Math.min(lineIndent, indent);
|
indent = Math.min(lineIndent, indent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return lines.map(line => line.substr(indent)).join('\n').trim();
|
return lines.map(line => line.substr(indent)).join('\n').trim();
|
||||||
}
|
}
|
15
aio/src/app/custom-elements/code/code.module.ts
Normal file
15
aio/src/app/custom-elements/code/code.module.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CodeComponent } from './code.component';
|
||||||
|
import { MatSnackBarModule } from '@angular/material';
|
||||||
|
import { PrettyPrinter } from './pretty-printer.service';
|
||||||
|
import { CopierService } from 'app/shared/copier.service';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ CommonModule, MatSnackBarModule ],
|
||||||
|
declarations: [ CodeComponent ],
|
||||||
|
entryComponents: [ CodeComponent ],
|
||||||
|
exports: [ CodeComponent ],
|
||||||
|
providers: [ PrettyPrinter, CopierService ]
|
||||||
|
})
|
||||||
|
export class CodeModule { }
|
@ -33,8 +33,8 @@ export class PrettyPrinter {
|
|||||||
.then(
|
.then(
|
||||||
() => (window as any)['prettyPrintOne'],
|
() => (window as any)['prettyPrintOne'],
|
||||||
err => {
|
err => {
|
||||||
const msg = `Cannot get prettify.js from server: ${err.message}`;
|
const msg = 'Cannot get prettify.js from server';
|
||||||
this.logger.error(new Error(msg));
|
this.logger.error(msg, err);
|
||||||
// return a pretty print fn that always fails.
|
// return a pretty print fn that always fails.
|
||||||
return () => { throw new Error(msg); };
|
return () => { throw new Error(msg); };
|
||||||
});
|
});
|
@ -0,0 +1,16 @@
|
|||||||
|
import { NgModule, Type } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ContributorListComponent } from './contributor-list.component';
|
||||||
|
import { ContributorService } from './contributor.service';
|
||||||
|
import { ContributorComponent } from './contributor.component';
|
||||||
|
import { WithCustomElementComponent } from '../element-registry';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ CommonModule ],
|
||||||
|
declarations: [ ContributorListComponent, ContributorComponent ],
|
||||||
|
entryComponents: [ ContributorListComponent ],
|
||||||
|
providers: [ ContributorService ]
|
||||||
|
})
|
||||||
|
export class ContributorListModule implements WithCustomElementComponent {
|
||||||
|
customElementComponent: Type<any> = ContributorListComponent;
|
||||||
|
}
|
@ -6,6 +6,8 @@ import 'rxjs/add/operator/map';
|
|||||||
import 'rxjs/add/operator/publishLast';
|
import 'rxjs/add/operator/publishLast';
|
||||||
|
|
||||||
import { Contributor, ContributorGroup } from './contributors.model';
|
import { Contributor, ContributorGroup } from './contributors.model';
|
||||||
|
|
||||||
|
// TODO(andrewjs): Look into changing this so that we don't import the service just to get the const
|
||||||
import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
|
import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
|
||||||
|
|
||||||
const contributorsPath = CONTENT_URL_PREFIX + 'contributors.json';
|
const contributorsPath = CONTENT_URL_PREFIX + 'contributors.json';
|
@ -2,14 +2,11 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { LocationService } from 'app/shared/location.service';
|
import { LocationService } from 'app/shared/location.service';
|
||||||
|
|
||||||
/**
|
/** Renders the current location path. */
|
||||||
* A simple embedded component that displays the current location path
|
|
||||||
*/
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'current-location',
|
selector: 'current-location',
|
||||||
template: '{{ location.currentPath | async }}'
|
template: '{{ location.currentPath | async }}'
|
||||||
})
|
})
|
||||||
export class CurrentLocationComponent {
|
export class CurrentLocationComponent {
|
||||||
constructor(public location: LocationService) {
|
constructor(public location: LocationService) { }
|
||||||
}
|
|
||||||
}
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import { NgModule, Type } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CurrentLocationComponent } from './current-location.component';
|
||||||
|
import { WithCustomElementComponent } from '../element-registry';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ CommonModule ],
|
||||||
|
declarations: [ CurrentLocationComponent ],
|
||||||
|
entryComponents: [ CurrentLocationComponent ]
|
||||||
|
})
|
||||||
|
export class CurrentLocationModule implements WithCustomElementComponent {
|
||||||
|
customElementComponent: Type<any> = CurrentLocationComponent;
|
||||||
|
}
|
22
aio/src/app/custom-elements/custom-elements.module.ts
Normal file
22
aio/src/app/custom-elements/custom-elements.module.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { NgModule, NgModuleFactoryLoader, SystemJsNgModuleLoader } from '@angular/core';
|
||||||
|
import { ROUTES} from '@angular/router';
|
||||||
|
import { ElementsLoader } from './elements-loader';
|
||||||
|
import {
|
||||||
|
ELEMENT_MODULE_PATHS,
|
||||||
|
ELEMENT_MODULE_PATHS_AS_ROUTES,
|
||||||
|
ELEMENT_MODULE_PATHS_TOKEN
|
||||||
|
} from './element-registry';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
providers: [
|
||||||
|
ElementsLoader,
|
||||||
|
{ provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader },
|
||||||
|
{ provide: ELEMENT_MODULE_PATHS_TOKEN, useValue: ELEMENT_MODULE_PATHS },
|
||||||
|
|
||||||
|
// Providing these routes as a signal to the build system that these modules should be
|
||||||
|
// registered as lazy-loadable.
|
||||||
|
// TODO(andrewjs): Provide first-class support for providing this.
|
||||||
|
{ provide: ROUTES, useValue: ELEMENT_MODULE_PATHS_AS_ROUTES, multi: true },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CustomElementsModule { }
|
64
aio/src/app/custom-elements/element-registry.ts
Normal file
64
aio/src/app/custom-elements/element-registry.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { InjectionToken, Type } from '@angular/core';
|
||||||
|
|
||||||
|
// Modules containing custom elements must be set up as lazy-loaded routes (loadChildren)
|
||||||
|
// TODO(andrewjs): This is a hack, Angular should have first-class support for preparing a module
|
||||||
|
// that contains custom elements.
|
||||||
|
export const ELEMENT_MODULE_PATHS_AS_ROUTES = [
|
||||||
|
{
|
||||||
|
selector: 'aio-announcement-bar',
|
||||||
|
loadChildren: './announcement-bar/announcement-bar.module#AnnouncementBarModule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'aio-api-list',
|
||||||
|
loadChildren: './api/api-list.module#ApiListModule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'live-example',
|
||||||
|
loadChildren: './live-example/live-example.module#LiveExampleModule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'aio-file-not-found-search',
|
||||||
|
loadChildren: './search/file-not-found-search.module#FileNotFoundSearchModule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'aio-resource-list',
|
||||||
|
loadChildren: './resource/resource-list.module#ResourceListModule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'current-location',
|
||||||
|
loadChildren: './current-location/current-location.module#CurrentLocationModule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'aio-contributor-list',
|
||||||
|
loadChildren: './contributor/contributor-list.module#ContributorListModule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'code-tabs',
|
||||||
|
loadChildren: './code/code-tabs.module#CodeTabsModule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'code-example',
|
||||||
|
loadChildren: './code/code-example.module#CodeExampleModule'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'expandable-section',
|
||||||
|
loadChildren: './expandable-section/expandable-section.module#ExpandableSectionModule'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface expected to be implemented by all modules that declare a component that can be used as
|
||||||
|
* a custom element.
|
||||||
|
*/
|
||||||
|
export interface WithCustomElementComponent {
|
||||||
|
customElementComponent: Type<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Injection token to provide the element path modules. */
|
||||||
|
export const ELEMENT_MODULE_PATHS_TOKEN = new InjectionToken('aio/elements-map');
|
||||||
|
|
||||||
|
/** Map of possible custom element selectors to their lazy-loadable module paths. */
|
||||||
|
export const ELEMENT_MODULE_PATHS = new Map<string, string>();
|
||||||
|
ELEMENT_MODULE_PATHS_AS_ROUTES.forEach(route => {
|
||||||
|
ELEMENT_MODULE_PATHS.set(route.selector, route.loadChildren);
|
||||||
|
});
|
140
aio/src/app/custom-elements/elements-loader.spec.ts
Normal file
140
aio/src/app/custom-elements/elements-loader.spec.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
ComponentFactory,
|
||||||
|
ComponentFactoryResolver, ComponentRef, Injector, NgModuleFactory, NgModuleFactoryLoader,
|
||||||
|
NgModuleRef,
|
||||||
|
Type
|
||||||
|
} from '@angular/core';
|
||||||
|
import {TestBed, fakeAsync, tick} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ElementsLoader } from './elements-loader';
|
||||||
|
import { ELEMENT_MODULE_PATHS_TOKEN, WithCustomElementComponent } from './element-registry';
|
||||||
|
|
||||||
|
const actualCustomElements = window.customElements;
|
||||||
|
|
||||||
|
class FakeComponentFactory extends ComponentFactory<any> {
|
||||||
|
selector: string;
|
||||||
|
componentType: Type<any>;
|
||||||
|
ngContentSelectors: string[];
|
||||||
|
inputs = [{propName: this.identifyingInput, templateName: this.identifyingInput}];
|
||||||
|
outputs = [];
|
||||||
|
|
||||||
|
constructor(private identifyingInput: string) { super(); }
|
||||||
|
|
||||||
|
create(injector: Injector,
|
||||||
|
projectableNodes?: any[][],
|
||||||
|
rootSelectorOrNode?: string | any,
|
||||||
|
ngModule?: NgModuleRef<any>): ComponentRef<string> {
|
||||||
|
return jasmine.createSpyObj('ComponentRef', ['methods']);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const FAKE_COMPONENT_FACTORIES = new Map([
|
||||||
|
['element-a-module-path', new FakeComponentFactory('element-a-input')]
|
||||||
|
]);
|
||||||
|
|
||||||
|
describe('ElementsLoader', () => {
|
||||||
|
let elementsLoader: ElementsLoader;
|
||||||
|
let injectedModuleRef: NgModuleRef<any>;
|
||||||
|
let fakeCustomElements;
|
||||||
|
|
||||||
|
// ElementsLoader uses the window's customElements API. Provide a fake for this test.
|
||||||
|
beforeEach(() => {
|
||||||
|
fakeCustomElements = jasmine.createSpyObj('customElements', ['define']);
|
||||||
|
window.customElements = fakeCustomElements;
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
window.customElements = actualCustomElements;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const injector = TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
ElementsLoader,
|
||||||
|
{ provide: NgModuleFactoryLoader, useClass: FakeModuleFactoryLoader },
|
||||||
|
{ provide: ELEMENT_MODULE_PATHS_TOKEN, useValue: new Map([
|
||||||
|
['element-a-selector', 'element-a-module-path']
|
||||||
|
])},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
injectedModuleRef = injector.get(NgModuleRef);
|
||||||
|
elementsLoader = injector.get(ElementsLoader);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to register an element', 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>`;
|
||||||
|
|
||||||
|
elementsLoader.loadContainingCustomElements(hostEl);
|
||||||
|
tick();
|
||||||
|
|
||||||
|
const defineArgs = fakeCustomElements.define.calls.argsFor(0);
|
||||||
|
expect(defineArgs[0]).toBe('element-a-selector');
|
||||||
|
|
||||||
|
// Verify the right component was loaded/created
|
||||||
|
expect(defineArgs[1].observedAttributes[0]).toBe('element-a-input');
|
||||||
|
|
||||||
|
expect(elementsLoader.elementsToLoad.has('element-a-selector')).toBeFalsy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should only register an element one time', fakeAsync(() => {
|
||||||
|
const hostEl = document.createElement('div');
|
||||||
|
hostEl.innerHTML = `<element-a-selector></element-a-selector>`;
|
||||||
|
|
||||||
|
elementsLoader.loadContainingCustomElements(hostEl);
|
||||||
|
tick(); // Tick for the module factory loader's async `load` function
|
||||||
|
|
||||||
|
// Call again to to check how many times registerAsCustomElements was called.
|
||||||
|
elementsLoader.loadContainingCustomElements(hostEl);
|
||||||
|
tick(); // Tick for the module factory loader's async `load` function
|
||||||
|
|
||||||
|
// Should have only been called once, since the second load would not query for element-a
|
||||||
|
expect(window.customElements.define).toHaveBeenCalledTimes(1);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// TEST CLASSES/HELPERS
|
||||||
|
|
||||||
|
class FakeCustomElementModule implements WithCustomElementComponent {
|
||||||
|
customElementComponent: Type<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeComponentFactoryResolver extends ComponentFactoryResolver {
|
||||||
|
constructor(private modulePath) { super(); }
|
||||||
|
|
||||||
|
resolveComponentFactory(component: Type<any>): ComponentFactory<any> {
|
||||||
|
return FAKE_COMPONENT_FACTORIES.get(this.modulePath)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeModuleRef extends NgModuleRef<WithCustomElementComponent> {
|
||||||
|
injector: Injector;
|
||||||
|
componentFactoryResolver = new FakeComponentFactoryResolver(this.modulePath);
|
||||||
|
instance: WithCustomElementComponent = new FakeCustomElementModule();
|
||||||
|
|
||||||
|
constructor(private modulePath) { super(); }
|
||||||
|
|
||||||
|
destroy() {}
|
||||||
|
onDestroy(callback: () => void) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeModuleFactory extends NgModuleFactory<any> {
|
||||||
|
moduleType: Type<any>;
|
||||||
|
moduleRefToCreate = new FakeModuleRef(this.modulePath);
|
||||||
|
|
||||||
|
constructor(private modulePath) { super(); }
|
||||||
|
|
||||||
|
create(parentInjector: Injector | null): NgModuleRef<any> {
|
||||||
|
return this.moduleRefToCreate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeModuleFactoryLoader extends NgModuleFactoryLoader {
|
||||||
|
load(modulePath: string): Promise<NgModuleFactory<any>> {
|
||||||
|
const fakeModuleFactory = new FakeModuleFactory(modulePath);
|
||||||
|
return Promise.resolve(fakeModuleFactory);
|
||||||
|
}
|
||||||
|
}
|
70
aio/src/app/custom-elements/elements-loader.ts
Normal file
70
aio/src/app/custom-elements/elements-loader.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
ComponentFactory,
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
NgModuleFactoryLoader,
|
||||||
|
NgModuleRef,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ELEMENT_MODULE_PATHS_TOKEN, WithCustomElementComponent } from './element-registry';
|
||||||
|
import { of } from 'rxjs/observable/of';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { fromPromise } from 'rxjs/observable/fromPromise';
|
||||||
|
import { createNgElementConstructor, getConfigFromComponentFactory } from '@angular/elements';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ElementsLoader {
|
||||||
|
/** Map of unregistered custom elements and their respective module paths to load. */
|
||||||
|
elementsToLoad: Map<string, string>;
|
||||||
|
|
||||||
|
constructor(private moduleFactoryLoader: NgModuleFactoryLoader,
|
||||||
|
private moduleRef: NgModuleRef<any>,
|
||||||
|
@Inject(ELEMENT_MODULE_PATHS_TOKEN) elementModulePaths) {
|
||||||
|
this.elementsToLoad = new Map(elementModulePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the provided element for any custom elements that have not yet been registered with
|
||||||
|
* the browser. Custom elements that are registered will be removed from the list of unregistered
|
||||||
|
* elements so that they will not be queried in subsequent calls.
|
||||||
|
*/
|
||||||
|
loadContainingCustomElements(element: HTMLElement): Observable<null> {
|
||||||
|
const selectors: any[] = Array.from(this.elementsToLoad.keys())
|
||||||
|
.filter(s => element.querySelector(s));
|
||||||
|
|
||||||
|
if (!selectors.length) { return of(null); }
|
||||||
|
|
||||||
|
selectors.forEach(s => this.register(s));
|
||||||
|
|
||||||
|
// Returns observable that completes when all discovered elements have been registered.
|
||||||
|
return fromPromise(Promise.all(selectors.map(s => this.register(s))).then(result => null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Registers the custom element defined on the WithCustomElement module factory. */
|
||||||
|
private register(selector: string) {
|
||||||
|
const modulePath = this.elementsToLoad.get(selector)!;
|
||||||
|
return this.moduleFactoryLoader.load(modulePath).then(elementModuleFactory => {
|
||||||
|
if (!this.elementsToLoad.has(selector)) { return; }
|
||||||
|
|
||||||
|
const injector = this.moduleRef.injector;
|
||||||
|
const elementModuleRef = elementModuleFactory.create(injector);
|
||||||
|
const componentFactory = this.getCustomElementComponentFactory(elementModuleRef);
|
||||||
|
|
||||||
|
const ngElementConfig = getConfigFromComponentFactory(componentFactory, injector);
|
||||||
|
const NgElement = createNgElementConstructor(ngElementConfig);
|
||||||
|
|
||||||
|
customElements!.define(selector, NgElement);
|
||||||
|
this.elementsToLoad.delete(selector);
|
||||||
|
|
||||||
|
return customElements.whenDefined(selector);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the component factory of the custom element defined on the NgModuleRef. */
|
||||||
|
private getCustomElementComponentFactory(
|
||||||
|
customElementModuleRef: NgModuleRef<WithCustomElementComponent>): ComponentFactory<string> {
|
||||||
|
const resolver = customElementModuleRef.componentFactoryResolver;
|
||||||
|
const customElementComponent = customElementModuleRef.instance.customElementComponent;
|
||||||
|
|
||||||
|
return resolver.resolveComponentFactory(customElementComponent);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
<mat-expansion-panel style="background: inherit">
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
{{title}}
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</mat-expansion-panel>
|
@ -0,0 +1,11 @@
|
|||||||
|
/* tslint:disable component-selector */
|
||||||
|
import {Component, Input} from '@angular/core';
|
||||||
|
|
||||||
|
/** Custom element wrapper for the material expansion panel with a title input. */
|
||||||
|
@Component({
|
||||||
|
selector: 'expandable-section',
|
||||||
|
templateUrl: 'expandable-section.component.html',
|
||||||
|
})
|
||||||
|
export class ExpandableSectionComponent {
|
||||||
|
@Input() title;
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import { NgModule, Type } from '@angular/core';
|
||||||
|
import { ExpandableSectionComponent } from './expandable-section.component';
|
||||||
|
import { WithCustomElementComponent } from '../element-registry';
|
||||||
|
import { MatExpansionModule } from '@angular/material';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ MatExpansionModule ],
|
||||||
|
declarations: [ ExpandableSectionComponent, ],
|
||||||
|
entryComponents: [ ExpandableSectionComponent ]
|
||||||
|
})
|
||||||
|
export class ExpandableSectionModule implements WithCustomElementComponent {
|
||||||
|
customElementComponent: Type<any> = ExpandableSectionComponent;
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import { NgModule, Type } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { EmbeddedStackblitzComponent, LiveExampleComponent } from './live-example.component';
|
||||||
|
import { WithCustomElementComponent } from '../element-registry';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ CommonModule ],
|
||||||
|
declarations: [ LiveExampleComponent, EmbeddedStackblitzComponent ],
|
||||||
|
entryComponents: [ LiveExampleComponent ]
|
||||||
|
})
|
||||||
|
export class LiveExampleModule implements WithCustomElementComponent {
|
||||||
|
customElementComponent: Type<any> = LiveExampleComponent;
|
||||||
|
}
|
15
aio/src/app/custom-elements/resource/resource-list.module.ts
Normal file
15
aio/src/app/custom-elements/resource/resource-list.module.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { NgModule, Type } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ResourceListComponent } from './resource-list.component';
|
||||||
|
import { ResourceService } from './resource.service';
|
||||||
|
import { WithCustomElementComponent } from '../element-registry';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ CommonModule ],
|
||||||
|
declarations: [ ResourceListComponent ],
|
||||||
|
entryComponents: [ ResourceListComponent ],
|
||||||
|
providers: [ ResourceService ]
|
||||||
|
})
|
||||||
|
export class ResourceListModule implements WithCustomElementComponent {
|
||||||
|
customElementComponent: Type<any> = ResourceListComponent;
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { NgModule, Type } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { FileNotFoundSearchComponent } from './file-not-found-search.component';
|
||||||
|
import { WithCustomElementComponent } from '../element-registry';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ CommonModule, SharedModule ],
|
||||||
|
declarations: [ FileNotFoundSearchComponent ],
|
||||||
|
entryComponents: [ FileNotFoundSearchComponent ]
|
||||||
|
})
|
||||||
|
export class FileNotFoundSearchModule implements WithCustomElementComponent {
|
||||||
|
customElementComponent: Type<any> = FileNotFoundSearchComponent;
|
||||||
|
}
|
@ -1,13 +0,0 @@
|
|||||||
import { NgModule, NgModuleFactoryLoader, SystemJsNgModuleLoader } from '@angular/core';
|
|
||||||
|
|
||||||
import { EmbedComponentsService } from './embed-components.service';
|
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
providers: [
|
|
||||||
EmbedComponentsService,
|
|
||||||
{ provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class EmbedComponentsModule {
|
|
||||||
}
|
|
@ -1,378 +0,0 @@
|
|||||||
import { ComponentFactory, ComponentFactoryResolver, ComponentRef, NgModuleFactoryLoader } from '@angular/core';
|
|
||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import {
|
|
||||||
MockNgModuleFactoryLoader, TestEmbedComponentsService, TestModule, mockEmbeddedModulePath,
|
|
||||||
testEagerEmbeddedComponents, testEagerEmbeddedSelectors, testLazyEmbeddedComponents
|
|
||||||
} from 'testing/embed-components-utils';
|
|
||||||
import { EmbedComponentsService, ComponentsOrModulePath } from './embed-components.service';
|
|
||||||
|
|
||||||
|
|
||||||
describe('EmbedComponentsService', () => {
|
|
||||||
let service: TestEmbedComponentsService;
|
|
||||||
let host: HTMLElement;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({imports: [TestModule]});
|
|
||||||
|
|
||||||
service = TestBed.get(EmbedComponentsService);
|
|
||||||
host = document.createElement('div');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be instantiated', () => {
|
|
||||||
expect(service).toEqual(jasmine.any(EmbedComponentsService));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#createComponentFactories()', () => {
|
|
||||||
let factories: typeof service.componentFactories;
|
|
||||||
let resolver: ComponentFactoryResolver;
|
|
||||||
|
|
||||||
const doCreateComponentFactories = () =>
|
|
||||||
service.createComponentFactories(testEagerEmbeddedComponents, resolver);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
factories = service.componentFactories;
|
|
||||||
resolver = TestBed.get(ComponentFactoryResolver) as ComponentFactoryResolver;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create a factory entry for each component', () => {
|
|
||||||
expect(factories.size).toBe(0);
|
|
||||||
|
|
||||||
doCreateComponentFactories();
|
|
||||||
expect(factories.size).toBe(testEagerEmbeddedComponents.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should key the factory entries by selector', () => {
|
|
||||||
doCreateComponentFactories();
|
|
||||||
|
|
||||||
const actualSelectors = Array.from(factories.keys());
|
|
||||||
const expectedSelectors = testEagerEmbeddedSelectors;
|
|
||||||
|
|
||||||
expect(actualSelectors).toEqual(expectedSelectors);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should store the projected content property name', () => {
|
|
||||||
doCreateComponentFactories();
|
|
||||||
|
|
||||||
const actualContentPropNames = Array.from(factories.values()).map(x => x.contentPropertyName);
|
|
||||||
const expectedContentPropNames = testEagerEmbeddedSelectors.map(x => service.selectorToContentPropertyName(x));
|
|
||||||
|
|
||||||
expect(actualContentPropNames).toEqual(expectedContentPropNames);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should store the factory for each component', () => {
|
|
||||||
doCreateComponentFactories();
|
|
||||||
|
|
||||||
const actualFactories = Array.from(factories.values()).map(x => x.factory);
|
|
||||||
const expectedComponentTypes = testEagerEmbeddedComponents;
|
|
||||||
|
|
||||||
actualFactories.forEach((factory, i) => {
|
|
||||||
expect(factory).toEqual(jasmine.any(ComponentFactory));
|
|
||||||
expect(factory.componentType).toBe(expectedComponentTypes[i]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#createComponents()', () => {
|
|
||||||
const FooComponent = testEagerEmbeddedComponents[0];
|
|
||||||
const BarComponent = testEagerEmbeddedComponents[1];
|
|
||||||
|
|
||||||
beforeEach(() => service.prepareComponentFactories(testEagerEmbeddedComponents));
|
|
||||||
|
|
||||||
it('should apply all embedded components (and return the `ComponentRef`s)', () => {
|
|
||||||
host.innerHTML = `
|
|
||||||
<p>Header</p>
|
|
||||||
<p><aio-eager-foo></aio-eager-foo></p>
|
|
||||||
<p><aio-eager-bar></aio-eager-bar></p>
|
|
||||||
<p>Footer</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const componentRefs = service.createComponents(host);
|
|
||||||
|
|
||||||
expect(host.innerHTML).toContain('Foo Component');
|
|
||||||
expect(host.innerHTML).toContain('Bar Component');
|
|
||||||
|
|
||||||
expect(componentRefs.length).toBe(2);
|
|
||||||
expect(componentRefs[0].instance).toEqual(jasmine.any(FooComponent));
|
|
||||||
expect(componentRefs[1].instance).toEqual(jasmine.any(BarComponent));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should apply embedded components to all matching elements', () => {
|
|
||||||
host.innerHTML = `
|
|
||||||
<p>Header</p>
|
|
||||||
<p><aio-eager-foo></aio-eager-foo></p>
|
|
||||||
<p><aio-eager-bar></aio-eager-bar></p>
|
|
||||||
<p><aio-eager-foo></aio-eager-foo></p>
|
|
||||||
<p><aio-eager-bar></aio-eager-bar></p>
|
|
||||||
<p>Footer</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const componentRefs = service.createComponents(host);
|
|
||||||
|
|
||||||
expect(componentRefs.length).toBe(4);
|
|
||||||
expect(componentRefs[0].instance).toEqual(jasmine.any(FooComponent));
|
|
||||||
expect(componentRefs[1].instance).toEqual(jasmine.any(FooComponent));
|
|
||||||
expect(componentRefs[2].instance).toEqual(jasmine.any(BarComponent));
|
|
||||||
expect(componentRefs[3].instance).toEqual(jasmine.any(BarComponent));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow projecting content by assigning it on the element', () => {
|
|
||||||
const projectedContent = 'Projected content';
|
|
||||||
host.innerHTML = `
|
|
||||||
<p>Header</p>
|
|
||||||
<p><aio-eager-bar>${projectedContent}</aio-eager-bar></p>
|
|
||||||
<p>Footer</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const componentRefs = service.createComponents(host);
|
|
||||||
componentRefs[0].changeDetectorRef.detectChanges();
|
|
||||||
|
|
||||||
const barEl = host.querySelector('aio-eager-bar')!;
|
|
||||||
|
|
||||||
expect((barEl as any)['aioEagerBarContent']).toBe(projectedContent);
|
|
||||||
expect(barEl.innerHTML).toContain(projectedContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Because `FooComponent` is processed before `BarComponent`...
|
|
||||||
it('should apply `FooComponent` within `BarComponent`', () => {
|
|
||||||
host.innerHTML = `
|
|
||||||
<aio-eager-bar>
|
|
||||||
<aio-eager-foo></aio-eager-foo>
|
|
||||||
</aio-eager-bar>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const componentRefs = service.createComponents(host);
|
|
||||||
componentRefs.forEach(ref => ref.changeDetectorRef.detectChanges());
|
|
||||||
|
|
||||||
expect(host.innerHTML).toContain('Foo Component');
|
|
||||||
expect(host.innerHTML).toContain('Bar Component');
|
|
||||||
|
|
||||||
expect(componentRefs.length).toBe(2);
|
|
||||||
expect(componentRefs[0].instance).toEqual(jasmine.any(FooComponent));
|
|
||||||
expect(componentRefs[1].instance).toEqual(jasmine.any(BarComponent));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Because `BarComponent` is processed after `FooComponent`...
|
|
||||||
it('should not apply `BarComponent` within `FooComponent`', () => {
|
|
||||||
host.innerHTML = `
|
|
||||||
<aio-eager-foo>
|
|
||||||
<aio-eager-bar></aio-eager-bar>
|
|
||||||
</aio-eager-foo>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const componentRefs = service.createComponents(host);
|
|
||||||
componentRefs.forEach(ref => ref.changeDetectorRef.detectChanges());
|
|
||||||
|
|
||||||
expect(host.innerHTML).toContain('Foo Component');
|
|
||||||
expect(host.innerHTML).not.toContain('Bar Component');
|
|
||||||
|
|
||||||
expect(componentRefs.length).toBe(1);
|
|
||||||
expect(componentRefs[0].instance).toEqual(jasmine.any(FooComponent));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#embedInto()', () => {
|
|
||||||
let mockComponentRefs: ComponentRef<any>[];
|
|
||||||
let createComponentsSpy: jasmine.Spy;
|
|
||||||
let prepareComponentFactoriesSpy: jasmine.Spy;
|
|
||||||
|
|
||||||
const doEmbed = (contents: string) =>
|
|
||||||
new Promise<ComponentRef<any>[]>((resolve, reject) => {
|
|
||||||
host.innerHTML = contents;
|
|
||||||
service.embedInto(host).subscribe(resolve, reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockComponentRefs = [{foo: true}, {bar: true}] as any as ComponentRef<any>[];
|
|
||||||
|
|
||||||
createComponentsSpy = spyOn(service, 'createComponents').and.returnValue(mockComponentRefs);
|
|
||||||
prepareComponentFactoriesSpy = spyOn(service, 'prepareComponentFactories')
|
|
||||||
.and.returnValue(Promise.resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return an observable', done => {
|
|
||||||
service.embedInto(host).subscribe(done, done.fail);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('(preparing component factories)', () => {
|
|
||||||
it('should return an array of `ComponentRef`s', async () => {
|
|
||||||
// When there are embedded components.
|
|
||||||
expect(await doEmbed('<aio-eager-foo></aio-eager-foo>')).toEqual(mockComponentRefs);
|
|
||||||
expect(await doEmbed('<aio-lazy-bar></aio-lazy-bar>')).toEqual(mockComponentRefs);
|
|
||||||
|
|
||||||
// When there are no embedded components.
|
|
||||||
expect(await doEmbed('<div>Test</div>')).toEqual([]);
|
|
||||||
expect(await doEmbed('')).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should prepare all component factories if there are embedded components', async () => {
|
|
||||||
await doEmbed(`
|
|
||||||
<div><aio-eager-foo><b>foo</b></aio-eager-foo></div>
|
|
||||||
<span><aio-lazy-foo><i>bar</i></aio-lazy-foo></span>
|
|
||||||
`);
|
|
||||||
|
|
||||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledTimes(2);
|
|
||||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledWith(testEagerEmbeddedComponents);
|
|
||||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledWith(mockEmbeddedModulePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should only prepare the necessary factories', async () => {
|
|
||||||
await doEmbed('<aio-eager-foo>Eager only</aio-eager-foo>');
|
|
||||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledWith(testEagerEmbeddedComponents);
|
|
||||||
|
|
||||||
await doEmbed('<aio-lazy-foo>Lazy only</aio-lazy-foo>');
|
|
||||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledTimes(2);
|
|
||||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledWith(mockEmbeddedModulePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not load embedded components if the document does not contain any', async () => {
|
|
||||||
await doEmbed('');
|
|
||||||
await doEmbed('<no-aio-eager-foo></no-aio-eager-foo>');
|
|
||||||
await doEmbed('<no-aio-lazy-foo></no-aio-lazy-foo>');
|
|
||||||
|
|
||||||
expect(prepareComponentFactoriesSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('(creating embedded components)', () => {
|
|
||||||
it('should create embedded components if the element contains any', async () => {
|
|
||||||
await doEmbed('<div><aio-eager-foo><i>blah</i></aio-eager-foo></div>');
|
|
||||||
|
|
||||||
expect(createComponentsSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledBefore(createComponentsSpy);
|
|
||||||
|
|
||||||
prepareComponentFactoriesSpy.calls.reset();
|
|
||||||
createComponentsSpy.calls.reset();
|
|
||||||
|
|
||||||
await doEmbed('<aio-lazy-bar><i>blah</i></aio-lazy-bar>');
|
|
||||||
expect(createComponentsSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(prepareComponentFactoriesSpy).toHaveBeenCalledBefore(createComponentsSpy);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should emit the created embedded components', async () => {
|
|
||||||
const componentRefs = await doEmbed('<aio-eager-foo></aio-eager-foo>');
|
|
||||||
expect(componentRefs).toBe(mockComponentRefs);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not create embedded components if the element does not contain any', async () => {
|
|
||||||
await doEmbed(`
|
|
||||||
<aio-eager-foo-not></aio-eager-foo-not>
|
|
||||||
<aio-lazy-bar></aio-lazy-bar>
|
|
||||||
`);
|
|
||||||
expect(createComponentsSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not create embedded components if the document is empty', async () => {
|
|
||||||
await doEmbed('');
|
|
||||||
expect(createComponentsSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not create embedded components if unsubscribed from', async () => {
|
|
||||||
const preparePromise = Promise.resolve();
|
|
||||||
prepareComponentFactoriesSpy.and.returnValue(preparePromise);
|
|
||||||
|
|
||||||
// When not unsubscribed from...
|
|
||||||
host.innerHTML = '<aio-eager-foo></aio-eager-foo>';
|
|
||||||
service.embedInto(host).subscribe();
|
|
||||||
await new Promise(resolve => setTimeout(resolve));
|
|
||||||
expect(createComponentsSpy).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
createComponentsSpy.calls.reset();
|
|
||||||
|
|
||||||
// When unsubscribed from...
|
|
||||||
host.innerHTML = '<aio-eager-foo></aio-eager-foo>';
|
|
||||||
service.embedInto(host).subscribe().unsubscribe();
|
|
||||||
await new Promise(resolve => setTimeout(resolve));
|
|
||||||
expect(createComponentsSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#prepareComponentFactories()', () => {
|
|
||||||
let loader: MockNgModuleFactoryLoader;
|
|
||||||
let resolver: ComponentFactoryResolver;
|
|
||||||
let createComponentFactoriesSpy: jasmine.Spy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
loader = TestBed.get(NgModuleFactoryLoader);
|
|
||||||
resolver = TestBed.get(ComponentFactoryResolver);
|
|
||||||
|
|
||||||
createComponentFactoriesSpy = spyOn(service, 'createComponentFactories');
|
|
||||||
});
|
|
||||||
|
|
||||||
[testLazyEmbeddedComponents, mockEmbeddedModulePath].forEach((compsOrPath: ComponentsOrModulePath) => {
|
|
||||||
const useComponents = Array.isArray(compsOrPath);
|
|
||||||
|
|
||||||
describe(`(using ${useComponents ? 'component types' : 'module path'})`, () => {
|
|
||||||
const doPrepareComponentFactories = () =>
|
|
||||||
service.prepareComponentFactories(compsOrPath);
|
|
||||||
|
|
||||||
it('should return a promise', done => {
|
|
||||||
doPrepareComponentFactories().then(done, done.fail);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create the component factories', async () => {
|
|
||||||
expect(createComponentFactoriesSpy).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
await doPrepareComponentFactories();
|
|
||||||
expect(createComponentFactoriesSpy).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
const args = createComponentFactoriesSpy.calls.mostRecent().args;
|
|
||||||
expect(args[0]).toBe(testLazyEmbeddedComponents);
|
|
||||||
|
|
||||||
if (useComponents) {
|
|
||||||
expect(args[1]).toBe(resolver);
|
|
||||||
} else {
|
|
||||||
expect(args[1]).not.toBe(resolver);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not create create the component factories more than once', async () => {
|
|
||||||
const results = await Promise.all([
|
|
||||||
doPrepareComponentFactories(),
|
|
||||||
doPrepareComponentFactories(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(createComponentFactoriesSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(results[1]).toBe(results[0]);
|
|
||||||
|
|
||||||
const anotherResult = await doPrepareComponentFactories();
|
|
||||||
|
|
||||||
expect(createComponentFactoriesSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(anotherResult).toBe(results[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should ${useComponents ? 'not load' : 'load'} the embedded module`, async () => {
|
|
||||||
expect(loader.loadedPaths).toEqual([]);
|
|
||||||
|
|
||||||
await doPrepareComponentFactories();
|
|
||||||
const expectedLoadedPaths = useComponents ? [] : [mockEmbeddedModulePath];
|
|
||||||
|
|
||||||
expect(loader.loadedPaths).toEqual(expectedLoadedPaths);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should not load the embedded module more than once`, async () => {
|
|
||||||
await Promise.all([
|
|
||||||
doPrepareComponentFactories(),
|
|
||||||
doPrepareComponentFactories(),
|
|
||||||
]);
|
|
||||||
const loadedPathCount = loader.loadedPaths.length;
|
|
||||||
|
|
||||||
expect(loadedPathCount).toBeLessThan(2);
|
|
||||||
|
|
||||||
await doPrepareComponentFactories();
|
|
||||||
|
|
||||||
expect(loader.loadedPaths.length).toBe(loadedPathCount);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#selectorToContentPropertyName()', () => {
|
|
||||||
it('should convert an element selector to a property name', () => {
|
|
||||||
expect(service.selectorToContentPropertyName('foobar')).toBe('foobarContent');
|
|
||||||
expect(service.selectorToContentPropertyName('baz-qux')).toBe('bazQuxContent');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,154 +0,0 @@
|
|||||||
import {
|
|
||||||
ComponentFactory, ComponentFactoryResolver, ComponentRef, Inject, Injectable, InjectionToken,
|
|
||||||
Injector, NgModuleFactory, NgModuleFactoryLoader, Type
|
|
||||||
} from '@angular/core';
|
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
import { of } from 'rxjs/observable/of';
|
|
||||||
import 'rxjs/add/operator/switchMap';
|
|
||||||
|
|
||||||
|
|
||||||
export interface EmbeddedComponentFactory {
|
|
||||||
contentPropertyName: string;
|
|
||||||
factory: ComponentFactory<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A mapping from combined component selectors (keys) to the corresponding components (values). The
|
|
||||||
* components can be specified either as a list of embedded components or a path to a module that
|
|
||||||
* provides embedded components (i.e. implements `WithEmbeddedComponents`).
|
|
||||||
*/
|
|
||||||
export interface EmbeddedComponentsMap {
|
|
||||||
[multiSelector: string]: ComponentsOrModulePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface expected to be implemented by all modules that contribute components to the
|
|
||||||
* `EmbeddedComponentsMap`.
|
|
||||||
*/
|
|
||||||
export interface WithEmbeddedComponents {
|
|
||||||
embeddedComponents: Type<any>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Either an array of components or the path to a module that implements `WithEmbeddedComponents`.
|
|
||||||
*/
|
|
||||||
export type ComponentsOrModulePath = Type<any>[] | string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The injection token for the `EmbeddedComponentsMap`.
|
|
||||||
*/
|
|
||||||
export const EMBEDDED_COMPONENTS = new InjectionToken<EmbeddedComponentsMap>('EMBEDDED_COMPONENTS');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Embed components into an element. It takes care of indentifying the embedded components, loading
|
|
||||||
* the necessary modules and instantiating the components.
|
|
||||||
*
|
|
||||||
* Embeddable components are identified and loaded based on the info in `EmbeddedComponentsMap`
|
|
||||||
* (provided through dependency injection).
|
|
||||||
*
|
|
||||||
* The caller is responsible for trigering change detection and destroying the components as
|
|
||||||
* necessary.
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class EmbedComponentsService {
|
|
||||||
private componentFactoriesReady = new Map<ComponentsOrModulePath, Promise<void>>();
|
|
||||||
protected componentFactories = new Map<string, EmbeddedComponentFactory>();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private injector: Injector,
|
|
||||||
private loader: NgModuleFactoryLoader,
|
|
||||||
private resolver: ComponentFactoryResolver,
|
|
||||||
@Inject(EMBEDDED_COMPONENTS) private embeddedComponentsMap: EmbeddedComponentsMap) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Embed components into the specified element:
|
|
||||||
* - Load the necessary modules (if any).
|
|
||||||
* - Prepare the component factories.
|
|
||||||
* - Instantiate the components.
|
|
||||||
*
|
|
||||||
* Return the list of `ComponentRef`s.
|
|
||||||
*/
|
|
||||||
embedInto(elem: HTMLElement): Observable<ComponentRef<any>[]> {
|
|
||||||
const requiredComponents = Object.keys(this.embeddedComponentsMap)
|
|
||||||
.filter(selector => elem.querySelector(selector))
|
|
||||||
.map(selector => this.embeddedComponentsMap[selector]);
|
|
||||||
|
|
||||||
const factoriesReady = requiredComponents.map(compsOrPath => this.prepareComponentFactories(compsOrPath));
|
|
||||||
|
|
||||||
return !requiredComponents.length
|
|
||||||
? of([])
|
|
||||||
: of(undefined)
|
|
||||||
.switchMap(() => Promise.all(factoriesReady))
|
|
||||||
.switchMap(() => [this.createComponents(elem)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve the embedded component factories (which will later be used to instantiate components).
|
|
||||||
*/
|
|
||||||
protected createComponentFactories(components: Type<any>[], resolver: ComponentFactoryResolver): void {
|
|
||||||
for (const comp of components) {
|
|
||||||
const factory = resolver.resolveComponentFactory(comp);
|
|
||||||
const selector = factory.selector;
|
|
||||||
const contentPropertyName = this.selectorToContentPropertyName(selector);
|
|
||||||
this.componentFactories.set(selector, {contentPropertyName, factory});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate embedded components for the current contents of `elem`.
|
|
||||||
* (Store the original HTML contents of each element on the corresponding property for later
|
|
||||||
* retrieval by the component instance.)
|
|
||||||
*/
|
|
||||||
protected createComponents(elem: HTMLElement): ComponentRef<any>[] {
|
|
||||||
const componentRefs: ComponentRef<any>[] = [];
|
|
||||||
|
|
||||||
this.componentFactories.forEach(({contentPropertyName, factory}, selector) => {
|
|
||||||
const componentHosts = elem.querySelectorAll(selector);
|
|
||||||
|
|
||||||
// Cast due to https://github.com/Microsoft/TypeScript/issues/4947.
|
|
||||||
for (const host of componentHosts as any as HTMLElement[]) {
|
|
||||||
// Hack: Preserve the current element content, because the factory will empty it out.
|
|
||||||
// Security: The source of this `innerHTML` should always be authored by the documentation
|
|
||||||
// team and is considered to be safe.
|
|
||||||
(host as any)[contentPropertyName] = host.innerHTML;
|
|
||||||
componentRefs.push(factory.create(this.injector, [], host));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return componentRefs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepare the component factories for the given components.
|
|
||||||
* If necessary, load and instantiate the module first.
|
|
||||||
*/
|
|
||||||
protected prepareComponentFactories(compsOrPath: ComponentsOrModulePath): Promise<void> {
|
|
||||||
if (!this.componentFactoriesReady.has(compsOrPath)) {
|
|
||||||
const componentsAndResolverPromise = (typeof compsOrPath !== 'string')
|
|
||||||
? Promise.resolve({components: compsOrPath, resolver: this.resolver})
|
|
||||||
: this.loader.load(compsOrPath).then((ngModuleFactory: NgModuleFactory<WithEmbeddedComponents>) => {
|
|
||||||
const moduleRef = ngModuleFactory.create(this.injector);
|
|
||||||
return {
|
|
||||||
components: moduleRef.instance.embeddedComponents,
|
|
||||||
resolver: moduleRef.componentFactoryResolver,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const readyPromise = componentsAndResolverPromise
|
|
||||||
.then(({components, resolver}) => this.createComponentFactories(components, resolver));
|
|
||||||
|
|
||||||
this.componentFactoriesReady.set(compsOrPath, readyPromise);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.componentFactoriesReady.get(compsOrPath)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute the component content property name by converting the selector to camelCase and
|
|
||||||
* appending `Content`, e.g. `live-example` => `liveExampleContent`.
|
|
||||||
*/
|
|
||||||
protected selectorToContentPropertyName(selector: string): string {
|
|
||||||
return selector.replace(/-(.)/g, (match, $1) => $1.toUpperCase()) + 'Content';
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
import { Component, DebugElement, Input } from '@angular/core';
|
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
import { By } from '@angular/platform-browser';
|
|
||||||
|
|
||||||
import { CodeExampleComponent } from './code-example.component';
|
|
||||||
|
|
||||||
describe('CodeExampleComponent', () => {
|
|
||||||
let hostComponent: HostComponent;
|
|
||||||
let codeComponent: TestCodeComponent;
|
|
||||||
let codeExampleDe: DebugElement;
|
|
||||||
let codeExampleComponent: CodeExampleComponent;
|
|
||||||
let fixture: ComponentFixture<HostComponent>;
|
|
||||||
|
|
||||||
const oneLineCode = `const foo = "bar";`;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [ CodeExampleComponent, HostComponent, TestCodeComponent ],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function createComponent(codeExampleContent = '') {
|
|
||||||
fixture = TestBed.createComponent(HostComponent);
|
|
||||||
hostComponent = fixture.componentInstance;
|
|
||||||
codeExampleDe = fixture.debugElement.children[0];
|
|
||||||
codeExampleComponent = codeExampleDe.componentInstance;
|
|
||||||
codeComponent = codeExampleDe.query(By.directive(TestCodeComponent)).componentInstance;
|
|
||||||
|
|
||||||
// Copy the CodeExample's innerHTML (content)
|
|
||||||
// into the `codeExampleContent` property as the DocViewer does
|
|
||||||
codeExampleDe.nativeElement.codeExampleContent = codeExampleContent;
|
|
||||||
|
|
||||||
fixture.detectChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
it('should create CodeExampleComponent', () => {
|
|
||||||
createComponent();
|
|
||||||
expect(codeExampleComponent).toBeTruthy('CodeExampleComponent');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass content to CodeComponent (<aio-code>)', () => {
|
|
||||||
createComponent(oneLineCode);
|
|
||||||
expect(codeComponent.code).toBe(oneLineCode);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass language to CodeComponent', () => {
|
|
||||||
TestBed.overrideComponent(HostComponent, {
|
|
||||||
set: {template: '<code-example language="html"></code-example>'}});
|
|
||||||
createComponent(oneLineCode);
|
|
||||||
expect(codeComponent.language).toBe('html');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass linenums to CodeComponent', () => {
|
|
||||||
TestBed.overrideComponent(HostComponent, {
|
|
||||||
set: {template: '<code-example linenums="true"></code-example>'}});
|
|
||||||
createComponent(oneLineCode);
|
|
||||||
expect(codeComponent.linenums).toBe('true');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add title (header) when set `title` attribute', () => {
|
|
||||||
TestBed.overrideComponent(HostComponent, {
|
|
||||||
set: {template: '<code-example title="Great Example"></code-example>'}});
|
|
||||||
createComponent(oneLineCode);
|
|
||||||
const actual = codeExampleDe.query(By.css('header')).nativeElement.textContent;
|
|
||||||
expect(actual).toBe('Great Example');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove the `title` attribute after initialisation', () => {
|
|
||||||
TestBed.overrideComponent(HostComponent, {
|
|
||||||
set: {template: '<code-example title="Great Example"></code-example>'}});
|
|
||||||
createComponent(oneLineCode);
|
|
||||||
expect(codeExampleDe.nativeElement.getAttribute('title')).toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass hideCopy to CodeComponent', () => {
|
|
||||||
TestBed.overrideComponent(HostComponent, {
|
|
||||||
set: {template: '<code-example hideCopy="true"></code-example>'}});
|
|
||||||
createComponent(oneLineCode);
|
|
||||||
expect(codeComponent.hideCopy).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have `avoidFile` class when `avoid` atty present', () => {
|
|
||||||
TestBed.overrideComponent(HostComponent, {
|
|
||||||
set: {template: '<code-example avoid></code-example>'}});
|
|
||||||
createComponent(oneLineCode);
|
|
||||||
const classes: DOMTokenList = codeExampleDe.nativeElement.classList;
|
|
||||||
expect(classes.contains('avoidFile')).toBe(true, 'has avoidFile class');
|
|
||||||
expect(codeExampleComponent.isAvoid).toBe(true, 'isAvoid flag');
|
|
||||||
expect(codeComponent.hideCopy).toBe(true, 'hiding copy button');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have `avoidFile` class when `.avoid` in path', () => {
|
|
||||||
TestBed.overrideComponent(HostComponent, {
|
|
||||||
set: {template: '<code-example path="test.avoid.ts"></code-example>'}});
|
|
||||||
createComponent(oneLineCode);
|
|
||||||
const classes: DOMTokenList = codeExampleDe.nativeElement.classList;
|
|
||||||
expect(classes.contains('avoidFile')).toBe(true, 'has avoidFile class');
|
|
||||||
expect(codeExampleComponent.isAvoid).toBe(true, 'isAvoid flag');
|
|
||||||
expect(codeComponent.hideCopy).toBe(true, 'hide copy button flag');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not have `avoidFile` class in normal case', () => {
|
|
||||||
createComponent(oneLineCode);
|
|
||||||
const classes: DOMTokenList = codeExampleDe.nativeElement.classList;
|
|
||||||
expect(classes.contains('avoidFile')).toBe(false, 'avoidFile class');
|
|
||||||
expect(codeExampleComponent.isAvoid).toBe(false, 'isAvoid flag');
|
|
||||||
expect(codeComponent.hideCopy).toBe(false, 'hide copy button flag');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//// Test helpers ////
|
|
||||||
@Component({
|
|
||||||
selector: 'aio-code',
|
|
||||||
template: `
|
|
||||||
<div>lang: {{language}}</div>
|
|
||||||
<div>linenums: {{linenums}}</div>
|
|
||||||
code: <pre>{{someCode}}</pre>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
class TestCodeComponent {
|
|
||||||
@Input() code = '';
|
|
||||||
@Input() language: string;
|
|
||||||
@Input() linenums: string;
|
|
||||||
@Input() path: string;
|
|
||||||
@Input() region: string;
|
|
||||||
@Input() hideCopy: boolean;
|
|
||||||
|
|
||||||
get someCode() {
|
|
||||||
return this.code && this.code.length > 30 ? this.code.substr(0, 30) + '...' : this.code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'aio-host-comp',
|
|
||||||
template: `<code-example></code-example>`
|
|
||||||
})
|
|
||||||
class HostComponent { }
|
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
/* tslint:disable component-selector */
|
|
||||||
import { Component, ElementRef, HostBinding, OnInit } from '@angular/core';
|
|
||||||
import { getBoolFromAttribute } from 'app/shared/attribute-utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An embeddable code block that displays nicely formatted code.
|
|
||||||
* Example usage:
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* <code-example language="ts" linenums="2" class="special" title="Do Stuff">
|
|
||||||
* // a code block
|
|
||||||
* console.log('do stuff');
|
|
||||||
* </code-example>
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'code-example',
|
|
||||||
template: `
|
|
||||||
<header *ngIf="title">{{title}}</header>
|
|
||||||
<aio-code [ngClass]="classes" [code]="code"
|
|
||||||
[language]="language" [linenums]="linenums"
|
|
||||||
[path]="path" [region]="region"
|
|
||||||
[hideCopy]="hideCopy" [title]="title"></aio-code>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
export class CodeExampleComponent implements OnInit {
|
|
||||||
|
|
||||||
classes: {};
|
|
||||||
code: string;
|
|
||||||
language: string;
|
|
||||||
linenums: string;
|
|
||||||
path: string;
|
|
||||||
region: string;
|
|
||||||
title: string;
|
|
||||||
hideCopy: boolean;
|
|
||||||
|
|
||||||
@HostBinding('class.avoidFile')
|
|
||||||
isAvoid = false;
|
|
||||||
|
|
||||||
constructor(private elementRef: ElementRef) {
|
|
||||||
const element: HTMLElement = this.elementRef.nativeElement;
|
|
||||||
|
|
||||||
this.language = element.getAttribute('language') || '';
|
|
||||||
this.linenums = element.getAttribute('linenums') || '';
|
|
||||||
this.path = element.getAttribute('path') || '';
|
|
||||||
this.region = element.getAttribute('region') || '';
|
|
||||||
this.title = element.getAttribute('title') || '';
|
|
||||||
// Now remove the title attribute to prevent unwanted tooltip popups when hovering over the code.
|
|
||||||
element.removeAttribute('title');
|
|
||||||
|
|
||||||
const avoid = getBoolFromAttribute(element, 'avoid');
|
|
||||||
this.isAvoid = avoid || this.path.indexOf('.avoid.') !== -1;
|
|
||||||
this.hideCopy = this.isAvoid || getBoolFromAttribute(element, ['hidecopy', 'hide-copy']);
|
|
||||||
|
|
||||||
this.classes = {
|
|
||||||
'headed-code': !!this.title,
|
|
||||||
'simple-code': !this.title,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
// The `codeExampleContent` property is set by the DocViewer when it builds this component.
|
|
||||||
// It is the original innerHTML of the host element.
|
|
||||||
this.code = this.elementRef.nativeElement.codeExampleContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,246 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { Component, DebugElement, Input, NO_ERRORS_SCHEMA } from '@angular/core';
|
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
import { MatTabGroup, MatTabsModule } from '@angular/material';
|
|
||||||
import { By } from '@angular/platform-browser';
|
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|
||||||
|
|
||||||
import { CodeTabsComponent } from './code-tabs.component';
|
|
||||||
|
|
||||||
|
|
||||||
describe('CodeTabsComponent', () => {
|
|
||||||
let fixture: ComponentFixture<HostComponent>;
|
|
||||||
let hostComponent: HostComponent;
|
|
||||||
let codeTabsDe: DebugElement;
|
|
||||||
let codeTabsComponent: CodeTabsComponent;
|
|
||||||
|
|
||||||
const createComponentBasic = (codeTabsContent = '') => {
|
|
||||||
fixture = TestBed.createComponent(HostComponent);
|
|
||||||
hostComponent = fixture.componentInstance;
|
|
||||||
codeTabsDe = fixture.debugElement.children[0];
|
|
||||||
codeTabsComponent = codeTabsDe.componentInstance;
|
|
||||||
|
|
||||||
// Copy the CodeTab's innerHTML (content)
|
|
||||||
// into the `codeTabsContent` property as the DocViewer does.
|
|
||||||
codeTabsDe.nativeElement.codeTabsContent = codeTabsContent;
|
|
||||||
fixture.detectChanges();
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [ CodeTabsComponent, HostComponent, TestCodeComponent ],
|
|
||||||
imports: [ CommonModule ],
|
|
||||||
schemas: [ NO_ERRORS_SCHEMA ],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create CodeTabsComponent', () => {
|
|
||||||
createComponentBasic();
|
|
||||||
expect(codeTabsComponent).toBeTruthy('CodeTabsComponent');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('(tab labels)', () => {
|
|
||||||
let labelElems: HTMLSpanElement[];
|
|
||||||
|
|
||||||
const createComponent = (codeTabsContent?: string) => {
|
|
||||||
createComponentBasic(codeTabsContent);
|
|
||||||
const labelDes = codeTabsDe.queryAll(By.css('.mat-tab-label'));
|
|
||||||
labelElems = labelDes.map(de => de.nativeElement.querySelector('span'));
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [ MatTabsModule, NoopAnimationsModule ]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create a label for each tab', () => {
|
|
||||||
createComponent(`
|
|
||||||
<code-pane>foo</code-pane>
|
|
||||||
<code-pane>bar</code-pane>
|
|
||||||
<code-pane>baz</code-pane>
|
|
||||||
`);
|
|
||||||
|
|
||||||
expect(labelElems.length).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use the `title` as label', () => {
|
|
||||||
createComponent(`
|
|
||||||
<code-pane title="foo-title">foo</code-pane>
|
|
||||||
<code-pane title="bar-title">bar</code-pane>
|
|
||||||
`);
|
|
||||||
const texts = labelElems.map(s => s.textContent);
|
|
||||||
|
|
||||||
expect(texts).toEqual(['foo-title', 'bar-title']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add the `class` to the label element', () => {
|
|
||||||
createComponent(`
|
|
||||||
<code-pane class="foo-class">foo</code-pane>
|
|
||||||
<code-pane class="bar-class">bar</code-pane>
|
|
||||||
`);
|
|
||||||
const classes = labelElems.map(s => s.className);
|
|
||||||
|
|
||||||
expect(classes[0].split(' ')).toContain('foo-class');
|
|
||||||
expect(classes[1].split(' ')).toContain('bar-class');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should disable ripple effect on tab labels', () => {
|
|
||||||
createComponent();
|
|
||||||
const tabsGroupComponent = codeTabsDe.query(By.directive(MatTabGroup)).componentInstance;
|
|
||||||
|
|
||||||
expect(tabsGroupComponent.disableRipple).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('(tab content)', () => {
|
|
||||||
let codeDes: DebugElement[];
|
|
||||||
let codeComponents: TestCodeComponent[];
|
|
||||||
|
|
||||||
const createComponent = (codeTabsContent?: string) => {
|
|
||||||
createComponentBasic(codeTabsContent);
|
|
||||||
codeDes = codeTabsDe.queryAll(By.directive(TestCodeComponent));
|
|
||||||
codeComponents = codeDes.map(de => de.componentInstance);
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should pass `class` to CodeComponent (<aio-code>)', () => {
|
|
||||||
createComponent(`
|
|
||||||
<code-pane class="foo-class">foo</code-pane>
|
|
||||||
<code-pane class="bar-class">bar</code-pane>
|
|
||||||
`);
|
|
||||||
const classes = codeDes.map(de => de.nativeElement.className);
|
|
||||||
|
|
||||||
expect(classes).toEqual(['foo-class', 'bar-class']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass content to CodeComponent (<aio-code>)', () => {
|
|
||||||
createComponent(`
|
|
||||||
<code-pane>foo</code-pane>
|
|
||||||
<code-pane>bar</code-pane>
|
|
||||||
`);
|
|
||||||
const codes = codeComponents.map(c => c.code);
|
|
||||||
|
|
||||||
expect(codes).toEqual(['foo', 'bar']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass `language` to CodeComponent (<aio-code>)', () => {
|
|
||||||
createComponent(`
|
|
||||||
<code-pane language="foo-lang">foo</code-pane>
|
|
||||||
<code-pane language="bar-lang">bar</code-pane>
|
|
||||||
`);
|
|
||||||
const langs = codeComponents.map(c => c.language);
|
|
||||||
|
|
||||||
expect(langs).toEqual(['foo-lang', 'bar-lang']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass `linenums` to CodeComponent (<aio-code>)', () => {
|
|
||||||
createComponent(`
|
|
||||||
<code-pane linenums="foo-lnums">foo</code-pane>
|
|
||||||
<code-pane linenums="bar-lnums">bar</code-pane>
|
|
||||||
<code-pane linenums="">baz</code-pane>
|
|
||||||
<code-pane linenums>qux</code-pane>
|
|
||||||
`);
|
|
||||||
const lnums = codeComponents.map(c => c.linenums);
|
|
||||||
|
|
||||||
expect(lnums).toEqual(['foo-lnums', 'bar-lnums', '', '']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use the default value (if present on <code-tabs>) if `linenums` is not specified', () => {
|
|
||||||
TestBed.overrideComponent(HostComponent, {
|
|
||||||
set: { template: '<code-tabs linenums="default-lnums"></code-tabs>' }
|
|
||||||
});
|
|
||||||
|
|
||||||
createComponent(`
|
|
||||||
<code-pane linenums="foo-lnums">foo</code-pane>
|
|
||||||
<code-pane linenums>bar</code-pane>
|
|
||||||
<code-pane>baz</code-pane>
|
|
||||||
`);
|
|
||||||
const lnums = codeComponents.map(c => c.linenums);
|
|
||||||
|
|
||||||
expect(lnums).toEqual(['foo-lnums', '', 'default-lnums']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass `path` to CodeComponent (<aio-code>)', () => {
|
|
||||||
createComponent(`
|
|
||||||
<code-pane path="foo-path">foo</code-pane>
|
|
||||||
<code-pane path="bar-path">bar</code-pane>
|
|
||||||
`);
|
|
||||||
const paths = codeComponents.map(c => c.path);
|
|
||||||
|
|
||||||
expect(paths).toEqual(['foo-path', 'bar-path']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should default to an empty string if `path` is not spcified', () => {
|
|
||||||
createComponent(`
|
|
||||||
<code-pane>foo</code-pane>
|
|
||||||
<code-pane>bar</code-pane>
|
|
||||||
`);
|
|
||||||
const paths = codeComponents.map(c => c.path);
|
|
||||||
|
|
||||||
expect(paths).toEqual(['', '']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass `region` to CodeComponent (<aio-code>)', () => {
|
|
||||||
createComponent(`
|
|
||||||
<code-pane region="foo-region">foo</code-pane>
|
|
||||||
<code-pane region="bar-region">bar</code-pane>
|
|
||||||
`);
|
|
||||||
const regions = codeComponents.map(c => c.region);
|
|
||||||
|
|
||||||
expect(regions).toEqual(['foo-region', 'bar-region']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should default to an empty string if `region` is not spcified', () => {
|
|
||||||
createComponent(`
|
|
||||||
<code-pane>foo</code-pane>
|
|
||||||
<code-pane>bar</code-pane>
|
|
||||||
`);
|
|
||||||
const regions = codeComponents.map(c => c.region);
|
|
||||||
|
|
||||||
expect(regions).toEqual(['', '']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass `title` to CodeComponent (<aio-code>)', () => {
|
|
||||||
createComponent(`
|
|
||||||
<code-pane title="foo-title">foo</code-pane>
|
|
||||||
<code-pane title="bar-title">bar</code-pane>
|
|
||||||
`);
|
|
||||||
const titles = codeComponents.map(c => c.title);
|
|
||||||
|
|
||||||
expect(titles).toEqual(['foo-title', 'bar-title']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//// Test helpers ////
|
|
||||||
@Component({
|
|
||||||
selector: 'aio-code',
|
|
||||||
template: `
|
|
||||||
<div>lang: {{ language }}</div>
|
|
||||||
<div>linenums: {{ linenums }}</div>
|
|
||||||
code: <pre>{{ someCode }}</pre>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
class TestCodeComponent {
|
|
||||||
@Input() code = '';
|
|
||||||
@Input() hideCopy: boolean;
|
|
||||||
@Input() language: string;
|
|
||||||
@Input() linenums: string;
|
|
||||||
@Input() path: string;
|
|
||||||
@Input() region: string;
|
|
||||||
@Input() title: string;
|
|
||||||
|
|
||||||
get someCode() {
|
|
||||||
if (this.code && this.code.length > 30) {
|
|
||||||
return `${this.code.substring(0, 30)}...`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'aio-host-comp',
|
|
||||||
template: `<code-tabs></code-tabs>`
|
|
||||||
})
|
|
||||||
class HostComponent {}
|
|
@ -1,85 +0,0 @@
|
|||||||
/* tslint:disable component-selector */
|
|
||||||
import { Component, ElementRef, OnInit } from '@angular/core';
|
|
||||||
|
|
||||||
export interface TabInfo {
|
|
||||||
class: string|null;
|
|
||||||
code: string;
|
|
||||||
language: string|null;
|
|
||||||
linenums: any;
|
|
||||||
path: string;
|
|
||||||
region: string;
|
|
||||||
title: string|null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An embedded component used to generate tabbed code panes inside docs
|
|
||||||
*
|
|
||||||
* The innerHTML of the `<code-tabs>` component should contain `<code-pane>` elements.
|
|
||||||
* Each `<code-pane>` has the same interface as the embedded `<code-example>` component.
|
|
||||||
* The optional `linenums` attribute is the default `linenums` for each code pane.
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'code-tabs',
|
|
||||||
template: `
|
|
||||||
<mat-tab-group class="code-tab-group" disableRipple>
|
|
||||||
<mat-tab style="overflow-y: hidden;" *ngFor="let tab of tabs">
|
|
||||||
<ng-template mat-tab-label>
|
|
||||||
<span class="{{ tab.class }}">{{ tab.title }}</span>
|
|
||||||
</ng-template>
|
|
||||||
<aio-code class="{{ tab.class }}"
|
|
||||||
[code]="tab.code"
|
|
||||||
[language]="tab.language"
|
|
||||||
[linenums]="tab.linenums"
|
|
||||||
[path]="tab.path"
|
|
||||||
[region]="tab.region"
|
|
||||||
[title]="tab.title">
|
|
||||||
</aio-code>
|
|
||||||
</mat-tab>
|
|
||||||
</mat-tab-group>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
export class CodeTabsComponent implements OnInit {
|
|
||||||
tabs: TabInfo[];
|
|
||||||
linenumsDefault: string;
|
|
||||||
|
|
||||||
constructor(private elementRef: ElementRef) { }
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
const element = this.elementRef.nativeElement;
|
|
||||||
this.linenumsDefault = this.getLinenums(element);
|
|
||||||
|
|
||||||
// The `codeTabsContent` property is set by the DocViewer when it builds this component.
|
|
||||||
// It is the original innerHTML of the host element.
|
|
||||||
const content = element.codeTabsContent;
|
|
||||||
this.processContent(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
processContent(content: string) {
|
|
||||||
// We add it to an element so that we can easily parse the HTML
|
|
||||||
const element = document.createElement('div');
|
|
||||||
// **Security:** `codeTabsContent` is provided by docs authors and as such is considered to
|
|
||||||
// be safe for innerHTML purposes.
|
|
||||||
element.innerHTML = content;
|
|
||||||
|
|
||||||
this.tabs = [];
|
|
||||||
const codeExamples = element.querySelectorAll('code-pane');
|
|
||||||
for (let i = 0; i < codeExamples.length; i++) {
|
|
||||||
const codeExample = codeExamples.item(i);
|
|
||||||
const tab = {
|
|
||||||
class: codeExample.getAttribute('class'),
|
|
||||||
code: codeExample.innerHTML,
|
|
||||||
language: codeExample.getAttribute('language'),
|
|
||||||
linenums: this.getLinenums(codeExample),
|
|
||||||
path: codeExample.getAttribute('path') || '',
|
|
||||||
region: codeExample.getAttribute('region') || '',
|
|
||||||
title: codeExample.getAttribute('title')
|
|
||||||
};
|
|
||||||
this.tabs.push(tab);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getLinenums(element: Element) {
|
|
||||||
const linenums = element.getAttribute('linenums');
|
|
||||||
return linenums == null ? this.linenumsDefault : linenums;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
import { NgModule, Type } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
import { ContributorService } from './contributor/contributor.service';
|
|
||||||
import { CopierService } from 'app/shared/copier.service';
|
|
||||||
import { PrettyPrinter } from './code/pretty-printer.service';
|
|
||||||
import { WithEmbeddedComponents } from 'app/embed-components/embed-components.service';
|
|
||||||
|
|
||||||
// Any components that we want to use inside embedded components must be declared or imported here
|
|
||||||
// It is not enough just to import them inside the AppModule
|
|
||||||
|
|
||||||
// Reusable components (used inside embedded components)
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
|
||||||
import { MatTabsModule } from '@angular/material/tabs';
|
|
||||||
import { CodeComponent } from './code/code.component';
|
|
||||||
import { SharedModule } from 'app/shared/shared.module';
|
|
||||||
|
|
||||||
// Embedded Components
|
|
||||||
import { ApiListComponent } from './api/api-list.component';
|
|
||||||
import { ApiService } from './api/api.service';
|
|
||||||
import { CodeExampleComponent } from './code/code-example.component';
|
|
||||||
import { CodeTabsComponent } from './code/code-tabs.component';
|
|
||||||
import { ContributorListComponent } from './contributor/contributor-list.component';
|
|
||||||
import { ContributorComponent } from './contributor/contributor.component';
|
|
||||||
import { CurrentLocationComponent } from './current-location.component';
|
|
||||||
import { FileNotFoundSearchComponent } from './search/file-not-found-search.component';
|
|
||||||
import { LiveExampleComponent, EmbeddedStackblitzComponent } from './live-example/live-example.component';
|
|
||||||
import { ResourceListComponent } from './resource/resource-list.component';
|
|
||||||
import { ResourceService } from './resource/resource.service';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Components that can be embedded in docs,
|
|
||||||
* such as CodeExampleComponent, LiveExampleComponent,...
|
|
||||||
*/
|
|
||||||
export const embeddedComponents: Type<any>[] = [
|
|
||||||
ApiListComponent, CodeExampleComponent, CodeTabsComponent, ContributorListComponent,
|
|
||||||
CurrentLocationComponent, FileNotFoundSearchComponent, LiveExampleComponent, ResourceListComponent
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
MatIconModule,
|
|
||||||
MatSnackBarModule,
|
|
||||||
MatTabsModule,
|
|
||||||
SharedModule
|
|
||||||
],
|
|
||||||
declarations: [
|
|
||||||
embeddedComponents,
|
|
||||||
CodeComponent,
|
|
||||||
ContributorComponent,
|
|
||||||
EmbeddedStackblitzComponent
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
ApiService,
|
|
||||||
ContributorService,
|
|
||||||
CopierService,
|
|
||||||
PrettyPrinter,
|
|
||||||
ResourceService
|
|
||||||
],
|
|
||||||
entryComponents: [ embeddedComponents ]
|
|
||||||
})
|
|
||||||
export class EmbeddedModule implements WithEmbeddedComponents {
|
|
||||||
embeddedComponents = embeddedComponents;
|
|
||||||
}
|
|
@ -1,4 +1,3 @@
|
|||||||
import { ComponentRef } from '@angular/core';
|
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { Title, Meta } from '@angular/platform-browser';
|
import { Title, Meta } from '@angular/platform-browser';
|
||||||
|
|
||||||
@ -6,11 +5,11 @@ import { Observable } from 'rxjs/Observable';
|
|||||||
import { of } from 'rxjs/observable/of';
|
import { of } from 'rxjs/observable/of';
|
||||||
|
|
||||||
import { FILE_NOT_FOUND_ID, FETCHING_ERROR_ID } from 'app/documents/document.service';
|
import { FILE_NOT_FOUND_ID, FETCHING_ERROR_ID } from 'app/documents/document.service';
|
||||||
import { EmbedComponentsService } from 'app/embed-components/embed-components.service';
|
|
||||||
import { Logger } from 'app/shared/logger.service';
|
import { Logger } from 'app/shared/logger.service';
|
||||||
|
import { CustomElementsModule } from 'app/custom-elements/custom-elements.module';
|
||||||
import { TocService } from 'app/shared/toc.service';
|
import { TocService } from 'app/shared/toc.service';
|
||||||
import {
|
import {
|
||||||
MockEmbedComponentsService, MockTitle, MockTocService, ObservableWithSubscriptionSpies,
|
MockTitle, MockTocService, ObservableWithSubscriptionSpies,
|
||||||
TestDocViewerComponent, TestModule, TestParentComponent
|
TestDocViewerComponent, TestModule, TestParentComponent
|
||||||
} from 'testing/doc-viewer-utils';
|
} from 'testing/doc-viewer-utils';
|
||||||
import { MockLogger } from 'testing/logger.service';
|
import { MockLogger } from 'testing/logger.service';
|
||||||
@ -25,7 +24,7 @@ describe('DocViewerComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TestModule]
|
imports: [TestModule, CustomElementsModule],
|
||||||
});
|
});
|
||||||
|
|
||||||
parentFixture = TestBed.createComponent(TestParentComponent);
|
parentFixture = TestBed.createComponent(TestParentComponent);
|
||||||
@ -87,44 +86,7 @@ describe('DocViewerComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#ngDoCheck()', () => {
|
|
||||||
let componentInstances: ComponentRef<any>[];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
componentInstances = [
|
|
||||||
{changeDetectorRef: {detectChanges: jasmine.createSpy('detectChanges')}},
|
|
||||||
{changeDetectorRef: {detectChanges: jasmine.createSpy('detectChanges')}},
|
|
||||||
{changeDetectorRef: {detectChanges: jasmine.createSpy('detectChanges')}},
|
|
||||||
] as any;
|
|
||||||
docViewer.embeddedComponentRefs.push(...componentInstances);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
// Clean up the fake component instances, to avoid error in `ngOnDestroy()`.
|
|
||||||
docViewer.embeddedComponentRefs = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect changes on each active component instance', () => {
|
|
||||||
parentFixture.detectChanges();
|
|
||||||
componentInstances.forEach(({changeDetectorRef: cd}) => {
|
|
||||||
expect(cd.detectChanges).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
parentFixture.detectChanges();
|
|
||||||
componentInstances.forEach(({changeDetectorRef: cd}) => {
|
|
||||||
expect(cd.detectChanges).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#ngOnDestroy()', () => {
|
describe('#ngOnDestroy()', () => {
|
||||||
it('should destroy the active embedded component instances', () => {
|
|
||||||
const destroyEmbeddedComponentsSpy = spyOn(docViewer, 'destroyEmbeddedComponents');
|
|
||||||
docViewer.ngOnDestroy();
|
|
||||||
|
|
||||||
expect(destroyEmbeddedComponentsSpy).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stop responding to document changes', () => {
|
it('should stop responding to document changes', () => {
|
||||||
const renderSpy = spyOn(docViewer, 'render').and.returnValue([undefined]);
|
const renderSpy = spyOn(docViewer, 'render').and.returnValue([undefined]);
|
||||||
|
|
||||||
@ -143,33 +105,6 @@ describe('DocViewerComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#destroyEmbeddedComponents()', () => {
|
|
||||||
let componentInstances: ComponentRef<any>[];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
componentInstances = [
|
|
||||||
{destroy: jasmine.createSpy('destroy#1')},
|
|
||||||
{destroy: jasmine.createSpy('destroy#2')},
|
|
||||||
{destroy: jasmine.createSpy('destroy#3')},
|
|
||||||
] as any;
|
|
||||||
docViewer.embeddedComponentRefs.push(...componentInstances);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should destroy each active component instance', () => {
|
|
||||||
docViewer.destroyEmbeddedComponents();
|
|
||||||
|
|
||||||
expect(componentInstances.length).toBe(3);
|
|
||||||
componentInstances.forEach(comp => expect(comp.destroy).toHaveBeenCalledTimes(1));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clear the list of active component instances', () => {
|
|
||||||
expect(docViewer.embeddedComponentRefs.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
docViewer.destroyEmbeddedComponents();
|
|
||||||
expect(docViewer.embeddedComponentRefs.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#prepareTitleAndToc()', () => {
|
describe('#prepareTitleAndToc()', () => {
|
||||||
const EMPTY_DOC = '';
|
const EMPTY_DOC = '';
|
||||||
const DOC_WITHOUT_H1 = 'Some content';
|
const DOC_WITHOUT_H1 = 'Some content';
|
||||||
@ -357,8 +292,6 @@ describe('DocViewerComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('#render()', () => {
|
describe('#render()', () => {
|
||||||
let destroyEmbeddedComponentsSpy: jasmine.Spy;
|
|
||||||
let embedIntoSpy: jasmine.Spy;
|
|
||||||
let prepareTitleAndTocSpy: jasmine.Spy;
|
let prepareTitleAndTocSpy: jasmine.Spy;
|
||||||
let swapViewsSpy: jasmine.Spy;
|
let swapViewsSpy: jasmine.Spy;
|
||||||
|
|
||||||
@ -367,10 +300,6 @@ describe('DocViewerComponent', () => {
|
|||||||
docViewer.render({contents, id}).subscribe(resolve, reject));
|
docViewer.render({contents, id}).subscribe(resolve, reject));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const embedComponentsService = TestBed.get(EmbedComponentsService) as MockEmbedComponentsService;
|
|
||||||
|
|
||||||
destroyEmbeddedComponentsSpy = spyOn(docViewer, 'destroyEmbeddedComponents');
|
|
||||||
embedIntoSpy = embedComponentsService.embedInto.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));
|
||||||
});
|
});
|
||||||
@ -404,7 +333,7 @@ describe('DocViewerComponent', () => {
|
|||||||
expect(docViewerEl.textContent).toBe('');
|
expect(docViewerEl.textContent).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should prepare the title and ToC (before embedding components)', async () => {
|
it('should prepare the title and ToC', 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');
|
||||||
@ -413,7 +342,6 @@ describe('DocViewerComponent', () => {
|
|||||||
await doRender('Some content', 'foo');
|
await doRender('Some content', 'foo');
|
||||||
|
|
||||||
expect(prepareTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
expect(prepareTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(prepareTitleAndTocSpy).toHaveBeenCalledBefore(embedIntoSpy);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 () => {
|
||||||
@ -456,73 +384,7 @@ describe('DocViewerComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('(embedding components)', () => {
|
|
||||||
it('should embed components', async () => {
|
|
||||||
await doRender('Some content');
|
|
||||||
expect(embedIntoSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(embedIntoSpy).toHaveBeenCalledWith(docViewer.nextViewContainer);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should attempt to embed components even if the document is empty', async () => {
|
|
||||||
await doRender('');
|
|
||||||
await doRender(null);
|
|
||||||
|
|
||||||
expect(embedIntoSpy).toHaveBeenCalledTimes(2);
|
|
||||||
expect(embedIntoSpy.calls.argsFor(0)).toEqual([docViewer.nextViewContainer]);
|
|
||||||
expect(embedIntoSpy.calls.argsFor(1)).toEqual([docViewer.nextViewContainer]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should store the embedded components', async () => {
|
|
||||||
const embeddedComponents: ComponentRef<any>[] = [];
|
|
||||||
embedIntoSpy.and.returnValue(of(embeddedComponents));
|
|
||||||
|
|
||||||
await doRender('Some content');
|
|
||||||
|
|
||||||
expect(docViewer.embeddedComponentRefs).toBe(embeddedComponents);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should unsubscribe from the previous "embed" observable when unsubscribed from', () => {
|
|
||||||
const obs = new ObservableWithSubscriptionSpies();
|
|
||||||
embedIntoSpy.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('(destroying old embedded components)', () => {
|
|
||||||
it('should destroy old embedded components after creating new embedded components', async () => {
|
|
||||||
await doRender('<div></div>');
|
|
||||||
|
|
||||||
expect(destroyEmbeddedComponentsSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(embedIntoSpy).toHaveBeenCalledBefore(destroyEmbeddedComponentsSpy);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should still destroy old embedded components if the new document is empty', async () => {
|
|
||||||
await doRender('');
|
|
||||||
expect(destroyEmbeddedComponentsSpy).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
await doRender(null);
|
|
||||||
expect(destroyEmbeddedComponentsSpy).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('(swapping views)', () => {
|
describe('(swapping views)', () => {
|
||||||
it('should swap the views after destroying old embedded components', async () => {
|
|
||||||
await doRender('<div></div>');
|
|
||||||
|
|
||||||
expect(swapViewsSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(destroyEmbeddedComponentsSpy).toHaveBeenCalledBefore(swapViewsSpy);
|
|
||||||
});
|
|
||||||
|
|
||||||
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('');
|
||||||
expect(swapViewsSpy).toHaveBeenCalledTimes(1);
|
expect(swapViewsSpy).toHaveBeenCalledTimes(1);
|
||||||
@ -572,8 +434,6 @@ describe('DocViewerComponent', () => {
|
|||||||
await doRender('Some content', 'foo');
|
await doRender('Some content', 'foo');
|
||||||
|
|
||||||
expect(prepareTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
expect(prepareTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(embedIntoSpy).not.toHaveBeenCalled();
|
|
||||||
expect(destroyEmbeddedComponentsSpy).not.toHaveBeenCalled();
|
|
||||||
expect(swapViewsSpy).not.toHaveBeenCalled();
|
expect(swapViewsSpy).not.toHaveBeenCalled();
|
||||||
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
||||||
expect(logger.output.error).toEqual([
|
expect(logger.output.error).toEqual([
|
||||||
@ -584,49 +444,6 @@ 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');
|
|
||||||
embedIntoSpy.and.callFake(() => {
|
|
||||||
expect(docViewer.nextViewContainer.innerHTML).not.toBe('');
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
await doRender('Some content', 'bar');
|
|
||||||
|
|
||||||
expect(prepareTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(embedIntoSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(destroyEmbeddedComponentsSpy).not.toHaveBeenCalled();
|
|
||||||
expect(swapViewsSpy).not.toHaveBeenCalled();
|
|
||||||
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
|
||||||
expect(logger.output.error).toEqual([
|
|
||||||
[jasmine.any(Error)]
|
|
||||||
]);
|
|
||||||
expect(logger.output.error[0][0].message).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 `destroyEmbeddedComponents()` fails', async () => {
|
|
||||||
const error = Error('Typical `destroyEmbeddedComponents()` error');
|
|
||||||
destroyEmbeddedComponentsSpy.and.callFake(() => {
|
|
||||||
expect(docViewer.nextViewContainer.innerHTML).not.toBe('');
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
await doRender('Some content', 'baz');
|
|
||||||
|
|
||||||
expect(prepareTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(embedIntoSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(destroyEmbeddedComponentsSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(swapViewsSpy).not.toHaveBeenCalled();
|
|
||||||
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
|
||||||
expect(logger.output.error).toEqual([
|
|
||||||
[jasmine.any(Error)]
|
|
||||||
]);
|
|
||||||
expect(logger.output.error[0][0].message).toEqual(`[DocViewer] Error preparing document 'baz': ${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');
|
||||||
@ -638,8 +455,6 @@ describe('DocViewerComponent', () => {
|
|||||||
await doRender('Some content', 'qux');
|
await doRender('Some content', 'qux');
|
||||||
|
|
||||||
expect(prepareTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
expect(prepareTitleAndTocSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(embedIntoSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(destroyEmbeddedComponentsSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(swapViewsSpy).toHaveBeenCalledTimes(1);
|
expect(swapViewsSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
expect(docViewer.nextViewContainer.innerHTML).toBe('');
|
||||||
expect(logger.output.error).toEqual([
|
expect(logger.output.error).toEqual([
|
||||||
@ -671,25 +486,13 @@ describe('DocViewerComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('(events)', () => {
|
describe('(events)', () => {
|
||||||
it('should emit `docReady` after embedding components', async () => {
|
it('should emit `docReady`', 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(embedIntoSpy).toHaveBeenCalledBefore(onDocReadySpy);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should emit `docReady` before destroying old embedded components and swapping views', async () => {
|
|
||||||
const onDocReadySpy = jasmine.createSpy('onDocReady');
|
|
||||||
docViewer.docReady.subscribe(onDocReadySpy);
|
|
||||||
|
|
||||||
await doRender('Some content');
|
|
||||||
|
|
||||||
expect(onDocReadySpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(onDocReadySpy).toHaveBeenCalledBefore(destroyEmbeddedComponentsSpy);
|
|
||||||
expect(onDocReadySpy).toHaveBeenCalledBefore(swapViewsSpy);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit `docRendered` after swapping views', async () => {
|
it('should emit `docRendered` after swapping views', async () => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, ComponentRef, DoCheck, ElementRef, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
|
import { Component, ElementRef, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
|
||||||
import { Title, Meta } from '@angular/platform-browser';
|
import { Title, Meta } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
@ -10,9 +10,9 @@ import 'rxjs/add/operator/switchMap';
|
|||||||
import 'rxjs/add/operator/takeUntil';
|
import 'rxjs/add/operator/takeUntil';
|
||||||
|
|
||||||
import { DocumentContents, FILE_NOT_FOUND_ID, FETCHING_ERROR_ID } from 'app/documents/document.service';
|
import { DocumentContents, FILE_NOT_FOUND_ID, FETCHING_ERROR_ID } from 'app/documents/document.service';
|
||||||
import { EmbedComponentsService } from 'app/embed-components/embed-components.service';
|
|
||||||
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 { ElementsLoader } from 'app/custom-elements/elements-loader';
|
||||||
|
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
@ -28,7 +28,7 @@ const initialDocViewerContent = initialDocViewerElement ? initialDocViewerElemen
|
|||||||
// TODO(robwormald): shadow DOM and emulated don't work here (?!)
|
// TODO(robwormald): shadow DOM and emulated don't work here (?!)
|
||||||
// encapsulation: ViewEncapsulation.Native
|
// encapsulation: ViewEncapsulation.Native
|
||||||
})
|
})
|
||||||
export class DocViewerComponent implements DoCheck, OnDestroy {
|
export class DocViewerComponent implements OnDestroy {
|
||||||
// Enable/Disable view transition animations.
|
// Enable/Disable view transition animations.
|
||||||
static animationsEnabled = true;
|
static animationsEnabled = true;
|
||||||
|
|
||||||
@ -38,7 +38,6 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
|
|||||||
private onDestroy$ = new EventEmitter<void>();
|
private onDestroy$ = new EventEmitter<void>();
|
||||||
private docContents$ = new EventEmitter<DocumentContents>();
|
private docContents$ = new EventEmitter<DocumentContents>();
|
||||||
|
|
||||||
protected embeddedComponentRefs: ComponentRef<any>[] = [];
|
|
||||||
protected currViewContainer: HTMLElement = document.createElement('div');
|
protected currViewContainer: HTMLElement = document.createElement('div');
|
||||||
protected nextViewContainer: HTMLElement = document.createElement('div');
|
protected nextViewContainer: HTMLElement = document.createElement('div');
|
||||||
|
|
||||||
@ -69,12 +68,11 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
elementRef: ElementRef,
|
elementRef: ElementRef,
|
||||||
private embedComponentsService: EmbedComponentsService,
|
|
||||||
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) {
|
||||||
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;
|
||||||
@ -83,29 +81,16 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
|
|||||||
this.currViewContainer = this.hostElement.firstElementChild as HTMLElement;
|
this.currViewContainer = this.hostElement.firstElementChild as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onDestroy$.subscribe(() => this.destroyEmbeddedComponents());
|
|
||||||
this.docContents$
|
this.docContents$
|
||||||
.switchMap(newDoc => this.render(newDoc))
|
.switchMap(newDoc => this.render(newDoc))
|
||||||
.takeUntil(this.onDestroy$)
|
.takeUntil(this.onDestroy$)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngDoCheck() {
|
|
||||||
this.embeddedComponentRefs.forEach(comp => comp.changeDetectorRef.detectChanges());
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.onDestroy$.emit();
|
this.onDestroy$.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy the embedded components to avoid memory leaks.
|
|
||||||
*/
|
|
||||||
protected destroyEmbeddedComponents(): void {
|
|
||||||
this.embeddedComponentRefs.forEach(comp => comp.destroy());
|
|
||||||
this.embeddedComponentRefs = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare for setting the window title and ToC.
|
* Prepare for setting the window title and ToC.
|
||||||
* Return a function to actually set them.
|
* Return a function to actually set them.
|
||||||
@ -154,10 +139,8 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
|
|||||||
// and is considered to be safe.
|
// and is considered to be safe.
|
||||||
.do(() => this.nextViewContainer.innerHTML = doc.contents || '')
|
.do(() => this.nextViewContainer.innerHTML = doc.contents || '')
|
||||||
.do(() => addTitleAndToc = this.prepareTitleAndToc(this.nextViewContainer, doc.id))
|
.do(() => addTitleAndToc = this.prepareTitleAndToc(this.nextViewContainer, doc.id))
|
||||||
.switchMap(() => this.embedComponentsService.embedInto(this.nextViewContainer))
|
.switchMap(() => this.elementsLoader.loadContainingCustomElements(this.nextViewContainer))
|
||||||
.do(() => this.docReady.emit())
|
.do(() => this.docReady.emit())
|
||||||
.do(() => this.destroyEmbeddedComponents())
|
|
||||||
.do(componentRefs => this.embeddedComponentRefs = componentRefs)
|
|
||||||
.switchMap(() => this.swapViews(addTitleAndToc))
|
.switchMap(() => this.swapViews(addTitleAndToc))
|
||||||
.do(() => this.docRendered.emit())
|
.do(() => this.docRendered.emit())
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
@ -109,6 +109,7 @@
|
|||||||
xhr.send();
|
xhr.send();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
@ -30,6 +30,8 @@
|
|||||||
/** HACK: force import of environment.ts/environment.prod.ts to load env specific polyfills */
|
/** HACK: force import of environment.ts/environment.prod.ts to load env specific polyfills */
|
||||||
import './environments/environment';
|
import './environments/environment';
|
||||||
|
|
||||||
|
/** window.customElements */
|
||||||
|
import '@webcomponents/custom-elements/custom-elements.min';
|
||||||
|
|
||||||
/** 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`.
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { Component, ComponentRef, NgModule, ViewChild } from '@angular/core';
|
import { Component, NgModule, ViewChild } from '@angular/core';
|
||||||
import { Title, Meta } from '@angular/platform-browser';
|
import { Title, Meta } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
import { DocumentContents } from 'app/documents/document.service';
|
import { DocumentContents } from 'app/documents/document.service';
|
||||||
import { EmbedComponentsService } from 'app/embed-components/embed-components.service';
|
|
||||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
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';
|
||||||
@ -17,11 +16,9 @@ import { MockLogger } from 'testing/logger.service';
|
|||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
export class TestDocViewerComponent extends DocViewerComponent {
|
export class TestDocViewerComponent extends DocViewerComponent {
|
||||||
embeddedComponentRefs: ComponentRef<any>[];
|
|
||||||
currViewContainer: HTMLElement;
|
currViewContainer: HTMLElement;
|
||||||
nextViewContainer: HTMLElement;
|
nextViewContainer: HTMLElement;
|
||||||
|
|
||||||
destroyEmbeddedComponents(): void { return null as any; }
|
|
||||||
prepareTitleAndToc(targetElem: HTMLElement, docId: string): () => void { return null as any; }
|
prepareTitleAndToc(targetElem: HTMLElement, docId: string): () => void { return null as any; }
|
||||||
render(doc: DocumentContents): Observable<void> { return null as any; }
|
render(doc: DocumentContents): Observable<void> { return null as any; }
|
||||||
swapViews(onInsertedCb?: () => void): Observable<void> { return null as any; }
|
swapViews(onInsertedCb?: () => void): Observable<void> { return null as any; }
|
||||||
@ -43,10 +40,6 @@ export class TestParentComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mock services.
|
// Mock services.
|
||||||
export class MockEmbedComponentsService {
|
|
||||||
embedInto = jasmine.createSpy('EmbedComponentsService#embedInto');
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MockTitle {
|
export class MockTitle {
|
||||||
setTitle = jasmine.createSpy('Title#reset');
|
setTitle = jasmine.createSpy('Title#reset');
|
||||||
}
|
}
|
||||||
@ -68,7 +61,6 @@ export class MockTocService {
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: Logger, useClass: MockLogger },
|
{ provide: Logger, useClass: MockLogger },
|
||||||
{ provide: EmbedComponentsService, useClass: MockEmbedComponentsService },
|
|
||||||
{ provide: Title, useClass: MockTitle },
|
{ provide: Title, useClass: MockTitle },
|
||||||
{ provide: Meta, useClass: MockMeta },
|
{ provide: Meta, useClass: MockMeta },
|
||||||
{ provide: TocService, useClass: MockTocService },
|
{ provide: TocService, useClass: MockTocService },
|
||||||
|
@ -1,138 +0,0 @@
|
|||||||
import {
|
|
||||||
Component, ComponentFactoryResolver, ComponentRef, CompilerFactory, ElementRef, NgModule,
|
|
||||||
NgModuleFactoryLoader, OnInit, Type, ViewChild, getPlatform
|
|
||||||
} from '@angular/core';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ComponentsOrModulePath, EMBEDDED_COMPONENTS, EmbedComponentsService, EmbeddedComponentFactory,
|
|
||||||
WithEmbeddedComponents
|
|
||||||
} from 'app/embed-components/embed-components.service';
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
/// `TestEmbedComponentsService` (for exposing internal methods as public). ///
|
|
||||||
/// Only used for type-casting; the actual implementation is irrelevant. ///
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
export class TestEmbedComponentsService extends EmbedComponentsService {
|
|
||||||
componentFactories: Map<string, EmbeddedComponentFactory>;
|
|
||||||
|
|
||||||
createComponentFactories(components: Type<any>[], resolver: ComponentFactoryResolver): void { return null as any; }
|
|
||||||
createComponents(elem: HTMLElement): ComponentRef<any>[] { return null as any; }
|
|
||||||
prepareComponentFactories(compsOrPath: ComponentsOrModulePath): Promise<void> { return null as any; }
|
|
||||||
selectorToContentPropertyName(selector: string): string { return null as any; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
/// Mock `EmbeddedModule` and test components. ///
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// Test embedded components.
|
|
||||||
@Component({
|
|
||||||
selector: 'aio-eager-foo',
|
|
||||||
template: `Eager Foo Component`,
|
|
||||||
})
|
|
||||||
class EagerFooComponent { }
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'aio-eager-bar',
|
|
||||||
template: `
|
|
||||||
<hr>
|
|
||||||
<h2>Eager Bar Component</h2>
|
|
||||||
<p #content></p>
|
|
||||||
<hr>
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
class EagerBarComponent implements OnInit {
|
|
||||||
@ViewChild('content') contentRef: ElementRef;
|
|
||||||
|
|
||||||
constructor(public elementRef: ElementRef) { }
|
|
||||||
|
|
||||||
// Project content in `ngOnInit()` just like in `CodeExampleComponent`.
|
|
||||||
ngOnInit() {
|
|
||||||
// Security: This is a test component; never deployed.
|
|
||||||
this.contentRef.nativeElement.innerHTML = this.elementRef.nativeElement.aioEagerBarContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'aio-lazy-foo',
|
|
||||||
template: `Lazy Foo Component`,
|
|
||||||
})
|
|
||||||
class LazyFooComponent { }
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'aio-lazy-bar',
|
|
||||||
template: `
|
|
||||||
<hr>
|
|
||||||
<h2>Lazy Bar Component</h2>
|
|
||||||
<p #content></p>
|
|
||||||
<hr>
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
class LazyBarComponent implements OnInit {
|
|
||||||
@ViewChild('content') contentRef: ElementRef;
|
|
||||||
|
|
||||||
constructor(public elementRef: ElementRef) { }
|
|
||||||
|
|
||||||
// Project content in `ngOnInit()` just like in `CodeExampleComponent`.
|
|
||||||
ngOnInit() {
|
|
||||||
// Security: This is a test component; never deployed.
|
|
||||||
this.contentRef.nativeElement.innerHTML = this.elementRef.nativeElement.aioLazyBarContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export test embedded selectors and components.
|
|
||||||
export const testEagerEmbeddedSelectors = ['aio-eager-foo', 'aio-eager-bar'];
|
|
||||||
export const testEagerEmbeddedComponents = [EagerFooComponent, EagerBarComponent];
|
|
||||||
export const testLazyEmbeddedSelectors = ['aio-lazy-foo', 'aio-lazy-bar'];
|
|
||||||
export const testLazyEmbeddedComponents = [LazyFooComponent, LazyBarComponent];
|
|
||||||
|
|
||||||
// Export mock `EmbeddedModule` and path.
|
|
||||||
export const mockEmbeddedModulePath = 'mock/mock-embedded#MockEmbeddedModule';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [testLazyEmbeddedComponents],
|
|
||||||
entryComponents: [testLazyEmbeddedComponents],
|
|
||||||
})
|
|
||||||
class MockEmbeddedModule implements WithEmbeddedComponents {
|
|
||||||
embeddedComponents = testLazyEmbeddedComponents;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
/// `TestModule`. ///
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// Mock services.
|
|
||||||
export class MockNgModuleFactoryLoader implements NgModuleFactoryLoader {
|
|
||||||
loadedPaths: string[] = [];
|
|
||||||
|
|
||||||
load(path: string) {
|
|
||||||
this.loadedPaths.push(path);
|
|
||||||
|
|
||||||
const platformRef = getPlatform();
|
|
||||||
const compilerFactory = platformRef!.injector.get(CompilerFactory) as CompilerFactory;
|
|
||||||
const compiler = compilerFactory.createCompiler([]);
|
|
||||||
|
|
||||||
return compiler.compileModuleAsync(MockEmbeddedModule);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
providers: [
|
|
||||||
EmbedComponentsService,
|
|
||||||
{ provide: NgModuleFactoryLoader, useClass: MockNgModuleFactoryLoader },
|
|
||||||
{
|
|
||||||
provide: EMBEDDED_COMPONENTS,
|
|
||||||
useValue: {
|
|
||||||
[testEagerEmbeddedSelectors.join(',')]: testEagerEmbeddedComponents,
|
|
||||||
[testLazyEmbeddedSelectors.join(',')]: mockEmbeddedModulePath,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
declarations: [testEagerEmbeddedComponents],
|
|
||||||
entryComponents: [testEagerEmbeddedComponents],
|
|
||||||
})
|
|
||||||
export class TestModule { }
|
|
@ -130,6 +130,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.7.1"
|
tslib "^1.7.1"
|
||||||
|
|
||||||
|
"@angular/elements@file:../dist/packages-dist/elements":
|
||||||
|
version "6.0.0-beta.5-8531ff3335"
|
||||||
|
dependencies:
|
||||||
|
tslib "^1.7.1"
|
||||||
|
|
||||||
"@angular/forms@^5.2.0":
|
"@angular/forms@^5.2.0":
|
||||||
version "5.2.0"
|
version "5.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-5.2.0.tgz#b5fb6b9ba97334bca0e3202d7fee6b9162cbc824"
|
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-5.2.0.tgz#b5fb6b9ba97334bca0e3202d7fee6b9162cbc824"
|
||||||
@ -331,6 +336,14 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@webcomponents/custom-elements@^1.0.8":
|
||||||
|
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"
|
||||||
|
|
||||||
JSONStream@^1.2.1:
|
JSONStream@^1.2.1:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a"
|
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a"
|
||||||
|
Reference in New Issue
Block a user