feat(docs-infra): add getting started widgets (#26059)

PR Close #26059
This commit is contained in:
Brandon Roberts 2018-09-14 13:24:33 -05:00 committed by Kara Erickson
parent 496372dd30
commit affcbbdd7e
24 changed files with 839 additions and 1676 deletions

View File

@ -120,3 +120,20 @@ Try this <live-example></live-example>.
<live-example embedded name="testy" stackblitz="super-stackblitz"></live-example>
<p>More text follows ...</p>
<p>Getting Started Widgets</p>
<p>Interpolation</p>
<aio-gs-interpolation></aio-gs-interpolation>
<p>Property Binding</p>
<aio-gs-property-binding></aio-gs-property-binding>
<p>Event Binding</p>
<aio-gs-event-binding></aio-gs-event-binding>
<p>NgIf</p>
<aio-gs-ng-if></aio-gs-ng-if>
<p>NgFor</p>
<aio-gs-ng-for></aio-gs-ng-for>

View File

@ -2,8 +2,8 @@
"aio": {
"master": {
"uncompressed": {
"runtime": 3173,
"main": 494475,
"runtime": 3881,
"main": 499953,
"polyfills": 53926,
"prettify": 14917
}

View File

@ -48,6 +48,26 @@ export const ELEMENT_MODULE_PATHS_AS_ROUTES = [
selector: 'live-example',
loadChildren: './live-example/live-example.module#LiveExampleModule'
},
{
selector: 'aio-gs-interpolation',
loadChildren: './getting-started/interpolation/interpolation.module#InterpolationModule'
},
{
selector: 'aio-gs-property-binding',
loadChildren: './getting-started/property-binding/property-binding.module#PropertyBindingModule'
},
{
selector: 'aio-gs-event-binding',
loadChildren: './getting-started/event-binding/event-binding.module#EventBindingModule'
},
{
selector: 'aio-gs-ng-if',
loadChildren: './getting-started/ng-if/ng-if.module#NgIfModule'
},
{
selector: 'aio-gs-ng-for',
loadChildren: './getting-started/ng-for/ng-for.module#NgForModule'
},
];
/**

View File

@ -0,0 +1,39 @@
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ContainerComponent } from './container.component';
@Component({
template: `
<aio-gs-container>
<ng-container class="template">Template</ng-container>
<ng-container class="data">Data</ng-container>
<ng-container class="result">Result</ng-container>
</aio-gs-container>
`
})
export class TestComponent {}
describe('Getting Started Container Component', () => {
let fixture: ComponentFixture<ContainerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ ContainerComponent, TestComponent ]
});
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
});
it('should project the content into the appropriate areas', () => {
const compiled = fixture.debugElement.nativeElement;
const pre = compiled.querySelector('pre');
const code = compiled.querySelector('code');
const tabledata = compiled.querySelectorAll('td');
expect(pre.textContent).toContain('Template');
expect(code.textContent).toContain('Data');
expect(tabledata[2].textContent).toContain('Result');
});
});

View File

@ -0,0 +1,80 @@
import { Component } from '@angular/core';
@Component({
selector: 'aio-gs-container',
template: `
<table>
<thead>
<tr>
<th>Template</th>
<th>Data</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<pre><ng-content select=".template"></ng-content></pre>
</td>
<td>
<code><ng-content select=".data"></ng-content></code>
</td>
<td><ng-content select=".result"></ng-content></td>
</tr>
</tbody>
</table>
`,
styles: [
`
pre {
margin: 0;
}
code {
display: flex;
align-items: center;
}
@media only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
/* Force table to not be like tables anymore */
table, thead, tbody, th, td, tr {
display: block;
}
/* Hide table headers (but not display: none;, for accessibility) */
thead tr {
position: absolute;
top: -9999px;
left: -9999px;
}
tr { border: 1px solid #ccc; }
td {
/* Behave like a "row" */
border: none;
border-bottom: 1px solid #eee;
position: relative;
padding-top: 10%;
}
td:before {
/* Now like a table header */
position: absolute;
/* Top/left values mimic padding */
top: 6px;
left: 6px;
width: 45%;
padding-right: 10px;
}
/* Label the data */
td:nth-of-type(1):before { content: "Template"; }
td:nth-of-type(2):before { content: "Data"; }
td:nth-of-type(3):before { content: "Result"; }
}
`
]
})
export class ContainerComponent { }

View File

@ -0,0 +1,10 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ContainerComponent } from './container.component';
@NgModule({
imports: [ CommonModule ],
declarations: [ ContainerComponent ],
exports: [ ContainerComponent ]
})
export class ContainerModule { }

View File

@ -0,0 +1,46 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ContainerModule } from '../container/container.module';
import { EventBindingComponent } from './event-binding.component';
import { createCustomEvent } from '../../../../testing/dom-utils';
describe('Getting Started Event Binding Component', () => {
let component: EventBindingComponent;
let fixture: ComponentFixture<EventBindingComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ ContainerModule ],
declarations: [ EventBindingComponent ]
});
fixture = TestBed.createComponent(EventBindingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
spyOn(window, 'alert');
});
it('should update the name property on input change', () => {
const text = 'Hello Angular';
const compiled = fixture.debugElement.nativeElement;
const input: HTMLInputElement = compiled.querySelector('input');
input.value = text;
input.dispatchEvent(createCustomEvent(document, 'input', ''));
fixture.detectChanges();
expect(component.name).toBe(text);
});
it('should display an alert when the button is clicked', () => {
const compiled = fixture.debugElement.nativeElement;
const button: HTMLButtonElement = compiled.querySelector('button');
button.click();
expect(window.alert).toHaveBeenCalledWith('Hello, Angular!');
});
});

View File

@ -0,0 +1,30 @@
import { Component } from '@angular/core';
@Component({
selector: 'aio-gs-event-binding',
template: `
<aio-gs-container>
<ng-container class="template">&lt;button (click)="greet(name)"&gt;
Greet
&lt;/button&gt;</ng-container>
<ng-container class="data">
name = '<input #input (input)="name = input.value" [value]="name">';
</ng-container>
<ng-container class="result">
<button (click)="greet(name)">
Greet
</button>
</ng-container>
</aio-gs-container>
`,
preserveWhitespaces: true
})
export class EventBindingComponent {
name = 'Angular';
greet(name: string) {
window.alert(`Hello, ${name}!`);
}
}

View File

@ -0,0 +1,15 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { WithCustomElementComponent } from '../../element-registry';
import { EventBindingComponent } from './event-binding.component';
import { ContainerModule } from '../container/container.module';
@NgModule({
imports: [ CommonModule, ContainerModule ],
declarations: [ EventBindingComponent ],
exports: [ EventBindingComponent ],
entryComponents: [ EventBindingComponent ]
})
export class EventBindingModule implements WithCustomElementComponent {
customElementComponent: Type<any> = EventBindingComponent;
}

View File

@ -0,0 +1,41 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InterpolationComponent } from './interpolation.component';
import { ContainerModule } from '../container/container.module';
import { createCustomEvent } from '../../../../testing/dom-utils';
describe('Getting Started Interpolation Component', () => {
let component: InterpolationComponent;
let fixture: ComponentFixture<InterpolationComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ ContainerModule ],
declarations: [ InterpolationComponent ]
});
fixture = TestBed.createComponent(InterpolationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should update the siteName property on input change', () => {
const text = 'Hello Angular';
const compiled = fixture.debugElement.nativeElement;
const input: HTMLInputElement = compiled.querySelector('input');
input.value = text;
input.dispatchEvent(createCustomEvent(document, 'input', ''));
fixture.detectChanges();
expect(component.siteName).toBe(text);
});
it('should display the siteName', () => {
const compiled = fixture.debugElement.nativeElement;
const header: HTMLHeadingElement = compiled.querySelector('h1');
expect(header.textContent).toContain('My Store');
});
});

View File

@ -0,0 +1,20 @@
import { Component } from '@angular/core';
@Component({
selector: 'aio-gs-interpolation',
template: `
<aio-gs-container>
<ng-container class="template">&lt;h1&gt;Welcome to {{'{'+'{'}}siteName{{'}'+'}'}}&lt;h1&gt;</ng-container>
<ng-container class="data">
siteName = '<input #input (input)="siteName = input.value" [value]="siteName">';
</ng-container>
<ng-container class="result"><h1>Welcome to {{ siteName }}</h1></ng-container>
</aio-gs-container>
`
})
export class InterpolationComponent {
siteName = 'My Store';
}

View File

@ -0,0 +1,15 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { WithCustomElementComponent } from '../../element-registry';
import { InterpolationComponent } from './interpolation.component';
import { ContainerModule } from '../container/container.module';
@NgModule({
imports: [ CommonModule, ContainerModule ],
declarations: [ InterpolationComponent ],
exports: [ InterpolationComponent ],
entryComponents: [ InterpolationComponent ]
})
export class InterpolationModule implements WithCustomElementComponent {
customElementComponent: Type<any> = InterpolationComponent;
}

View File

@ -0,0 +1,42 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NgForComponent } from './ng-for.component';
import { ContainerModule } from '../container/container.module';
import { ProductService } from '../product.service';
describe('Getting Started NgFor Component', () => {
let component: NgForComponent;
let fixture: ComponentFixture<NgForComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ ContainerModule ],
declarations: [ NgForComponent ],
providers: [ ProductService ]
});
fixture = TestBed.createComponent(NgForComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should display the products', () => {
const compiled = fixture.debugElement.nativeElement;
const spans = compiled.querySelectorAll('span');
expect(spans[0]!.textContent).toContain('Shoes');
expect(spans[1]!.textContent).toContain('Phones');
});
it('should display an error message if provided products JSON is invalid', () => {
fixture.detectChanges();
component.productsData$.next('bad');
fixture.detectChanges();
component.parseError$.subscribe(error => {
expect(error).toBeTruthy();
})
});
});

View File

@ -0,0 +1,46 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { ProductService } from '../product.service';
@Component({
selector: 'aio-gs-ng-for',
template: `
<aio-gs-container>
<ng-container class="template">&lt;span *ngFor="let product of products">
{{'{'+'{'}}product{{'}'+'}'}}
&lt;/span&gt;</ng-container>
<ng-container class="data">
products = <input #input (input)="productsData$.next(input.value)" [value]="productsData$ | async">;
<div *ngIf="parseError$ | async" class="material-icons" matTooltip="The provided JSON is invalid">error_outline</div>
</ng-container>
<ng-container class="result">
<span *ngFor="let product of products$ | async">{{product}}</span>
</ng-container>
</aio-gs-container>
`,
styles: [`
span::after {
content: ' ';
}
`],
preserveWhitespaces: true
})
export class NgForComponent implements OnInit, OnDestroy {
productsData$ = this.productService.productsData$;
products$ = this.productService.products$;
parseError$ = this.productService.parseError$;
productsSub: Subscription;
constructor(private productService: ProductService) {}
ngOnInit() {
this.productsSub = this.productService.init().subscribe();
}
ngOnDestroy() {
this.productsSub.unsubscribe();
}
}

View File

@ -0,0 +1,18 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatTooltipModule } from '@angular/material/tooltip';
import { WithCustomElementComponent } from '../../element-registry';
import { NgForComponent } from './ng-for.component';
import { ContainerModule } from '../container/container.module';
import { ProductService } from '../product.service';
@NgModule({
imports: [ CommonModule, ContainerModule, MatTooltipModule ],
declarations: [ NgForComponent ],
exports: [ NgForComponent ],
entryComponents: [ NgForComponent ],
providers: [ ProductService ]
})
export class NgForModule implements WithCustomElementComponent {
customElementComponent: Type<any> = NgForComponent;
}

View File

@ -0,0 +1,51 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NgIfComponent } from './ng-if.component';
import { ContainerModule } from '../container/container.module';
import { ProductService } from '../product.service';
describe('Getting Started NgIf Component', () => {
let component: NgIfComponent;
let fixture: ComponentFixture<NgIfComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ ContainerModule ],
declarations: [ NgIfComponent ],
providers: [ ProductService ]
});
fixture = TestBed.createComponent(NgIfComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should display the message if products are listed', () => {
const compiled = fixture.debugElement.nativeElement;
const paragraph = compiled.querySelector('p');
expect(paragraph.textContent).toContain('available');
});
it('should not display the message if products list is empty', () => {
component.products$.next([]);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
const paragraph = compiled.querySelector('p');
expect(paragraph).toBeFalsy();
});
it('should display an error message if provided products JSON is invalid', () => {
fixture.detectChanges();
component.productsData$.next('bad');
fixture.detectChanges();
component.parseError$.subscribe(error => {
expect(error).toBeTruthy();
})
});
});

View File

@ -0,0 +1,43 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { ProductService } from '../product.service';
@Component({
selector: 'aio-gs-ng-if',
template: `
<aio-gs-container>
<ng-container class="template">&lt;p *ngIf="products.length > 0"&gt;
We still have products available.
&lt;/p&gt;</ng-container>
<ng-container class="data">
products = <input #input (input)="productsData$.next(input.value)" [value]="productsData$ | async">;
<div *ngIf="parseError$ | async" class="material-icons" matTooltip="The provided JSON is invalid">error_outline</div>
</ng-container>
<ng-container class="result">
<p *ngIf="(products$ | async)?.length > 0">
We still have products available.
</p>
</ng-container>
</aio-gs-container>
`,
preserveWhitespaces: true
})
export class NgIfComponent implements OnInit, OnDestroy {
productsData$ = this.productService.productsData$;
products$ = this.productService.products$;
parseError$ = this.productService.parseError$;
productsSub: Subscription;
constructor(private productService: ProductService) {}
ngOnInit() {
this.productsSub = this.productService.init().subscribe();
}
ngOnDestroy() {
this.productsSub.unsubscribe();
}
}

View File

@ -0,0 +1,18 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatTooltipModule } from '@angular/material/tooltip';
import { WithCustomElementComponent } from '../../element-registry';
import { NgIfComponent } from './ng-if.component';
import { ContainerModule } from '../container/container.module';
import { ProductService } from '../product.service';
@NgModule({
imports: [ CommonModule, ContainerModule, MatTooltipModule ],
declarations: [ NgIfComponent ],
exports: [ NgIfComponent ],
entryComponents: [ NgIfComponent ],
providers: [ ProductService ]
})
export class NgIfModule implements WithCustomElementComponent {
customElementComponent: Type<any> = NgIfComponent;
}

View File

@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs';
import { tap, debounceTime } from 'rxjs/operators';
export const PRODUCTS = ['Shoes', 'Phones'];
@Injectable()
export class ProductService {
productsData$ = new BehaviorSubject(JSON.stringify(PRODUCTS));
products$ = new BehaviorSubject<string[]>(PRODUCTS);
parseError$ = new Subject<boolean>();
init() {
return this.productsData$.pipe(
debounceTime(250),
tap(data => {
let parsed;
try {
parsed = JSON.parse(data);
} catch (e) {
parsed = null;
}
if (parsed && Array.isArray(parsed)) {
this.products$.next(parsed);
this.parseError$.next(false);
} else {
this.parseError$.next(true);
}
}));
}
}

View File

@ -0,0 +1,46 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PropertyBindingComponent } from './property-binding.component';
import { ContainerModule } from '../container/container.module';
import { createCustomEvent } from '../../../../testing/dom-utils';
describe('Getting Started Property Binding Component', () => {
let component: PropertyBindingComponent;
let fixture: ComponentFixture<PropertyBindingComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ ContainerModule ],
declarations: [ PropertyBindingComponent ]
});
fixture = TestBed.createComponent(PropertyBindingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should update the image title on input change', () => {
const text = 'Hello Angular';
const compiled = fixture.debugElement.nativeElement;
const input: HTMLInputElement = compiled.querySelector('input');
input.value = text;
input.dispatchEvent(createCustomEvent(document, 'input', ''));
fixture.detectChanges();
expect(component.imageTitle).toBe(text);
});
it('should display the image title', () => {
const compiled = fixture.debugElement.nativeElement;
const image: HTMLImageElement = compiled.querySelector('img');
expect(image.getAttribute('title')).toContain('Angular Logo');
});
});

View File

@ -0,0 +1,21 @@
import { Component } from '@angular/core';
@Component({
selector: 'aio-gs-property-binding',
template: `
<aio-gs-container>
<ng-container class="template">&lt;img ... [title]="imageTitle"&gt;</ng-container>
<ng-container class="data">
imageTitle = '<input #input (input)="imageTitle = input.value" [value]="imageTitle">';
</ng-container>
<ng-container class="result">
<img src="/assets/images/logos/angular/angular.svg" width="37" height="40" [title]="imageTitle">
</ng-container>
</aio-gs-container>
`
})
export class PropertyBindingComponent {
imageTitle = 'Angular Logo';
}

View File

@ -0,0 +1,15 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { WithCustomElementComponent } from '../../element-registry';
import { PropertyBindingComponent } from './property-binding.component';
import { ContainerModule } from '../container/container.module';
@NgModule({
imports: [ CommonModule, ContainerModule ],
declarations: [ PropertyBindingComponent ],
exports: [ PropertyBindingComponent ],
entryComponents: [ PropertyBindingComponent ]
})
export class PropertyBindingModule implements WithCustomElementComponent {
customElementComponent: Type<any> = PropertyBindingComponent;
}

View File

@ -0,0 +1,16 @@
/**
* Create a `CustomEvent` (even on browsers where `CustomEvent` is not a constructor).
*/
export function createCustomEvent(doc: Document, name: string, detail: any): CustomEvent {
const bubbles = false;
const cancelable = false;
// On IE9-11, `CustomEvent` is not a constructor.
if (typeof CustomEvent !== 'function') {
const event = doc.createEvent('CustomEvent');
event.initCustomEvent(name, bubbles, cancelable, detail);
return event;
}
return new CustomEvent(name, {bubbles, cancelable, detail});
}

File diff suppressed because it is too large Load Diff