简体中文 | English
Web Components engine based on VDOM, JSX, MobX & TypeScript
feature | WebCell 3 | WebCell 2 | React | Vue |
---|---|---|---|---|
JS language | TypeScript 5 | TypeScript 4 | ECMAScript or TypeScript | ECMAScript or TypeScript |
JS syntax | ES decorator stage-3 | ES decorator stage-2 | ||
XML syntax | JSX import | JSX factory | JSX factory/import | HTML/Vue template or JSX (optional) |
DOM API | Web components | Web components | HTML 5+ | HTML 5+ |
view renderer | DOM Renderer 2 | SnabbDOM | (built-in) | SnabbDOM (forked) |
state API | MobX @observable |
this.state |
this.state or useState() |
this.$data or ref() |
props API | MobX @observable |
@watch |
this.props or props => {} |
this.$props or defineProps() |
state manager | MobX 6+ | MobX 4/5 | Redux | VueX |
page router | JSX tags | JSX tags + JSON data | JSX tags | JSON data |
asset bundler | Parcel 2 | Parcel 1 | webpack | Vite |
npm install dom-renderer mobx web-cell
npm install parcel @parcel/config-default @parcel/transformer-typescript-tsc -D
package.json
{
"scripts": {
"start": "parcel source/index.html --open",
"build": "parcel build source/index.html --public-url ."
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES6",
"module": "ES2020",
"moduleResolution": "Node",
"useDefineForClassFields": true,
"jsx": "react-jsx",
"jsxImportSource": "dom-renderer"
}
}
.parcelrc
{
"extends": "@parcel/config-default",
"transformers": {
"*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"]
}
}
source/index.html
<script src="https://polyfill.web-cell.dev/feature/ECMAScript.js"></script>
<script src="https://polyfill.web-cell.dev/feature/WebComponents.js"></script>
<script src="https://polyfill.web-cell.dev/feature/ElementInternals.js"></script>
<script src="source/MyTag.tsx"></script>
<my-tag></my-tag>
import { DOMRenderer } from 'dom-renderer';
import { FC, PropsWithChildren } from 'web-cell';
const Hello: FC<PropsWithChildren> = ({ children = 'World' }) => (
<h1>Hello, {children}!</h1>
);
new DOMRenderer().render(<Hello>WebCell</Hello>);
import { DOMRenderer } from 'dom-renderer';
import { component } from 'web-cell';
@component({
tagName: 'hello-world',
mode: 'open'
})
class Hello extends HTMLElement {
render() {
return (
<h1>
Hello, <slot />!
</h1>
);
}
}
new DOMRenderer().render(
<>
<Hello>WebCell</Hello>
{/* or */}
<hello-world>WebCell</hello-world>
</>
);
import { DOMRenderer } from 'dom-renderer';
import { observable } from 'mobx';
import { WebCell, component, attribute, observer } from 'web-cell';
interface HelloProps {
name?: string;
}
interface Hello extends WebCell<HelloProps> {}
@component({ tagName: 'hello-world' })
@observer
class Hello extends HTMLElement implements WebCell<HelloProps> {
@attribute
@observable
accessor name = '';
render() {
return <h1>Hello, {this.name}!</h1>;
}
}
new DOMRenderer().render(<Hello name="WebCell" />);
// or for HTML tag props in TypeScript
declare global {
namespace JSX {
interface IntrinsicElements {
'hello-world': HelloProps;
}
}
}
new DOMRenderer().render(<hello-world name="WebCell" />);
import { DOMRenderer } from 'dom-renderer';
import { observable } from 'mobx';
import { FC, observer } from 'web-cell';
class CounterModel {
@observable
accessor times = 0;
}
const couterStore = new CounterModel();
const Counter: FC = observer(() => (
<button onClick={() => (couterStore.times += 1)}>
Counts: {couterStore.times}
</button>
));
new DOMRenderer().render(<Counter />);
import { DOMRenderer } from 'dom-renderer';
import { observable } from 'mobx';
import { component, observer } from 'web-cell';
@component({ tagName: 'my-counter' })
@observer
class Counter extends HTMLElement {
@observable
accessor times = 0;
handleClick = () => (this.times += 1);
render() {
return <button onClick={this.handleClick}>Counts: {this.times}</button>;
}
}
new DOMRenderer().render(<Counter />);
import { component } from 'web-cell';
import { stringifyCSS } from 'web-utility';
@component({
tagName: 'my-button',
mode: 'open'
})
export class MyButton extends HTMLElement {
style = stringifyCSS({
'.btn': {
color: 'white',
background: 'lightblue'
}
});
render() {
return (
<>
<style>{this.style}</style>
<a className="btn">
<slot />
</a>
</>
);
}
}
import { component } from 'web-cell';
@component({
tagName: 'my-button',
mode: 'open'
})
export class MyButton extends HTMLElement {
render() {
return (
<>
<link
rel="stylesheet"
href="https://unpkg.com/bootstrap@5.3.3/dist/css/bootstrap.min.css"
/>
<a className="btn">
<slot />
</a>
</>
);
}
}
scoped.css
.btn {
color: white;
background: lightblue;
}
MyButton.tsx
import { WebCell, component } from 'web-cell';
import styles from './scoped.css' assert { type: 'css' };
interface MyButton extends WebCell {}
@component({
tagName: 'my-button',
mode: 'open'
})
export class MyButton extends HTMLElement implements WebCell {
connectedCallback() {
this.root.adoptedStyleSheets = [styles];
}
render() {
return (
<a className="btn">
<slot />
</a>
);
}
}
import { component, on } from 'web-cell';
@component({ tagName: 'my-table' })
export class MyTable extends HTMLElement {
@on('click', ':host td > button')
handleEdit(event: MouseEvent, { dataset: { id } }: HTMLButtonElement) {
console.log(`editing row: ${id}`);
}
render() {
return (
<table>
<tr>
<td>1</td>
<td>A</td>
<td>
<button data-id="1">edit</button>
</td>
</tr>
<tr>
<td>2</td>
<td>B</td>
<td>
<button data-id="2">edit</button>
</td>
</tr>
<tr>
<td>3</td>
<td>C</td>
<td>
<button data-id="3">edit</button>
</td>
</tr>
</table>
);
}
}
import { observable } from 'mobx';
import { component, observer, reaction } from 'web-cell';
@component({ tagName: 'my-counter' })
@observer
export class Counter extends HTMLElement {
@observable
accessor times = 0;
handleClick = () => (this.times += 1);
@reaction(({ times }) => times)
echoTimes(newValue: number, oldValue: number) {
console.log(`newValue: ${newValue}, oldValue: ${oldValue}`);
}
render() {
return <button onClick={this.handleClick}>Counts: {this.times}</button>;
}
}
import { DOMRenderer } from 'dom-renderer';
import { WebField, component, formField, observer } from 'web-cell';
interface MyField extends WebField {}
@component({
tagName: 'my-field',
mode: 'open'
})
@formField
@observer
class MyField extends HTMLElement implements WebField {
render() {
const { name } = this;
return (
<input
name={name}
onChange={({ currentTarget: { value } }) =>
(this.value = value)
}
/>
);
}
}
new DOMRenderer().render(
<form method="POST" action="/api/data">
<MyField name="test" />
<button>submit</button>
</form>
);
AsyncTag.tsx
import { FC } from 'web-cell';
const AsyncTag: FC = () => <div>Async</div>;
export default AsyncTag;
index.tsx
import { DOMRenderer } from 'dom-renderer';
import { lazy } from 'web-cell';
const AsyncTag = lazy(() => import('./AsyncTag'));
new DOMRenderer().render(<AsyncTag />);
import { DOMRenderer } from 'dom-renderer';
new DOMRenderer().render(
<a>
<b>Async rendering</b>
</a>,
document.body,
'async'
);
import { component } from 'web-cell';
@component({
tagName: 'async-renderer',
renderMode: 'async'
})
export class AsyncRenderer extends HTMLElement {
render() {
return (
<a>
<b>Async rendering</b>
</a>
);
}
}
import { DOMRenderer } from 'dom-renderer';
import { AnimateCSS } from 'web-cell';
new DOMRenderer().render(
<AnimateCSS
type="fadeIn"
component={props => <h1 {...props}>Fade In</h1>}
/>
);
npm install jsdom
import 'web-cell/polyfill';
https://github.com/EasyWebApp/DOM-Renderer?tab=readme-ov-file#nodejs--bun
connectedCallback
disconnectedCallback
attributeChangedCallback
adoptedCallback
updatedCallback
mountedCallback
formAssociatedCallback
formDisabledCallback
formResetCallback
formStateRestoreCallback
We recommend these libraries to use with WebCell:
State management: MobX (also powered by TypeScript & Decorator)
Router: Cell Router
UI components
HTTP request: KoAJAX (based on Koa-like middlewares)
Utility: Web utility methods & types
Event stream: Iterable Observer (Observable
proposal)
MarkDown integration: Parcel MDX transformer (MDX Compiler plugin)