diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f17dcc --- /dev/null +++ b/README.md @@ -0,0 +1,145 @@ +# react-superaction + +Turn the virtual-dom into a declarative event-bus. + +(a port of [superaction](https://github.com/w-lfpup/superaction-js) for React) + +## Install + +Install via npm. + +```sh +npm install --save-dev @w-lfpup/react-superaction +``` + +Or install directly from github. + +```sh +npm install --save-dev https://github.com/w-lfpup/react-superaction +``` + +## Setup + +Add a `SuperActionProvider` component to broadcast action events. + +The `SuperActionProvider` component below listens for click events. React developers can access action events. + +```tsx +import React from "react"; +import ReactDOM from "react-dom/client"; +import { SuperActionProvider } from "@w-lfpup/react-superaction"; +import { Counter } from "./counter.js"; + +let rootEl = document.querySelector("##root")!; +const root = ReactDOM.createRoot(rootEl); + +let eventNames: string[] = ["click"]; + +root.render( + + + , +); +``` + +## Declare + +Add an attribute with the pattern `event-=action`. + +```html + +``` + +Now the button dispatches ActionEvents when clicked. + +## Listen + +The `useSuperAction` hook connects action events to react-land. + +```tsx +import React, { useState } from "react"; +import { ActionInterface, useSuperAction } from "@w-lfpup/react-superaction"; + +export function Counter() { + let [count, setCount] = useState(0); + + useSuperAction((action: ActionInterface) => { + if ("increment" === action.type) setCount(count + 1); + }); + + return +} +``` + +The action object has several properties related to an action event including: + +- the action type +- the original dom event +- the action event target +- associated formData + +```ts +let { type, event, target, formData } = action; +``` + +Form data is available when an action event originates from a element. + +## Event stacking + +`Superaction-js` listens to any DOM event that bubbles. It also dispatches all actions found along the composed path of a DOM event. + +Turns out that's [all UI Events](https://www.w3.org/TR/uievents/#events-uievents). Which is a lot of events! + +Consider the following example: + +```html + +
+ +
+ +``` + +When a person clicks the button above, the order of action events is: + +- Action "C" +- Action "B" +- Action "A" + +## Propagation + +Action events propagate similar to DOM events. Their declarative API reflects their DOM Event counterpart: + +- `event-prevent-default` +- `event-stop-propagation` +- `event-stop-immediate-propagation` + +Consider the following example: + +```html + +
+ + +
+ +``` + +So when a person clicks the buttons above, the order of actions is: + +Click button C: + +- Action "C" dispatched +- `preventDefault()` is called on the original `PointerEvent` +- Action "B" dispatched +- Action propagation is stopped similar to `event.stopImmediatePropagation()` +- Action "A" does _not_ dispatchß + +Click button D: + +- Action "D" dispatched +- Action event propagation stopped similar to `event.stopPropagation()` + +## License + +React-superaction is released under the BSD-3 Clause License. diff --git a/examples/counter/bundle.js b/examples/counter/bundle.js index 29e0209..cce2d46 100644 --- a/examples/counter/bundle.js +++ b/examples/counter/bundle.js @@ -30281,7 +30281,6 @@ class SuperAction { } #dispatch = this.#unboundDispatch.bind(this); #unboundDispatch(event) { - console.log(event); let { type, currentTarget, target } = event; if (!currentTarget) return; diff --git a/examples/form/bundle.js b/examples/form/bundle.js index cb2739b..569b8d3 100644 --- a/examples/form/bundle.js +++ b/examples/form/bundle.js @@ -30281,7 +30281,6 @@ class SuperAction { } #dispatch = this.#unboundDispatch.bind(this); #unboundDispatch(event) { - console.log(event); let { type, currentTarget, target } = event; if (!currentTarget) return; @@ -30341,7 +30340,7 @@ function useSuperAction(cb) { cb(action); } -function CustomForm() { +function Form() { let [formAsJSON, setFormAsJSON] = reactExports.useState(""); useSuperAction((action) => { let { type, formData } = action; @@ -30365,5 +30364,5 @@ let rootEl = document.querySelector("#root"); if (rootEl) { const root = ReactDOM.createRoot(rootEl); root.render(React.createElement(SuperActionProvider, { eventNames: eventNames }, - React.createElement(CustomForm, null))); + React.createElement(Form, null))); } diff --git a/examples/form/custom_form.js b/examples/form/form.js similarity index 96% rename from examples/form/custom_form.js rename to examples/form/form.js index 81c96ed..dda811e 100644 --- a/examples/form/custom_form.js +++ b/examples/form/form.js @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { useSuperAction } from "../../dist/mod.js"; -export function CustomForm() { +export function Form() { let [formAsJSON, setFormAsJSON] = useState(""); useSuperAction((action) => { let { type, formData } = action; diff --git a/examples/form/custom_form.tsx b/examples/form/form.tsx similarity index 96% rename from examples/form/custom_form.tsx rename to examples/form/form.tsx index 405c801..56c12d1 100644 --- a/examples/form/custom_form.tsx +++ b/examples/form/form.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { useSuperAction, useAction } from "../../dist/mod.js"; import { ActionInterface } from "@w-lfpup/superaction"; -export function CustomForm() { +export function Form() { let [formAsJSON, setFormAsJSON] = useState(""); useSuperAction((action: ActionInterface) => { diff --git a/examples/form/root.js b/examples/form/root.js index 1ea6899..e88de0e 100644 --- a/examples/form/root.js +++ b/examples/form/root.js @@ -1,11 +1,11 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { SuperActionProvider } from "../../dist/mod.js"; -import { CustomForm } from "./custom_form.js"; +import { Form } from "./form.js"; let eventNames = ["submit"]; let rootEl = document.querySelector("#root"); if (rootEl) { const root = ReactDOM.createRoot(rootEl); root.render(React.createElement(SuperActionProvider, { eventNames: eventNames }, - React.createElement(CustomForm, null))); + React.createElement(Form, null))); } diff --git a/examples/form/root.tsx b/examples/form/root.tsx index 6e43eb5..6390dc0 100644 --- a/examples/form/root.tsx +++ b/examples/form/root.tsx @@ -1,7 +1,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { SuperActionProvider } from "../../dist/mod.js"; -import { CustomForm } from "./custom_form.js"; +import { Form } from "./form.js"; let eventNames: string[] = ["submit"]; @@ -10,7 +10,7 @@ if (rootEl) { const root = ReactDOM.createRoot(rootEl); root.render( - +
, ); } diff --git a/package-lock.json b/package-lock.json index 1248fe0..973f68b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,27 @@ { - "name": "superaction-react", + "name": "@w-lfpup/react-superaction", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "@w-lfpup/react-superaction", + "version": "0.1.0", + "license": "BSD-3-Clause", + "dependencies": { + "@w-lfpup/superaction": "github:w-lfpup/superaction-js" + }, "devDependencies": { + "@reduxjs/toolkit": "^2.12.0", "@rollup/plugin-commonjs": "^29.0.2", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-replace": "^6.0.3", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", - "@w-lfpup/superaction": "github:w-lfpup/superaction-js", "prettier": "^3.8.3", "react": "^19.2.6", "react-dom": "^19.2.6", + "react-redux": "^9.3.0", "rollup": "^4.60.4", "typescript": "^6.0.3" } @@ -25,6 +33,33 @@ "dev": true, "license": "MIT" }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rollup/plugin-commonjs": { "version": "29.0.2", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.2.tgz", @@ -214,9 +249,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -231,9 +263,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -248,9 +277,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -265,9 +291,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -282,9 +305,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -299,9 +319,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -316,9 +333,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -333,9 +347,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -350,9 +361,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -367,9 +375,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -384,9 +389,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -401,9 +403,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -418,9 +417,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -511,6 +507,20 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -545,10 +555,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@w-lfpup/superaction": { "version": "0.4.3", "resolved": "git+ssh://git@github.com/w-lfpup/superaction-js.git#c9afc93bcd2b482154990db4c0fb864e0a7d03c6", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/commondir": { @@ -648,6 +664,17 @@ "node": ">= 0.4" } }, + "node_modules/immer": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/is-core-module": { "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", @@ -750,6 +777,54 @@ "react": "^19.2.6" } }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -857,6 +932,16 @@ "engines": { "node": ">=14.17" } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } } } } diff --git a/package.json b/package.json index b12ffbc..4151ffc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,12 @@ { - "name": "react-superaction", + "name": "@w-lfpup/react-superaction", "type": "module", + "main": "dist/mod.js", + "description": "Turn the virtual-dom into a declarative event-bus", + "license": "BSD-3-Clause", + "version": "0.1.0", "scripts": { + "prepare": "npm run build", "build": "npm run build:core && npm run build:examples", "build:core": "npx tsc --project src/", "build:examples": "npx tsc --project examples/", @@ -12,6 +17,7 @@ "@w-lfpup/superaction": "github:w-lfpup/superaction-js" }, "devDependencies": { + "@reduxjs/toolkit": "^2.12.0", "@rollup/plugin-commonjs": "^29.0.2", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-replace": "^6.0.3", @@ -20,7 +26,12 @@ "prettier": "^3.8.3", "react": "^19.2.6", "react-dom": "^19.2.6", + "react-redux": "^9.3.0", "rollup": "^4.60.4", "typescript": "^6.0.3" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/w-lfpup/react-superaction.git" } } diff --git a/src/hook.tsx b/src/hook.tsx index f642ea8..a363da6 100644 --- a/src/hook.tsx +++ b/src/hook.tsx @@ -16,19 +16,14 @@ export function useSuperAction(cb: Cb) { if (action) cb(action); } -// single action hook useAction("howdy") export function useAction(type: string, cb: Cb): ActionInterface | undefined { let action = useContext(SuperContext); let [prevAction, setPrevAction] = useState( undefined, ); - if (action === prevAction) return; - - if (type === action?.type) { - setPrevAction(action); - return action; - } + if (action === prevAction || type === action?.type) return; + setPrevAction(action); if (action) cb(action); }