Skip to content
This repository has been archived by the owner on Nov 9, 2017. It is now read-only.

Commit

Permalink
fix things for stephen
Browse files Browse the repository at this point in the history
  • Loading branch information
robwormald committed Jul 7, 2017
1 parent c443489 commit a0673b3
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 33 deletions.
87 changes: 86 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,88 @@
### experiments with Angular and Web Components

Nothing to see here yet :)
Sketching out hosting Angular Components as Custom Elements / Web Components



#### Angular Component API (4.x)

Angular components are

```ts

//annotate with metadata about template and styles
@Component({
selector: 'my-component',
templateUrl: 'my-component.html',
styleUrls: [ 'my-component.css' ],
viewEncapsulation: ViewEncapsulation.Native //Shadow DOM V0
providers: [ SomeService ],

})
class MyComponent {
//public API for component consumers
@Input() someProperty:any; //property values passed in
@Output() someEvent new EventEmitter(); //events go out

@ViewChild(ChildComponent) childView:ChildComponent;
@ViewChildren(ItemComponent) items:QueryList<ItemComponent>

//bind to host element events
@HostListener('mousemove', ['$event'])
onMouseMouse(event:MouseEvent){}

//bind to host properties and attributes
@HostBinding('attr.aria-foo') someValue;

//lifecycle events
ngOnInit(){}
ngOnChanges(changes){}
ngDoCheck(){}
ngOnDestroy(){}

//view lifecycle events
ngAfterContentInit(){}
ngAfterContentChecked(){}
ngAfterViewInit(){}
ngAfterViewChecked(){}


}

```


#### Custom Elements v1 API

```ts

class MyCustomElement extends HTMLElement {
//newable
constructor(...optionalArgs?:any[]){}

//attributes to observe changes to
static get observedAttributes():string[]{ return ['value', 'foo'] }

//properties
someProp:string;
set foo(value:string) { }
get foo() { return 'foo' }

//dispatch events
onClick(){
this.dispatchEvent(new CustomEvent('some-event', options))
}

//called when attributes change
attributeChangedCallback(attributeName, oldValue, newValue, namespace):void {}

//called when connected to a document / shadow tree
connectedCallback():void {}

//called when removed from document
disconnectedCallback():void {}

}

```

70 changes: 70 additions & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Karma configuration
// Generated on Wed Jun 28 2017 13:38:35 GMT-0700 (PDT)

module.exports = function(config) {
config.set({

// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',


// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],


// list of files / patterns to load in the browser
files: [
'public/es5-shim.js',
'public/angular.js'
],


// list of files to exclude
exclude: [
],


// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
},


// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],


// web server port
port: 9876,


// enable / disable colors in the output (reporters and logs)
colors: true,


// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,


// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,


// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Chrome'],


// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,

// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity
})
}
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,19 @@
"@ngrx/core": "^1.2.0",
"@ngrx/effects": "^2.0.3",
"@ngrx/store": "^2.2.2",
"@types/node": "^8.0.6",
"gulp": "^3.9.1",
"rxjs": "^5.4.1",
"typescript": "^2.3.4",
"webcomponentsjs": "^1.0.2",
"webpack": "^3.0.0",
"zone.js": "^0.8.12"
},
"devDependencies": {
"karma-chrome-launcher": "^2.2.0",
"karma-jasmine": "^1.1.0"
},
"scripts": {
"build": "ngc && webpack -p"
}
}
Empty file added rollup.config.js
Empty file.
38 changes: 27 additions & 11 deletions src/custom_element_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import { SimpleRendererFactory } from './simple_renderer'
//TODO: ask tbosch about correct way to do this?
core.ɵclearProviderOverrides();

const santizer:core.Sanitizer = {
sanitize: (ctx, v:any) => v
}

let renderer: SimpleRendererFactory;

//Custom Element wrapper that acts as injector + host element
//TODO: maybe this should implement ComponentRef instead/also
class NgElement<T> extends HTMLElement implements core.Injector, core.NgModuleRef<any> {
componentFactory: core.ComponentFactory<T>;
componentRef: core.ComponentRef<T>;
componentRef: core.ComponentRef<T> | undefined;
moduleType: any;
constructor(private parentInjector?: core.Injector) {
super();
Expand All @@ -26,30 +30,40 @@ class NgElement<T> extends HTMLElement implements core.Injector, core.NgModuleRe
case core.RendererFactory2:
return renderer;
case core.Sanitizer:
return santizer;
case core.ErrorHandler:
return null;
case core.NgModuleRef:
return this;
}
if(this.parentInjector){
return this.parentInjector.get(token, notFoundValue);
}
return core.Injector.NULL.get(token, notFoundValue);
}
connectedCallback() {
this.bootstrap();
}
disconnectedCallback() {
this.componentRef.destroy();
this.teardown();
}


