diff --git a/static/bun.lockb b/static/bun.lockb new file mode 100755 index 0000000..70622ae Binary files /dev/null and b/static/bun.lockb differ diff --git a/static/node_modules/bloatless-react/README.md b/static/node_modules/bloatless-react/README.md index c1f3772..5630017 100644 --- a/static/node_modules/bloatless-react/README.md +++ b/static/node_modules/bloatless-react/README.md @@ -7,7 +7,7 @@ Bloatless-React is a very minimal and flexible alternative to React. - Supports reactivity through States - Supports JSX for components - Written in TypeScript -- Really minimal (under 200 lines) +- Really minimal (under 300 lines) - No bloated server or compiler required - just bundle the JavaScript into a static .html file # Setup @@ -70,7 +70,7 @@ summary.subscribe(console.log); ## ListStates -A ListState\ is a State whose value is a Set\. A ListState allows specific subscriptions to detect when items get added and removed. This allows dynamic lists to run efficiently. +A `ListState` is a State whose value is a `Set`. A ListState allows specific subscriptions to detect when items get added and removed. This allows dynamic lists to run efficiently. ```TypeScript // Import @@ -78,7 +78,7 @@ import * as React from 'bloatless-react'; // Define Item type class Item implements React.Identifiable { - uuid = new React.UUID(); + id = React.UUID(); constructor(public text: string) {} } @@ -99,6 +99,17 @@ listState.add(newItem); listState.remove(newItem); ``` +## Persistence + +States can persist through reloads via LocalStorage. To implement this, modify your code like this: +```diff +-const myState = new React.State("hello"); ++const myState = React.restoreState("my-state", "hello"); + +-const myListState = new React.ListState(); ++const myListState = React.restoreListState("my-list-state"); +``` + # UI For a minimalist stylesheet, I'd recommend checking out my project called [carbon-mini](https://github.com/marlon-erler/carbon-mini/). @@ -107,28 +118,38 @@ Bloatless React provides a modified polyfill for the React API. This means that ## Handling Events -The `on:` directive adds an EventListener +The `on:` directive adds an EventListener. This directive also supports the `on:enter` event. ```TypeScript + ``` ## Changing Properties -The `subscribe:` directive subscribes to a State and changes the element's property. The property will be changed through the DOM API, so you can use innerText and innerHTML. You can not use this to set attributes. +The `subscribe:` directive subscribes to a State and changes a property of the element. Use this for properties available via the DOM model (ie. innerText, innerHTML). ```TypeScript const name = new React.State("John Doe"); ``` +## Setting Attributes + +The `set:` directive subscribes to a State and changes an attribute of the element. + +```TypeScript + +``` + ## Toggling Attributes -The `toggle:` directive toggles attributes on the HTML element without assigning a value. This is useful for the `disabled` attribute. +The `toggle:` directive toggles attributes on the HTML element without assigning a value. This is useful for the `disabled` attribute. You can use states or normal variables. ```TypeScript const isDisabled = new React.State(false); + ``` ## Binding Input Values @@ -151,14 +172,16 @@ import * as React from 'bloatless-react'; // Define Item class Item implements React.Identifiable { - uuid = new React.UUID(); + id = React.UUID(); constructor(public text: string) {} } // This ListItemConverter creates an HTML element based on an Item -const convertItem: React.ListItemConverter = (item, listState) => { +const convertItem: React.ListItemConverter = (item) => { function remove() { - listState.remove(item); + // you should keep track of your data in a separate model + // keep your removeListItem() method there + myDataModel.removeListItem(item); } return ( @@ -204,4 +227,30 @@ document.body.append( - Improve documentation ## 1.1.3 -- Add `on:enter` directive \ No newline at end of file +- Add `on:enter` directive + +## 1.2.0 +**BREAKING CHANGES** +- Replace UUID class with function; + - `new UUID()` => `UUID() returns string` +- Replace definition of `Identifiable` + - now has `id: string` instead of `uuid: UUID()` + +Other changes: +- Add State persistence +- Add error description when utilizing `subscribe:children` incorrectly +- Fix bug where ListState subscriptions were not called +- Improve documentation + +## 1.2.1 +- Add `set:` directive + +## 1.2.2 +- Remove list parameter from `ListItemConverter` +- Add `clear()` method to `ListState` + +## 1.2.3 +- Allow `toggle:` to be used without a state + +## 1.2.4 +- On `subscribe:children`, scroll new elements into view \ No newline at end of file diff --git a/static/node_modules/bloatless-react/index.ts b/static/node_modules/bloatless-react/index.ts index f96c379..76f5ca0 100755 --- a/static/node_modules/bloatless-react/index.ts +++ b/static/node_modules/bloatless-react/index.ts @@ -5,20 +5,16 @@ import { v4 } from "uuid"; */ /* Types */ export interface Identifiable { - uuid: UUID; + id: string; +} + +export interface Stringifiable { + toString: () => string; } /* Utility */ -export class UUID { - readonly value: string; - - constructor() { - this.value = v4(); - } - - toString(): string { - return this.value; - } +export function UUID(): string { + return v4(); } /* State */ @@ -30,10 +26,12 @@ export class State { private _value: T; private _bindings = new Set>(); + // init constructor(initialValue: T) { this._value = initialValue; } + // value get value(): T { return this._value; } @@ -44,6 +42,7 @@ export class State { this.callSubscriptions(); } + // subscriptions callSubscriptions(): void { this._bindings.forEach((fn) => fn(this._value)); } @@ -52,43 +51,66 @@ export class State { this._bindings.add(fn); fn(this._value); } + + // stringify + toString(): string { + return JSON.stringify(this._value); + } } export class ListState extends State> { private additionHandlers = new Set>(); - private removalHandlers = new Map>(); + private removalHandlers = new Map>(); - constructor() { - super(new Set()); + // init + constructor(initialItems?: T[]) { + super(new Set(initialItems)); } + // list add(...items: T[]): void { items.forEach((item) => { this.value.add(item); this.additionHandlers.forEach((handler) => handler(item)); }); + this.callSubscriptions(); } remove(...items: T[]): void { items.forEach((item) => { this.value.delete(item); - const uuid = item.uuid; + const id = item.id; - if (!this.removalHandlers.has(uuid)) return; - this.removalHandlers.get(uuid)!(item); - this.removalHandlers.delete(uuid); + if (!this.removalHandlers.has(id)) return; + this.removalHandlers.get(id)!(item); + this.removalHandlers.delete(id); }); + this.callSubscriptions(); } + clear() { + this.remove(...this.value.values()); + } + + // handlers handleAddition(handler: AdditionSubscription): void { this.additionHandlers.add(handler); + [...this.value.values()].forEach(handler); } handleRemoval(item: T, handler: RemovalSubscription): void { - this.removalHandlers.set(item.uuid, handler); + this.removalHandlers.set(item.id, handler); + } + + // stringification + toString(): string { + const array = [...this.value]; + const json = JSON.stringify(array); + return json; } } +// UTILITY export function createProxyState( statesToSubscibe: State[], fn: () => T @@ -100,9 +122,49 @@ export function createProxyState( return proxyState; } +function persistState(localStorageKey: string, state: State) { + state.subscribe(() => { + const stringifiedValue = state.toString(); + localStorage.setItem(localStorageKey, stringifiedValue); + }); +} + +export function restoreState( + localStorageKey: string, + initialStateValue: T +): State { + const storedString = + localStorage.getItem(localStorageKey) ?? + JSON.stringify(initialStateValue); + const convertedValue = JSON.parse(storedString); + + const state = new State(convertedValue); + persistState(localStorageKey, state); + + return state; +} + +export function restoreListState( + localStorageKey: string +): ListState { + const storedString = localStorage.getItem(localStorageKey) ?? ""; + + let initialItems: any[] = []; + + try { + const array = JSON.parse(storedString); + if (!Array.isArray(array)) throw ""; + initialItems = array; + } catch {} + + const state = new ListState(initialItems); + persistState(localStorageKey, state); + + return state; +} + export type ListItemConverter = ( - item: T, - listState: ListState + item: T ) => HTMLElement; /* @@ -139,20 +201,27 @@ export function createElement( } case "subscribe": { if (directiveValue == "children") { - const [listState, toElement] = value as [ - listState: ListState, - ( - item: any, - listState: ListState - ) => HTMLElement - ]; - listState.handleAddition((newItem) => { - const child = toElement(newItem, listState); - listState.handleRemoval(newItem, () => - child.remove() - ); - element.append(child); - }); + element.style.scrollBehavior = "smooth"; + try { + const [listState, toElement] = value as [ + listState: ListState, + ( + item: any, + listState: ListState + ) => HTMLElement + ]; + + listState.handleAddition((newItem) => { + const child = toElement(newItem, listState); + listState.handleRemoval(newItem, () => + child.remove() + ); + element.append(child); + child.scrollIntoView(); + }); + } catch { + throw `error: cannot process subscribe:children directive because ListItemConverter is not defined. Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation`; + } } else { const state = value as State; state.subscribe( @@ -173,9 +242,20 @@ export function createElement( break; } case "toggle": { + if (value.subscribe) { + const state = value as State; + state.subscribe((newValue) => + element.toggleAttribute(directiveValue, newValue) + ); + } else { + element.toggleAttribute(directiveValue, value); + } + break; + } + case "set": { const state = value as State; state.subscribe((newValue) => - element.toggleAttribute(directiveValue, newValue) + element.setAttribute(directiveValue, newValue) ); break; } diff --git a/static/node_modules/bloatless-react/package.json b/static/node_modules/bloatless-react/package.json index b86d958..2746ccd 100644 --- a/static/node_modules/bloatless-react/package.json +++ b/static/node_modules/bloatless-react/package.json @@ -1,6 +1,6 @@ { "name": "bloatless-react", - "version": "1.1.3", + "version": "1.2.4", "description": "very minimal and flexible react alternative", "repository": { "type": "git", diff --git a/static/package.json b/static/package.json index 9fc3e21..845797b 100644 --- a/static/package.json +++ b/static/package.json @@ -1,9 +1,9 @@ { - "dependencies": { - "bloatless-react": "^1.1.3", - "esbuild": "latest" - }, - "scripts": { - "build": "esbuild src/index.tsx --bundle --outdir=dist" - } + "dependencies": { + "bloatless-react": "^1.2.4", + "esbuild": "latest" + }, + "scripts": { + "build": "esbuild src/index.tsx --bundle --outdir=dist" + } }