Package Exports
- web-cell
- web-cell/dist/index.esm.js
- web-cell/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (web-cell) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
WebCell

简体中文 | English
Web Components engine based on VDOM, JSX, MobX & TypeScript
Feature
Engines comparison
| 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 |
Installation
npm install dom-renderer mobx web-cellWeb browser usage
Project bootstrap
Tool chain
npm install parcel @parcel/config-default @parcel/transformer-typescript-tsc -Dpackage.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>Function component
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>);Class component
Children slot
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>
</>
);DOM Props
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" />);Inner state
Function component
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 />);Class component
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 />);CSS scope
Inline style
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>
</>
);
}
}Link stylesheet
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.6/dist/css/bootstrap.min.css"
/>
<a className="btn">
<slot />
</a>
</>
);
}
}CSS module
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>
);
}
}Event delegation
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>
);
}
}MobX reaction
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>;
}
}Form association
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>
);Async component
import { DOMRenderer } from 'dom-renderer';
import { observer, PropsWithChildren } from 'web-cell';
import { sleep } from 'web-utility';
const AsyncComponent = observer(async ({ children }: PropsWithChildren) => {
await sleep(1);
return <p>Async Component in {children}</p>;
});
new DOMRenderer().render(<AsyncComponent>WebCell</AsyncComponent>);Async loading
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 />);Async rendering (experimental)
DOM tree
import { DOMRenderer } from 'dom-renderer';
new DOMRenderer().render(
<a>
<b>Async rendering</b>
</a>,
document.body,
'async'
);Class component
import { component } from 'web-cell';
@component({
tagName: 'async-renderer',
renderMode: 'async'
})
export class AsyncRenderer extends HTMLElement {
render() {
return (
<a>
<b>Async rendering</b>
</a>
);
}
}Animate CSS component
import { DOMRenderer } from 'dom-renderer';
import { AnimateCSS } from 'web-cell';
new DOMRenderer().render(
<AnimateCSS type="fadeIn" component={props => <h1 {...props}>Fade In</h1>} />
);Node.js usage
Tool chain
npm install jsdomPolyfill
import 'web-cell/polyfill';Server Side Rendering
https://github.com/EasyWebApp/DOM-Renderer?tab=readme-ov-file#nodejs--bun
Basic knowledge
- Web components
- Custom elements
- Shadow DOM
- Element Internals
- CSS variables
- View transitions
- ECMAScript 6+
- TypeScript 5+
Life Cycle hooks
connectedCallbackdisconnectedCallbackattributeChangedCallbackadoptedCallbackupdatedCallbackmountedCallbackformAssociatedCallbackformDisabledCallbackformResetCallbackformStateRestoreCallback
Scaffolds
Ecosystem
We recommend these libraries to use with WebCell:
State management: MobX (also powered by TypeScript & Decorator)
Router: Cell Router
UI components
- BootCell (based on BootStrap v5)
- MDUI (based on Material Design v3)
- GitHub Web Widget
HTTP request: KoAJAX (based on Koa-like middlewares)
Utility: Web utility methods & types
Event stream: Iterable Observer (
Observableproposal)MarkDown integration: Parcel MDX transformer (MDX Compiler plugin)
