This commit is contained in:
marlon-erler
2024-07-08 13:06:40 +02:00
parent 5669ed077c
commit 8a968ee212
5 changed files with 183 additions and 54 deletions
BIN
View File
Binary file not shown.
+58 -9
View File
@@ -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\<T\> is a State whose value is a Set\<T\>. A ListState allows specific subscriptions to detect when items get added and removed. This allows dynamic lists to run efficiently.
A `ListState<T>` is a State whose value is a `Set<T>`. 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<Item>();
+const myListState = React.restoreListState<Item>("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:<event>` directive adds an EventListener
The `on:<event>` directive adds an EventListener. This directive also supports the `on:enter` event.
```TypeScript
<input on:enter={someFunction}></input>
<button on:click={someFunction}>Click me</button>
```
## Changing Properties
The `subscribe:<property>` 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:<property>` 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");
<span subscribe:innerText={name}></span>
```
## Setting Attributes
The `set:<attribute>` directive subscribes to a State and changes an attribute of the element.
```TypeScript
<span set:someAttribute={attributeValue}></span>
```
## Toggling Attributes
The `toggle:<attribute>` directive toggles attributes on the HTML element without assigning a value. This is useful for the `disabled` attribute.
The `toggle:<attribute>` 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);
<button toggle:disabled={isDisabled}>Some button</button>
<button toggle:disabled={false}>Some button</button>
```
## 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> = (item, listState) => {
const convertItem: React.ListItemConverter<Item> = (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 (
<span>
@@ -205,3 +228,29 @@ document.body.append(
## 1.1.3
- 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:<attribute>` directive
## 1.2.2
- Remove list parameter from `ListItemConverter`
- Add `clear()` method to `ListState`
## 1.2.3
- Allow `toggle:<attribute>` to be used without a state
## 1.2.4
- On `subscribe:children`, scroll new elements into view
+101 -21
View File
@@ -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<T> {
private _value: T;
private _bindings = new Set<StateSubscription<T>>();
// init
constructor(initialValue: T) {
this._value = initialValue;
}
// value
get value(): T {
return this._value;
}
@@ -44,6 +42,7 @@ export class State<T> {
this.callSubscriptions();
}
// subscriptions
callSubscriptions(): void {
this._bindings.forEach((fn) => fn(this._value));
}
@@ -52,43 +51,66 @@ export class State<T> {
this._bindings.add(fn);
fn(this._value);
}
// stringify
toString(): string {
return JSON.stringify(this._value);
}
}
export class ListState<T extends Identifiable> extends State<Set<T>> {
private additionHandlers = new Set<AdditionSubscription<T>>();
private removalHandlers = new Map<UUID, RemovalSubscription<T>>();
private removalHandlers = new Map<string, RemovalSubscription<T>>();
constructor() {
super(new Set<T>());
// init
constructor(initialItems?: T[]) {
super(new Set<T>(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<T>): void {
this.additionHandlers.add(handler);
[...this.value.values()].forEach(handler);
}
handleRemoval(item: T, handler: RemovalSubscription<T>): 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<T>(
statesToSubscibe: State<any>[],
fn: () => T
@@ -100,9 +122,49 @@ export function createProxyState<T>(
return proxyState;
}
function persistState(localStorageKey: string, state: State<any>) {
state.subscribe(() => {
const stringifiedValue = state.toString();
localStorage.setItem(localStorageKey, stringifiedValue);
});
}
export function restoreState<T>(
localStorageKey: string,
initialStateValue: T
): State<T> {
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<T extends Identifiable>(
localStorageKey: string
): ListState<T> {
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<T>(initialItems);
persistState(localStorageKey, state);
return state;
}
export type ListItemConverter<T extends Identifiable> = (
item: T,
listState: ListState<T>
item: T
) => HTMLElement;
/*
@@ -139,6 +201,8 @@ export function createElement(
}
case "subscribe": {
if (directiveValue == "children") {
element.style.scrollBehavior = "smooth";
try {
const [listState, toElement] = value as [
listState: ListState<any>,
(
@@ -146,13 +210,18 @@ export function createElement(
listState: ListState<any>
) => 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<any>;
state.subscribe(
@@ -173,10 +242,21 @@ export function createElement(
break;
}
case "toggle": {
if (value.subscribe) {
const state = value as State<any>;
state.subscribe((newValue) =>
element.toggleAttribute(directiveValue, newValue)
);
} else {
element.toggleAttribute(directiveValue, value);
}
break;
}
case "set": {
const state = value as State<any>;
state.subscribe((newValue) =>
element.setAttribute(directiveValue, newValue)
);
break;
}
default:
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"dependencies": {
"bloatless-react": "^1.1.3",
"bloatless-react": "^1.2.4",
"esbuild": "latest"
},
"scripts": {