Created
June 25, 2019 19:52
-
-
Save NetanelBasal/0357bb367157ac07bc5c8d5e115a4c4e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { BrowserModule } from '@angular/platform-browser'; | |
import { ApplicationRef, ComponentFactoryResolver, ComponentRef, EventEmitter, Injector, NgModule } from '@angular/core'; | |
import { AppRoutingModule } from './app-routing.module'; | |
import { AppComponent } from './app.component'; | |
import { AlertComponent } from './hello/alert.component'; | |
import { map } from 'rxjs/operators'; | |
import { merge, Observable, Subscription } from 'rxjs'; | |
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 }); | |
} | |
function camelToDashCase(input: string): string { | |
return input.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`); | |
} | |
function getComponentFactory(component, injector) { | |
const componentFactoryResolver = injector.get(ComponentFactoryResolver); | |
return componentFactoryResolver.resolveComponentFactory(component); | |
} | |
function getDefaultAttributeToPropertyInputs( | |
inputs: { propName: string, templateName: string }[]) { | |
const attributeToPropertyInputs: { [key: string]: string } = {}; | |
inputs.forEach(({ propName, templateName }) => { | |
attributeToPropertyInputs[camelToDashCase(templateName)] = propName; | |
}); | |
return attributeToPropertyInputs; | |
} | |
function initializeOutputs(outputs, instance): Observable<any> { | |
const eventEmitters = outputs.map(({ propName, templateName }) => { | |
const emitter = instance[propName] as EventEmitter<any>; | |
return emitter.pipe(map((value: any) => ({ name: templateName, value }))); | |
}); | |
return merge(...eventEmitters); | |
} | |
function initializeComponent(element: HTMLElement, component, injector: Injector) { | |
const childInjector = Injector.create({ providers: [], parent: injector }); | |
const componentFactory = getComponentFactory(component, injector); | |
let componentRef = componentFactory.create(childInjector, [], element); | |
componentRef.changeDetectorRef.detectChanges(); | |
const applicationRef = injector.get<ApplicationRef>(ApplicationRef); | |
applicationRef.attachView(componentRef.hostView); | |
return componentRef; | |
} | |
export function customElementPlease(component, { injector }) { | |
const factory = getComponentFactory(component, injector); | |
const inputs = factory.inputs; | |
const attributeToPropertyInputs = getDefaultAttributeToPropertyInputs(inputs); | |
class NgElement extends HTMLElement { | |
static observedAttributes = Object.keys(attributeToPropertyInputs); | |
componentRef: ComponentRef<any>; | |
subscription: Subscription; | |
constructor() { | |
super(); | |
} | |
connectedCallback(): void { | |
if ( !this.componentRef ) { | |
this.componentRef = initializeComponent(this, component, injector); | |
} | |
const outputs = initializeOutputs(factory.outputs, this.componentRef.instance); | |
this.subscription = outputs.subscribe(e => { | |
const customEvent = createCustomEvent(this.ownerDocument, e.name, e.value); | |
this.dispatchEvent(customEvent); | |
}); | |
} | |
getInputValue(name: string) { | |
return this.componentRef.instance[name]; | |
} | |
setInputValue(property, newValue) { | |
this.componentRef.instance[property] = newValue; | |
this.componentRef.changeDetectorRef.detectChanges(); | |
} | |
attributeChangedCallback( | |
attrName: string, oldValue: string | null, newValue: string): void { | |
if ( !this.componentRef ) { | |
this.componentRef = initializeComponent(this, component, injector); | |
} | |
const propName = attributeToPropertyInputs[attrName] !; | |
this.setInputValue(propName, newValue); | |
} | |
disconnectedCallback(): void { | |
if ( this.componentRef ) { | |
this.componentRef !.destroy(); | |
this.componentRef = null; | |
} | |
if ( this.subscription ) { | |
this.subscription.unsubscribe(); | |
this.subscription = null; | |
} | |
} | |
} | |
inputs.map(({ propName }) => propName).forEach(property => { | |
Object.defineProperty(NgElement.prototype, property, { | |
get: function() { | |
return this.getInputValue(property); | |
}, | |
set: function(newValue: any) { | |
this.setInputValue(property, newValue); | |
}, | |
configurable: true, | |
enumerable: true, | |
}); | |
}); | |
return NgElement; | |
} | |
@NgModule({ | |
declarations: [ | |
AppComponent, | |
AlertComponent, | |
], | |
imports: [ | |
BrowserModule, | |
AppRoutingModule, | |
], | |
entryComponents: [AlertComponent], | |
providers: [], | |
bootstrap: [] | |
}) | |
export class AppModule { | |
constructor(private injector: Injector) { | |
} | |
ngDoBootstrap() { | |
const elm = customElementPlease(AlertComponent, { injector: this.injector }); | |
customElements.define('my-alert', elm); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment