Compare commits
No commits in common. "bceee7ecf1dfabe6bcc178e60c4beeefe931a60d" and "9ebc2ee5fe841bca531bf0eacb79fd2d3e64258e" have entirely different histories.
bceee7ecf1
...
9ebc2ee5fe
4 changed files with 56 additions and 108 deletions
134
README.md
134
README.md
|
|
@ -1,87 +1,11 @@
|
||||||
# zulip
|
# zulip
|
||||||
|
|
||||||
A modern, no-build web application using Lit web components with Zustand state management.
|
A minimal, no-build web app kit using only Lit-based web components via importmaps.
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
* **Lit-based web components** - Standards-based, framework-agnostic UI
|
## Quick Start Example
|
||||||
* **No-build tooling** - Import maps only, no bundler required
|
|
||||||
* **Zustand state management** - Lightweight, vanilla JS store
|
|
||||||
* **StoreController** - Reactive controller that bridges Zustand and Lit
|
|
||||||
|
|
||||||
## Project Structure
|
### 1. Define a Component
|
||||||
|
|
||||||
```
|
|
||||||
components/ -> standard lit components, including top-level "app-root"
|
|
||||||
controllers/ -> lit reactive controllers (such as Zustand<->Lit bridge)
|
|
||||||
store/ -> defines specific accessor sets
|
|
||||||
index.html -> entry point with inline importmap and app-root
|
|
||||||
README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Data Flow Diagram
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant User
|
|
||||||
participant LitComponent
|
|
||||||
participant StoreController
|
|
||||||
participant ZustandStore
|
|
||||||
|
|
||||||
User->>LitComponent: Click button
|
|
||||||
LitComponent->>ZustandStore: Call action (e.g., addItem)
|
|
||||||
ZustandStore->>ZustandStore: Update state
|
|
||||||
ZustandStore->>StoreController: Notify subscribers
|
|
||||||
StoreController->>LitComponent: requestUpdate()
|
|
||||||
LitComponent->>LitComponent: Re-render
|
|
||||||
LitComponent->>User: Show updated UI
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
### 1. Define Store (Zustand Vanilla)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// store/index.js
|
|
||||||
import { createStore } from 'zustand/vanilla'
|
|
||||||
|
|
||||||
export const store = createStore((set, get) => ({
|
|
||||||
count: 0,
|
|
||||||
increment: () => set(s => ({ count: s.count + 1 })),
|
|
||||||
decrement: () => set(s => ({ count: s.count - 1 })),
|
|
||||||
}))
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Create StoreController (Bridge)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// controllers/store.js
|
|
||||||
export class StoreController {
|
|
||||||
constructor(host, store, selector) {
|
|
||||||
this.host = host
|
|
||||||
this.store = store
|
|
||||||
this.selector = selector
|
|
||||||
host.addController(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostConnected() {
|
|
||||||
this._unsub = this.store.subscribe((state) => {
|
|
||||||
const next = this.selector(state)
|
|
||||||
if (next !== this.value) {
|
|
||||||
this.value = next
|
|
||||||
this.host.requestUpdate()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.value = this.selector(this.store.getState())
|
|
||||||
}
|
|
||||||
|
|
||||||
hostDisconnected() {
|
|
||||||
this._unsub?.()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Create Component (Lit + StoreController)
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// components/counter.js
|
// components/counter.js
|
||||||
|
|
@ -98,15 +22,16 @@ class Counter extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
button { padding: 1rem; font-size: 1.2rem; margin: 0.5rem; }
|
button { padding: 1rem; font-size: 1.2rem; }
|
||||||
`
|
`
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<p>Count: ${this.#count.value}</p>
|
<p>Count: ${this.count}</p>
|
||||||
<button @click=${() => store.getState().decrement()}>-</button>
|
<button @click=${() => this.count++}>
|
||||||
<button @click=${() => store.getState().increment()}>+</button>
|
Increment
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
@ -115,24 +40,53 @@ class Counter extends LitElement {
|
||||||
customElements.define('my-counter', Counter)
|
customElements.define('my-counter', Counter)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Instantiate in parent (such as app-root)
|
### 2. Use in App root (or other composition node)
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import counter from "./counter.js":
|
import "./my-counter.js";
|
||||||
|
|
||||||
class AppRoot extends LitElement {
|
class AppRoot extends LitElement {
|
||||||
|
...
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`<my-counter .count=${0}></my-counter>`;
|
||||||
<counter></counter>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 3. Define Data Flows
|
||||||
|
|
||||||
|
#### Parent-to-Child (Props)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Parent passes data down
|
||||||
|
html`<child-component .items=${this.items}></child-component>`
|
||||||
|
|
||||||
|
// Child receives via properties
|
||||||
|
static properties = {
|
||||||
|
items: { type: Array }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Child-to-Parent (Events)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Child emits event
|
||||||
|
this.dispatchEvent(new CustomEvent('add-item', {
|
||||||
|
detail: { item: newItem },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Parent listens
|
||||||
|
html`<child-component @add-item=${(e) => this.handleAdd(e.detail.item)}></child-component>`
|
||||||
|
```
|
||||||
|
|
||||||
## Benefits Over TSX
|
## Benefits Over TSX
|
||||||
|
|
||||||
1. **No build step** - Edit and refresh, instant feedback
|
1. **No build step** - Edit and refresh, instant feedback
|
||||||
|
|
||||||
|
1. **Minimal dependencies** - Just Lit, nothing else
|
||||||
|
|
||||||
1. **Smaller bundle** - No framework runtime, just standards
|
1. **Smaller bundle** - No framework runtime, just standards
|
||||||
|
|
||||||
1. **Better encapsulation** - Shadow DOM, scoped styles
|
1. **Better encapsulation** - Shadow DOM, scoped styles
|
||||||
|
|
@ -141,6 +95,4 @@ class AppRoot extends LitElement {
|
||||||
|
|
||||||
1. **Future-proof** - Built on web standards
|
1. **Future-proof** - Built on web standards
|
||||||
|
|
||||||
1. **Familiar patterns** - Zustand works like Redux/Context
|
1. **Easy to learn** - Simple mental model for state, with natural evolution into Zustand/IndexedDB or similar extensions
|
||||||
|
|
||||||
1. **Type-safe** - Can add JSDoc or TypeScript without build step
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,18 @@ import "./page-home.js";
|
||||||
import "./page-items.js";
|
import "./page-items.js";
|
||||||
|
|
||||||
class AppRoot extends LitElement {
|
class AppRoot extends LitElement {
|
||||||
#user = new StoreController(this, store, s => s.user);
|
static properties = {
|
||||||
#route = new StoreController(this, store, s => s.route);
|
user: { type: Object, state: true },
|
||||||
|
route: { type: String, state: true },
|
||||||
|
items: { type: Array, state: true }
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.user = null;
|
||||||
|
this.route = 'home';
|
||||||
|
this.items = [];
|
||||||
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
"lit": "https://esm.sh/lit@3",
|
"lit": "https://esm.sh/lit@3",
|
||||||
"lit/decorators.js": "https://esm.sh/lit@3/decorators.js",
|
"lit/decorators.js": "https://esm.sh/lit@3/decorators.js"
|
||||||
"zustand/vanilla": "https://esm.sh/zustand@5/vanilla"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { createStore } from 'zustand/vanilla'
|
|
||||||
|
|
||||||
export const store = createStore((set, get) => ({
|
|
||||||
user: null,
|
|
||||||
items: [],
|
|
||||||
route: 'home',
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
setUser: (user) => set({ user }),
|
|
||||||
addItem: (item) => set(s => ({ items: [...s.items, item] })),
|
|
||||||
removeItem: (id) => set(s => ({ items: s.items.filter(i => i.id !== id) })),
|
|
||||||
navigate: (route) => set({ route }),
|
|
||||||
}))
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue