Google DevFest 2021 成都站
Web 组件
服务端渲染
Template to Shadow DOM
<host-element>
<template shadowroot="open">
<slot></slot>
</template>
<h2>Light content</h2>
</host-element>
<host-element>
#shadow-root (open)
<slot>
↳
<h2>Light content</h2>
</slot>
</host-element>
Shadow DOM to Template
const html = element.getInnerHTML({
includeShadowRoots: true,
closedRoots: [shadowRoot1, shadowRoot2]
});
console.log(html);
组件初始化
<menu-toggle>
<template shadowroot="open">
<button>
<slot></slot>
</button>
</template>
Open Menu
</menu-toggle>
class MenuToggle extends HTMLElement {
constructor() {
var button = super().shadowRoot?.firstElementChild;
if (!button) {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `<button><slot></slot></button>`;
button = shadow.firstChild;
}
button.addEventListener('click', toggle);
}
}
customElements.define('menu-toggle', MenuToggle);
私有 Shadow DOM
class MenuToggle extends HTMLElement {
constructor() {
var { shadowRoot } = super().attachInternals();
if (!shadowRoot) {
shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `<button><slot></slot></button>`;
}
shadowRoot.firstChild.addEventListener('click', toggle);
}
}
customElements.define('menu-toggle', MenuToggle);
仅限于解析器 —— 为了安全
const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowroot', 'open'); // 啥也没做
div.appendChild(template);
console.log(div.shadowRoot); // null
const html = `
<div>
<template shadowroot="open"></template>
</div>
`;
const div = document.createElement('div');
div.innerHTML = html;
console.log(div.shadowRoot); // null
const document = new DOMParser().parseFromString(html, 'text/html', {
includeShadowRoots: true
});
const { shadowRoot } = document.querySelector('div');
console.log(shadowRoot); // # DocumentFragment
CSS 渲染
<nineties-button>
<template shadowroot="open">
<style>
button {
color: seagreen;
}
</style>
<link rel="stylesheet" href="/comicsans.css" />
<button>
<slot></slot>
</button>
</template>
I'm Blue
</nineties-button>
Polyfill 补丁
for (const template of document.querySelectorAll('template[shadowroot]')) {
const mode = template.getAttribute('shadowroot');
const shadowRoot = template.parentNode.attachShadow({ mode });
shadowRoot.append(template.content);
template.remove();
}
数据绑定(难产)
<template>
<section>
<h1>{{name}}</h1>
Email: <a href="mailto:{{email}}">{{email}}</a>
</section>
</template>
const template = document.currentScript.previousElementSibling;
const tree = template.createInstance({
name: 'Ryosuke Niwa',
email: '[email protected]'
});
document.body.prepend(tree);
tree.update({ name: 'Ryosuke Niwa', email: '[email protected]' });
表单
Event Submitter
https://github.com/idea2app/event-submitter-polyfill
HTML
<form>
<input name="data" />
<button type="submit" data-name="first">Fisrt</button>
<button type="submit" data-name="second">Second</button>
</form>
document.querySelector('form')?.addEventListener('submit', event => {
event.preventDefault();
const { name } = event.submitter.dataset,
{ data } = event.target.elements;
fetch(`/api/${name}`, { data: data.value });
});
React
import React, { FormEvent } from 'react';
import { render } from 'react-dom';
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const { name } = event.nativeEvent.submitter.dataset,
{ data } = event.currentTarget.elements;
fetch(`/api/${name}`, { data: data.value });
}
render(
<form onSubmit={handleSubmit}>
<input name="data" />
<button type="submit" data-name="first">
Fisrt
</button>
<button type="submit" data-name="second">
Second
</button>
</form>,
document.body
);
FormData 事件
const form = document.querySelector('form');
form?.addEventListener('formdata', ({ formData }) => {
formData.append('my-input', myInputValue);
});
Web 表单组件
Element Internals
class MyCounter extends HTMLElement {
static formAssociated = true;
private value_ = 0;
private internals_ = this.attachInternals();
get value() {
return this.value_;
}
set value(v) {
this.value_ = v;
}
get form() {
return this.internals_.form;
}
get name() {
return this.getAttribute('name');
}
get type() {
return this.localName;
}
get validity() {
return this.internals_.validity;
}
get validationMessage() {
return this.internals_.validationMessage;
}
get willValidate() {
return this.internals_.willValidate;
}
checkValidity() {
return this.internals_.checkValidity();
}
reportValidity() {
return this.internals_.reportValidity();
}
}
customElements.define('my-counter', MyCounter);
HTML
<template>
<div contenteditable="true"></div>
</template>
const { content } = document.currentScript.previousElementSibling;
class MyInput extends HTMLInputElement {
constructor() {
super().attachShadow({ mode: 'open' }).append(content.cloneNode(true));
}
}
customElements.define('my-input', MyInput, { extends: 'input' });
<input is="my-input" />
WebCell
import { component, mixinForm } from 'web-cell';
@component({
tagName: 'my-counter',
renderTarget: 'shadowRoot'
})
export class MyCounter extends mixinForm() {
render() {
return <div contenteditable="true" />;
}
}