bootstrap() {
const element = this;
if(element.componentRef){
return;
}
//create an instance of angular component
//TODO: figure out semantics of connect/disconnect and how it relates to component/module destruction
this.componentRef =
this.componentFactory.create(this.parentInjector ? this.parentInjector : core.Injector.NULL, [Array.of(this.children)], this, this);

element.componentRef =
element.componentFactory.create(this.parentInjector ? this.parentInjector : core.Injector.NULL, [Array.of(this.children)], this, this);
if(!element.componentRef){
throw new Error('component could not be created!')
}
//wire up the component's @Outputs to dispatchEvent()
const listeners = this.componentFactory.outputs.map((output) => {
const emitter: core.EventEmitter<any> = (this.componentRef.instance as any)[output.templateName];
const emitter: core.EventEmitter<any> = (element.componentRef!.instance as any)[output.templateName];
return emitter.subscribe((payload: any) => this.dispatchEvent(new CustomEvent(output.propName, { detail: payload })));
});

Expand All @@ -58,21 +72,23 @@ class NgElement<T> extends HTMLElement implements core.Injector, core.NgModuleRe
//TODO: this is gross. figure out a better way to handle this
Object.defineProperty(element, input.propName, {
set(value) {
(element.componentRef.instance as any)[input.templateName] = value;
(element.componentRef!.instance as any)[input.templateName] = value;
//TODO: consider batching these somehow. this is in the wrong place.
element.componentFactoryResolver.changeDetectorRef.detectChanges();
element.componentRef!.changeDetectorRef.detectChanges();
},
get() {
return (element.componentRef.instance as any)[input.templateName];
return (element.componentRef!.instance as any)[input.templateName];
}
})
})
//cleanup the component
this.componentRef.onDestroy(() => listeners.forEach(l => l.unsubscribe()));
element.componentRef!.onDestroy(() => listeners.forEach(l => l.unsubscribe()));
requestAnimationFrame(() => this.tick());
}

tick() { this.componentRef.changeDetectorRef.detectChanges(); }
teardown(){}

tick() { if(this.componentRef) { this.componentRef.changeDetectorRef.detectChanges(); } }

get injector() { return this; }
//TODO: wire up to customElements.get ?
Expand Down
3 changes: 1 addition & 2 deletions src/demos/progress-bar/progress-bar.ngelement.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import nge from '../../custom_element_adapter'
import {NgProgressBarNgFactory} from '../../ngfactory/src/demos/progress-bar/ng-progress-bar.ngfactory'
import {ProgressBar} from './progress-bar'

nge.define(NgProgressBarNgFactory);
customElements.define('progress-bar', ProgressBar);

8 changes: 5 additions & 3 deletions src/demos/todo-app/todo-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class TodoService {
this.todos.next(this.todos.value.concat([{text: 'new todo text', completed: false}]))
}
completeTodo(completedTodo:any){
this.todos.next(this.todos.value.map(todo => todo === completedTodo ? Object.assign({}, todo, {completed: true}) : todo ))
this.todos.next(this.todos.value.filter(todo => todo !== completedTodo))
}
}

Expand All @@ -17,7 +17,7 @@ export class TodoService {
<h1>Todo App</h1>
<input type="text" #todoInput>
<button (click)="addTodo(todoInput)"></button>
<todo-list [todos]="todoService.todos | push"></todo-list>
<todo-list [todos]="todoService.todos | push" (completeTodo)="completeTodo($event)"></todo-list>
`,
encapsulation: ViewEncapsulation.Native,
styles: [
Expand All @@ -32,7 +32,9 @@ export class TodoService {
providers: [TodoService]
})
export class TodoApp {
constructor(public todoService:TodoService, private cdr:ChangeDetectorRef){}
constructor(public todoService:TodoService, private cdr:ChangeDetectorRef){

}

//these are internal to the component.
addTodo(input:HTMLInputElement){
Expand Down
12 changes: 2 additions & 10 deletions src/demos/todo-app/todo-list.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import {Component, Input, Output, EventEmitter, ɵg, IterableDiffers} from '@angular/core'

export function _iterableDiffersFactory(){
return ɵg;
}
import {Component, Input, Output, EventEmitter} from '@angular/core'

@Component({
selector: 'todo-list',
template: `
<ul>
<li *ngFor="let todo of todos">
{{todo.text}} - <button (click)="complete(todo)">x</button>
{{todo.text}} - <button (click)="completeTodo.emit(todo)">x</button>
</li>
</ul>
`,
Expand All @@ -19,8 +15,4 @@ export function _iterableDiffersFactory(){
export class TodoList {
@Input() todos: any[]
@Output() completeTodo = new EventEmitter();

complete(todo:any){
this.completeTodo.emit(todo);
}
}
6 changes: 3 additions & 3 deletions src/directives/push_pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export class PushPipe {
private _currentStream:Observable<any>;
private _currentValue:any;
private _activeSubscription:Subscription;
constructor(private _cdr:ChangeDetectorRef ){}
transform(stream: Observable<any> | null){
constructor(private _cdr:ChangeDetectorRef){}
transform(stream: Observable<any> | null, transforms?:(s:Observable<any>) => Observable<any>){
if(!this._currentStream && stream){
this._createSubscription(stream);
}
Expand All @@ -29,7 +29,7 @@ export class PushPipe {
private _onValueChange(newValue:any){
if(newValue !== this._currentValue){
this._currentValue = newValue;
this._cdr.detectChanges();
requestAnimationFrame(() => this._cdr.detectChanges());
}
}
}
6 changes: 4 additions & 2 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ const path = require('path');
const webpack = require('webpack')
module.exports = {
entry: {
'todo-app': './lib/demos/todo-app/todo-app-element.js',
'now-card': './lib/demos/now-cards/now-card-element.js',
'todo-app': './lib/demos/todo-app/todo-app.ngelement.js',
'now-card-feed': './lib/demos/now-cards/now-card-feed.ngelement.js',
'now-card': './lib/demos/now-cards/now-card.ngelement.js',
'progress-bars': './lib/demos/progress-bar/progress-bar.ngelement.js',
'angular': ['@angular/core', './lib/custom_element_adapter.js']
},
output: {
Expand Down
Loading

0 comments on commit a0673b3

Please sign in to comment.