WebCell

WebCell

WebCell logo

简体中文 | English

Web Components engine based on VDOM, JSX, MobX & TypeScript

NPM Dependency CI & CD

Anti 996 license UI library recommendation list

Slideshow Gitter

Edit WebCell demo

NPM

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

Demo & GitHub template

npm install parcel @parcel/config-default @parcel/transformer-typescript-tsc -D
{
"scripts": {
"start": "parcel source/index.html --open",
"build": "parcel build source/index.html --public-url ."
}
}
{
"compilerOptions": {
"target": "ES6",
"module": "ES2020",
"moduleResolution": "Node",
"useDefineForClassFields": true,
"jsx": "react-jsx",
"jsxImportSource": "dom-renderer"
}
}
{
"extends": "@parcel/config-default",
"transformers": {
"*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"]
}
}
<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/[email protected]/dist/css/bootstrap.min.css"
/>
<a className="btn">
<slot />
</a>
</>
);
}
}
.btn {
color: white;
background: lightblue;
}
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>
);
import { FC } from 'web-cell';

const AsyncTag: FC = () => <div>Async</div>;

export default AsyncTag;
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

  1. connectedCallback
  2. disconnectedCallback
  3. attributeChangedCallback
  4. adoptedCallback
  5. updatedCallback
  6. mountedCallback
  7. formAssociatedCallback
  8. formDisabledCallback
  9. formResetCallback
  10. formStateRestoreCallback
  1. Basic
  2. DashBoard
  3. Mobile
  4. Static site

We recommend these libraries to use with WebCell:

  1. Development contribution