Step 1:
<body>
<label>my label</label>
<input/>
<button>Submit</button>
</body>
<body>
<app-form label="my label"></app-form>
</body>
Callback | Called When |
---|---|
constructor | element is created or upgraded |
connectedCallback | element is inserted into DOM |
disconnectedCallback | element is removed from DOM |
attributeChangedCallback | attributes in observedAttributes being changed (CUD) |
class AppForm extends HTMLElement {
constructor() {}
connectedCallback() {}
disconnectedCallback() {}
attributeChangedCallback() {}
}
<app-form label="my label"></app-form>
const appForm = document.createElement('app-form');
// AppForm constructor is called
appForm.setAttribute('label', 'my-label');
// AppForm attributeChangedCallback is called
document.body.appendChild(appForm);
// AppForm connectedCallback is called.
document.body.removeChild(appForm);
// AppForm disconnectedCallback is called.
class AppForm extends HTMLElement {
constructor() {
super();
}
}
Don't access element/attributes because they may not be present before upgrade phase.
class AppForm extends HTMLElement {
initialLabel = 'default label';
connectedCallback() {
const label = this.initialLabel;
this.innerHTML = `<label>${label}</label>
<input/><button>Submit</button>`;
const button = this.querySelector('button');
button.addEventListener('click', this.btnClicked);
}
}
class AppForm extends HTMLElement {
disconnectedCallback() {
// clean up code
const button = this.querySelector('button');
button.removeEventListener('click', this.btnClicked);
}
}
Cleanup resource or event listeners
class AppForm extends HTMLElement {
static get observedAttributes() {
return ['label'];
}
attributeChangedCallback(name, oldValue, newValue) {
this.updateLabel(newValue);
}
updateLabel(newLabel) {
const label = this.querySelector('label');
if (label) {
label.textContent = newLabel;
} else {
this.initialLabel = newLabel;
}
}
}
class AppForm extends HTMLElement {
get label() {
return this.getAttribute('label');
}
set label(newLabel) {
this.setAttribute('label', newLabel);
}
}
<body>
<app-form label="my label"></app-form>
<script>
const appForm = document.querySelector('app-form');
// set by property
appForm.label = 'new label';
// set by attribute
appForm.setAttribute('label', 'new label');
</script>
</body>
class AppForm extends HTMLElement {
btnClicked() {
const customEvent = document.createEvent('CustomEvent');
customEvent.initCustomEvent('submitted', false, false,
this.querySelector('input').value);
this.dispatchEvent(customEvent);
}
}
<script>
appForm.addEventListener('submitted', (event) => {
console.log('input from custom element', event.detail);
});
</script>
<app-form label="my label"></app-form>
customElements.define('app-form', AppForm);
import { Component, OnInit, Input, Output } from '@angular/core';
@Component({
selector: 'app-app-form',
template: `
<label>{{ label }}</label> <input /><button>Submit</button>
`,
styleUrls: ['./app-form.component.css']
})
export class AppFormComponent implements OnInit {
@Input() label: string;
@Output() submitted = new EventEmitter();
constructor() {}
ngOnInit() {}
submit() { this.submitted.emit(...) }
}
class CustomElementsWrapper extends HTMLElement {
constructor() {
super();
}
static get observedAttributes() {
return [];
}
connectedCallback() {}
disconnectedCallback() {}
attributeChangedCallback(attrName: string,
oldVal: string, newVal: string) { }
}
export class CustomElementsWrapper {
static prepare(injector: Injector, component: any) {
const componentFactory = injector
.get(ComponentFactoryResolver)
.resolveComponentFactory(component);
componentFactory.inputs.forEach(input =>
observedAttributes.push(input.templateName));
}
}
export class AppFormComponent {
@Input('label1') label: string;
}
<app-form label1="new label"></app-form>
// componentFactory.inputs
[
{
propName: 'label',
templateName: 'label1'
}
]
export function dynamicLoadComponent(element: HTMLElement, injector: Injector) {
// create injector for this dynamic component
const childInjector = Injector.create({ providers: [], parent: injector });
// resolve componentFactory based on Component Type
const componentFactoryResolver = injector.get(ComponentFactoryResolver);
const componentFactory = componentFactoryResolver.
resolveComponentFactory(AppFormComponent);
// create component instance with injector, projectionNode and root element.
const componentRef = componentFactory.create(childInjector, [], element);
// trigger change detection to render component
componentRef.changeDetectorRef.detectChanges();
// add component view to applicationRef to enable future change detection
const applicationRef = injector.get<ApplicationRef>(ApplicationRef);
applicationRef.attachView(componentRef.hostView);
return componentRef;
}
export class CustomElementsWrapper {
initialInputValues = {};
connectedCallback() {
loadDynamicComponent(this, this.injector);
this.componentFactory.inputs.forEach(
prop => this.componentRef.instance[prop.propName] =
this.initialInputValues[prop.propName]);
}
attributeChangedCallback(propName: string, _: string,
value: string) {
// component not loaded yet
if (!this.componentRef) {
this.initialInputValues[propName] = value;
return;
}
// ...
}
}
class CustomElementsWrapper {
destroy() {
this.componentRef.destroy();
}
}
class CustomElementsWrapper {
attributeChangedCallback(propName: string, _: string,
value: string) {
if (!this.componentRef) {
this.initialInputValues[propName] = value;
return;
}
if (this.componentRef.instance[propName] === value) {
return;
}
this.componentRef.instance[propName] = value;
this.changeDetectorRef.detectChanges();
}
}
<app-form></app-form>
<script>
const appForm = document.querySelector('app-form');
appForm.setAttribute('label', 'newLabel');
</script>
class CustomElementsWrapper {
componentFactory.inputs.map(({ propName }) => propName)
.forEach(property => {
Object.defineProperty(CustomElementsWrapper.prototype, property, {
get: function() {
return this.componentRef[property];
},
set: function(newValue: any) {
this.componentRef[property] = newValue;
this.componentRef.changeDetectorRef.detectChanges();
},
configurable: true,
enumerable: true,
});
}
<app-form></app-form>
<script>
const appForm = document.querySelector('app-form');
appForm.label = 'newLabel';
</script>
<app-form></app-form>
<script>
appForm.addEventListener('submitted', (event) =>
{ console.log('custom element is submitted with input', event.detail); });
</script>
export class CustomElementsWrapper {
connectedCallback() {
const eventEmitters =
this.componentFactory.outputs.map(({propName, templateName}) => {
const emitter = (this.componentRef.instance as any)[propName] as
EventEmitter<any>;
emitter.subscribe((value: any) => {
const customEvent = document.createEvent('CustomEvent');
customEvent.initCustomEvent(propName, false, false, value);
this.dispatchEvent(customEvent);
});
});
});
}
}
@NgModule({
declarations: [AppFormComponent],
imports: [BrowserModule],
entryComponents: [AppFormComponent],
providers: [],
bootstrap: []
})
export class AppModule {
constructor(private injector: Injector) {}
ngDoBootstrap() {
CustomElementsWrapper.prepare(this.injector, AppFormComponent);
customElements.define('app-form', CustomElementsWrapper);
}
}
isolated DOM/Scoped CSS
class AppForm extends HTMLElement {
constructor() {
this.shadowRoot = this.attachShadow({mode: 'open'});
}
connectedCallback() {
// create a shadow root
this.shadowRoot.innerHTML = `<label>${label}</label>
<input/><button>Submit</button>`;
}
}
@Component({
...,
encapsulation: ViewEncapsulation.ShadowDom
})
export class AppFormComponent implements OnInit {
}
<app-form>
Projection
<div slot="named">Named slot projection</div>
</app-form>
@Component({
selector: 'app-app-form',
template: `
<label>{{ label }}</label> <input #input />
<button (click)="submit()">Submit</button><br />
Projection: <slot></slot>
named projection: <slot name="named"></slot>
`,
styleUrls: ['./app-form.component.css']
})
export class AppFormComponent implements OnInit {
}
import { ɵrenderComponent as renderComponent,
ɵdetectChanges as detectChanges} from '@angular/core';
export class AppFormElement extends HTMLElement {
set label(newLabel: string) {
...
this.comp.label = newLabel;
detectChanges(this.comp);
}
connectedCallback() {
this.comp = renderComponent(AppFormComponent, { host: this });
...
}
}
import { enableProdMode } from '@angular/core';
import { environment } from './environments/environment';
import { AppFormElement } from './app/app-form/app-form-element';
if (environment.production) {
enableProdMode();
}
customElements.define('app-form', AppFormElement);
class AppModule {
ngDoBootstrap() {
// all callbacks will be in the zone
// which is the same when define
customElements.define('app-form', CustomElementsWrapper);
}
}
class CustomElementsWrapper extends HTMLElement {
connectedCallback() {
// don't need this line any more
// this.componentRef.changeDetectorRef.detectChanges();
}
attributeChangedCallback() {
// don't need this line any more
// this.componentRef.changeDetectorRef.detectChanges();
}
}
Web App
JQuery
React
Angular Elements
Zone.js
Zone.js
Zone.js
1. https://github.com/JiaLiPassion/ng-japan-custom-elements-angular
2. https://github.com/JiaLiPassion/ivy-custom-elements