phlex-reactive 0.4.8 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +868 -3
  3. data/README.md +986 -32
  4. data/app/controllers/phlex/reactive/actions_controller.rb +282 -190
  5. data/app/javascript/phlex/reactive/compute.js +52 -7
  6. data/app/javascript/phlex/reactive/compute.min.js +4 -0
  7. data/app/javascript/phlex/reactive/compute.min.js.map +10 -0
  8. data/app/javascript/phlex/reactive/confirm.min.js +4 -0
  9. data/app/javascript/phlex/reactive/confirm.min.js.map +10 -0
  10. data/app/javascript/phlex/reactive/reactive_controller.js +1487 -80
  11. data/app/javascript/phlex/reactive/reactive_controller.min.js +4 -0
  12. data/app/javascript/phlex/reactive/reactive_controller.min.js.map +10 -0
  13. data/lib/generators/phlex/reactive/component/USAGE +2 -1
  14. data/lib/generators/phlex/reactive/component/templates/component.rb.erb +1 -2
  15. data/lib/generators/phlex/reactive/component/templates/component_spec.rb.erb +33 -2
  16. data/lib/generators/phlex/reactive/install/install_generator.rb +9 -3
  17. data/lib/generators/phlex/reactive/install/templates/phlex_reactive.rb.erb +37 -0
  18. data/lib/phlex/reactive/component/dsl.rb +249 -0
  19. data/lib/phlex/reactive/component/helpers.rb +595 -0
  20. data/lib/phlex/reactive/component/identity.rb +92 -0
  21. data/lib/phlex/reactive/component/registry.rb +200 -0
  22. data/lib/phlex/reactive/component.rb +30 -442
  23. data/lib/phlex/reactive/doctor.rb +333 -0
  24. data/lib/phlex/reactive/engine.rb +27 -9
  25. data/lib/phlex/reactive/js.rb +222 -0
  26. data/lib/phlex/reactive/log_subscriber.rb +64 -0
  27. data/lib/phlex/reactive/param_schema.rb +390 -0
  28. data/lib/phlex/reactive/reply.rb +5 -3
  29. data/lib/phlex/reactive/response.rb +156 -16
  30. data/lib/phlex/reactive/stream.rb +98 -0
  31. data/lib/phlex/reactive/streamable.rb +307 -43
  32. data/lib/phlex/reactive/test_helpers/matchers.rb +112 -0
  33. data/lib/phlex/reactive/test_helpers.rb +308 -0
  34. data/lib/phlex/reactive/version.rb +1 -1
  35. data/lib/phlex/reactive.rb +329 -14
  36. data/lib/tasks/phlex_reactive.rake +14 -0
  37. metadata +19 -1
@@ -0,0 +1,4 @@
1
+ var j=new Map;function q(b,p){j.set(b,p)}function v(b){return j.get(b)}function w(){j.clear()}export{q as setComputeReducer,v as computeReducer,w as __resetComputeRegistryForTest};
2
+
3
+ //# debugId=5EACBA6D0928063864756E2164756E21
4
+ //# sourceMappingURL=compute.min.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["compute.js"],
4
+ "sourcesContent": [
5
+ "// The client-side compute (data-binding) registry — the \"instant\" half of the\n// new/unpersisted-record UX.\n//\n// A record-backed reactive component round-trips every change to the server\n// (the signed identity re-finds the record; the server re-renders). A NEW,\n// unpersisted record has no such server truth to re-render against on every\n// keystroke — the classic answer is a bespoke Stimulus controller doing the math\n// in the browser (carlqvist's new_order_controller.js). This registry lets that\n// math be a DECLARED part of the component instead: `reactive_compute :name,\n// inputs:, outputs:` (Ruby) names a reducer registered here, and the generic\n// reactive controller runs it on `input` — writing the outputs with NO round\n// trip. When the component ALSO carries on(...) (a persisted record, or a draft\n// you sync), the debounced POST reconciles from the authoritative server reply.\n//\n// The seam mirrors confirm.js: a settable registry with a lookup the controller\n// calls. Register once at boot:\n//\n// import { setComputeReducer } from \"phlex/reactive/compute\"\n// setComputeReducer(\"payment_split\", ({ allowance, cash, leasing, total }) => ({\n// allowance, leasing, cash: total - allowance - leasing,\n// }))\n//\n// The reducer's signature is (values, meta):\n//\n// values — a plain object of { inputName: value } over the declared inputs.\n// Untyped inputs (the array form) AND :number-typed inputs arrive as\n// Numbers (blank/NaN → 0); :string-typed inputs (the hash form,\n// issue #104) arrive as the RAW string. `reactive_compute :x, inputs:\n// { title: :string, qty: :number }` is what selects per-input types;\n// `inputs: %i[a b]` stays all-numeric (backward compatible).\n// meta — { changed }: the name (string) of the declared input the\n// triggering event edited, or null (a direct recompute() call, or a\n// target this root doesn't own / didn't declare as an input).\n//\n// OUTPUTS may be a form FIELD or a TEXT NODE (issue #104). An output whose name\n// matches an owned control writes its .value (+ the change-guarded input\n// dispatch below). An output with NO matching field writes textContent to every\n// owned [data-reactive-text=\"<name>\"] node (reactive_text(:name)) — XSS-safe by\n// construction, change-guarded, NO input dispatch (a text node has no listener\n// contract). A declared INPUT also mirrors into its own text node on every\n// input via an always-run pass — so reactive_text(:title) is a live field\n// preview with NO registered reducer at all.\n//\n// It returns a plain object of { outputName: value } — only the outputs it\n// names are written, so it can leave the edited field (and its caret)\n// untouched. A one-argument reducer keeps working unchanged (it just ignores\n// meta). `changed` is what makes a MULTI-WAY / MUTUAL rebalance expressible as\n// one reducer (issue #75) — branch on which field the user edited:\n//\n// setComputeReducer(\"three_way_split\", ({ field_a, field_b, field_c, total }, { changed }) => {\n// if (changed === \"field_c\") return { field_a: total - field_c - field_b }\n// return { field_c: total - field_a - field_b }\n// })\n//\n// Output writes are CHANGE-GUARDED: the controller writes a field and\n// dispatches a bubbling `input` event on it ONLY when the new value differs\n// from the field's current value (real browsers never fire `input` on a\n// programmatic .value write, so the controller dispatches explicitly — that's\n// what drives a chained summary repaint, matching the server's set_value +\n// dispatch(\"input\") contract). Returning the SAME value a field already holds\n// is skipped entirely — no write, no event — which is why a reducer with\n// overlapping inputs/outputs (like payment_split above) settles instead of\n// re-entering itself forever.\n//\n// CONVERGENCE REQUIREMENT: because an output write dispatches a REAL input\n// event (issue #76), recompute re-enters with changed = that OUTPUT field's\n// name (when it's also a declared input). A branching reducer must therefore\n// be convergent: the re-entrant pass must compute values EQUAL to what the\n// first pass already wrote to the DOM, so the change guard settles the chain.\n// The three_way_split above is: after `changed === \"field_a\"` writes field_c,\n// the re-entrant `changed === \"field_c\"` pass derives field_a back to the\n// value it already holds — no write, no event, settled in one bounce.\n\nconst reducers = new Map()\n\n// Register (or replace) the reducer for `key`. `fn` is\n// (values: Record<string, number>, meta: { changed: string | null })\n// => Record<string, unknown>.\nexport function setComputeReducer(key, fn) {\n reducers.set(key, fn)\n}\n\n// Look up a registered reducer; undefined when none — the controller then makes\n// #recompute a no-op rather than throwing (a missing reducer must not break the\n// page; it just means no client-side binding for that root).\nexport function computeReducer(key) {\n return reducers.get(key)\n}\n\n// Test seam: clear the registry so a reducer registered in one test can't leak.\nexport function __resetComputeRegistryForTest() {\n reducers.clear()\n}\n"
6
+ ],
7
+ "mappings": "AAyEA,IAAM,EAAW,IAAI,IAKd,SAAS,CAAiB,CAAC,EAAK,EAAI,CACzC,EAAS,IAAI,EAAK,CAAE,EAMf,SAAS,CAAc,CAAC,EAAK,CAClC,OAAO,EAAS,IAAI,CAAG,EAIlB,SAAS,CAA6B,EAAG,CAC9C,EAAS,MAAM",
8
+ "debugId": "5EACBA6D0928063864756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,4 @@
1
+ var k=(b)=>Promise.resolve(typeof window<"u"?window.confirm(b):!0);function p(b){k=b}export{p as setConfirmResolver,k as confirmResolver};
2
+
3
+ //# debugId=95D50A0903B9B05E64756E2164756E21
4
+ //# sourceMappingURL=confirm.min.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["confirm.js"],
4
+ "sourcesContent": [
5
+ "// The overridable confirmation resolver behind `on(:action, confirm: \"…\")`\n// (issue #55, follow-up to #52).\n//\n// The reactive controller preempts the click event (its own preventDefault +\n// POST), so Hotwire's `data-turbo-confirm` — which routes through\n// `Turbo.config.forms.confirm` — never runs for a reactive trigger. That made\n// the reactive path the ONE interaction a Hotwire app couldn't theme: every\n// confirmable reactive action showed the unstyled native window.confirm chrome.\n//\n// This module is the seam. `dispatch` resolves the confirm message through\n// `confirmResolver`, which defaults to a Promise-wrapped window.confirm (so the\n// 0.4.5 behavior is byte-for-byte unchanged when nothing is configured: sync,\n// no dependency, screen-reader friendly). An app reuses its themed dialog with\n// one line at boot:\n//\n// import { setConfirmResolver } from \"phlex/reactive/confirm\"\n// setConfirmResolver((message) => window.Turbo.config.forms.confirm(message))\n//\n// The resolver may be sync or async — the controller awaits it via\n// Promise.resolve, so a bare boolean and a Promise<boolean> both work. It must\n// resolve truthy to proceed; a falsy resolve (or a rejection) cancels the\n// action — exactly like declining the native prompt.\n\n// The default: wrap the synchronous native confirm in a Promise so the call\n// site can always `await` it. Read window lazily (per call), not at module load\n// — under SSR / test the global may not exist yet when this module is imported.\nexport let confirmResolver = (message) =>\n Promise.resolve(typeof window !== \"undefined\" ? window.confirm(message) : true)\n\n// Override the resolver. Pass a function `(message) => boolean | Promise<boolean>`.\n// Truthy resolves the action through; falsy (or a rejected Promise) cancels it.\nexport function setConfirmResolver(fn) {\n confirmResolver = fn\n}\n"
6
+ ],
7
+ "mappings": "AA0BO,IAAI,EAAkB,CAAC,IAC5B,QAAQ,QAAQ,OAAO,OAAW,IAAc,OAAO,QAAQ,CAAO,EAAI,EAAI,EAIzE,SAAS,CAAkB,CAAC,EAAI,CACrC,EAAkB",
8
+ "debugId": "95D50A0903B9B05E64756E2164756E21",
9
+ "names": []
10
+ }