fixing merge conflicts
This commit is contained in:
commit
bceee7ecf1
3 changed files with 95 additions and 56 deletions
134
README.md
134
README.md
|
|
@ -1,11 +1,87 @@
|
||||||
# zulip
|
# zulip
|
||||||
|
|
||||||
A minimal, no-build web app kit using only Lit-based web components via importmaps.
|
A modern, no-build web application using Lit web components with Zustand state management.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
## Quick Start Example
|
* **Lit-based web components** - Standards-based, framework-agnostic UI
|
||||||
|
* **No-build tooling** - Import maps only, no bundler required
|
||||||
|
* **Zustand state management** - Lightweight, vanilla JS store
|
||||||
|
* **StoreController** - Reactive controller that bridges Zustand and Lit
|
||||||
|
|
||||||
### 1. Define a Component
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
|
@ -22,16 +98,15 @@ class Counter extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
button { padding: 1rem; font-size: 1.2rem; }
|
button { padding: 1rem; font-size: 1.2rem; margin: 0.5rem; }
|
||||||
`
|
`
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<p>Count: ${this.count}</p>
|
<p>Count: ${this.#count.value}</p>
|
||||||
<button @click=${() => this.count++}>
|
<button @click=${() => store.getState().decrement()}>-</button>
|
||||||
Increment
|
<button @click=${() => store.getState().increment()}>+</button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
@ -40,53 +115,24 @@ class Counter extends LitElement {
|
||||||
customElements.define('my-counter', Counter)
|
customElements.define('my-counter', Counter)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Use in App root (or other composition node)
|
### 4. Instantiate in parent (such as app-root)
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import "./my-counter.js";
|
import counter from "./counter.js":
|
||||||
|
|
||||||
class AppRoot extends LitElement {
|
class AppRoot extends LitElement {
|
||||||
...
|
|
||||||
render() {
|
render() {
|
||||||
return html`<my-counter .count=${0}></my-counter>`;
|
return html`
|
||||||
|
<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
|
||||||
|
|
@ -95,4 +141,6 @@ html`<child-component @add-item=${(e) => this.handleAdd(e.detail.item)}></child-
|
||||||
|
|
||||||
1. **Future-proof** - Built on web standards
|
1. **Future-proof** - Built on web standards
|
||||||
|
|
||||||
1. **Easy to learn** - Simple mental model for state, with natural evolution into Zustand/IndexedDB or similar extensions
|
1. **Familiar patterns** - Zustand works like Redux/Context
|
||||||
|
|
||||||
|
1. **Type-safe** - Can add JSDoc or TypeScript without build step
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,8 @@ import "./page-home.js";
|
||||||
import "./page-items.js";
|
import "./page-items.js";
|
||||||
|
|
||||||
class AppRoot extends LitElement {
|
class AppRoot extends LitElement {
|
||||||
static properties = {
|
#user = new StoreController(this, store, s => s.user);
|
||||||
user: { type: Object, state: true },
|
#route = new StoreController(this, store, s => s.route);
|
||||||
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,7 +8,8 @@
|
||||||
{
|
{
|
||||||
"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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue