@jarenjs/app 0.56.0 → 0.66.1
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.
- package/README.md +24 -1
- package/dist/types/forms.d.ts +5 -3
- package/package.json +4 -4
- package/src/docstore.js +5 -3
- package/src/forms.js +28 -16
package/README.md
CHANGED
|
@@ -170,6 +170,10 @@ Two IDE-shaped primitives ship ready to bind, so a two-pane surface (the studio,
|
|
|
170
170
|
- **`createSplitterWidget({ action, grid, rail, cssVar, min, max, step })`** — a drag handle over a pane boundary. It drives a CSS ratio variable *live* during a drag (no per-move dispatch — that would flood the transaction log and undo) and commits the ratio through `action` on pointer-up only, plus keyboard resize as an ARIA separator. Register it like any widget; parameterize the grid/rail selectors, the CSS variable and the commit action so each surface binds its own.
|
|
171
171
|
- **`createDocStore({ storage, key })`** — a keyed `save`/`load`/`remove`/`names`/`all` CRUD over an injected `storage` (`localStorage` in the browser, an in-memory object in tests), so the package never touches `localStorage` itself. Paired with **`encodeShare(snapshot)`** / **`decodeShare(token)`**, a Unicode-safe base64url share-link codec (a corrupt token decodes to `null`, never a throw), it is the new/save/load/delete/share pattern behind the studio and play surfaces.
|
|
172
172
|
|
|
173
|
+
Collection keys and document names such as `__proto__` are ordinary own
|
|
174
|
+
members of the document store and survive persistence. Loading a name
|
|
175
|
+
that has not been saved returns `undefined`, including prototype-member names.
|
|
176
|
+
|
|
173
177
|
## Invariants the model can't cheat
|
|
174
178
|
|
|
175
179
|
```javascript
|
|
@@ -214,7 +218,12 @@ const app = createApp({
|
|
|
214
218
|
});
|
|
215
219
|
```
|
|
216
220
|
|
|
217
|
-
Schema in, live form out: text/email/number/date/color inputs, textareas, checkboxes, selects with precomputed options, nested object fieldsets, arrays with add/remove buttons, inline errors, and `x-form` visibility/enablement/computed reacting per keystroke. The `viewModel` option is the general **derivation boundary**: it maps state to the view stylesheet's input before every render, so JS-computed derivations enter the render path without ever entering the state. A DOM control's value is a string, and two controls carry something else: a select over a non-string enum, and the `json` editor over a structured value. Both round-trip through JSON text and decode it in `formEventFields()`, the format's one sanctioned place for host JavaScript at the DOM boundary (APP-FORMAT §5.4) — **register it or those two controls write nothing**. Remaining
|
|
221
|
+
Schema in, live form out: text/email/number/date/color inputs, textareas, checkboxes, selects with precomputed options, nested object fieldsets, arrays with add/remove buttons, inline errors, and `x-form` visibility/enablement/computed reacting per keystroke. The `viewModel` option is the general **derivation boundary**: it maps state to the view stylesheet's input before every render, so JS-computed derivations enter the render path without ever entering the state. A DOM control's value is a string, and two controls carry something else: a select over a non-string enum, and the `json` editor over a structured value. Both round-trip through JSON text and decode it in `formEventFields()`, the format's one sanctioned place for host JavaScript at the DOM boundary (APP-FORMAT §5.4) — **register it or those two controls write nothing**. Remaining limits (documented in `src/forms.js`): a cleared number input writes `null`, and intermediate object/array containers need to exist in the data. `createInitialData(model)` supplies them for a new form; loaded documents must supply them too.
|
|
222
|
+
|
|
223
|
+
Nested object and array fields honor `x-form.enabled` through their native
|
|
224
|
+
`fieldset` disablement, including child controls and array buttons. A `readOnly`
|
|
225
|
+
field uses native `readonly` for text inputs and textareas, and `disabled` for
|
|
226
|
+
checkboxes, selects, collection fieldsets, and add/remove buttons.
|
|
218
227
|
|
|
219
228
|
## Headless and server-side
|
|
220
229
|
|
|
@@ -233,6 +242,20 @@ Also exported: `compileActions`, `compileSubs`, `createFormView`, `createFormAct
|
|
|
233
242
|
|
|
234
243
|
Options: `node`, `document`, `effects`, `subs`, `eventFields` (named `$event` field extractors), `widgets` (registered widget definitions, forwarded to the renderer), `compileTypeTest`, `validateState`, `viewModel`, `onError` (default rethrows), `schedule` (render batching; default microtask — pass `(f) => f()` for synchronous tests). Compile failures throw `AppCompileError` (`JA0xxx`, with a `docPath` into the app document); runtime failures route `AppRuntimeError` (`JA2xxx`) through `onError`. The full code table is in [APP-FORMAT.md](docs/APP-FORMAT.md) §10.
|
|
235
244
|
|
|
245
|
+
## Exports
|
|
246
|
+
|
|
247
|
+
Every subpath a consumer can import, derived from the manifest by
|
|
248
|
+
`npm run docs:derive` (`npm run docs:check` fails when the two drift):
|
|
249
|
+
|
|
250
|
+
<!--fact:exports.app-->
|
|
251
|
+
| Import | Kind | Declarations |
|
|
252
|
+
|---|---|---|
|
|
253
|
+
| `@jarenjs/app` | JavaScript | declared |
|
|
254
|
+
| `@jarenjs/app/schemas/jaren-app.draft-07.schema.json` | schema | — |
|
|
255
|
+
| `@jarenjs/app/schemas/jaren-app.schema.json` | schema | — |
|
|
256
|
+
| `@jarenjs/app/package.json` | metadata | — |
|
|
257
|
+
<!--/fact-->
|
|
258
|
+
|
|
236
259
|
## Development
|
|
237
260
|
|
|
238
261
|
Unit tests live in `test/app/` at the repository root (`npm run test:app`). See [ROADMAP](../../docs/ROADMAP.md) for what's next: dirty-path-pruned re-rendering and time-travel tooling over the action log.
|
package/dist/types/forms.d.ts
CHANGED
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
* JavaScript at the DOM boundary (APP-FORMAT §5.4). A host that renders
|
|
23
23
|
* these controls MUST register them.
|
|
24
24
|
*
|
|
25
|
-
* Remaining
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* Remaining limitations: a cleared number input writes `null` (which
|
|
26
|
+
* surfaces as a validation error, not a dispatch error), and standard
|
|
27
|
+
* actions require intermediate object/array containers to exist in the
|
|
28
|
+
* data. `createInitialData` supplies those containers for a new form;
|
|
29
|
+
* loaded documents must supply them too.
|
|
28
30
|
*/
|
|
29
31
|
/**
|
|
30
32
|
* The event-field extractors the standard form controls need, for
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/app",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.66.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"prepack": "npm run build:types"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@jarenjs/core": "^0.
|
|
54
|
-
"@jarenjs/json": "^0.
|
|
55
|
-
"@jarenjs/view": "^0.
|
|
53
|
+
"@jarenjs/core": "^0.66.1",
|
|
54
|
+
"@jarenjs/json": "^0.66.1",
|
|
55
|
+
"@jarenjs/view": "^0.66.1"
|
|
56
56
|
}
|
|
57
57
|
}
|
package/src/docstore.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* forgivingly: a corrupt token decodes to `null`, never a throw.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { setObjectMember } from '@jarenjs/core/object';
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* @param {Object} opts
|
|
17
19
|
* @param {{ read: () => any, write: (store: any) => void }} opts.storage
|
|
@@ -29,10 +31,10 @@
|
|
|
29
31
|
export function createDocStore({ storage, key = 'experiments' }) {
|
|
30
32
|
// read once; keep the SAME object reference for every write-back
|
|
31
33
|
const store = storage.read() ?? {};
|
|
32
|
-
if (store
|
|
34
|
+
if (!Object.hasOwn(store, key) || store[key] == null) setObjectMember(store, key, {});
|
|
33
35
|
return {
|
|
34
|
-
save(name, value) { store[key]
|
|
35
|
-
load(name) { return store[key][name]; },
|
|
36
|
+
save(name, value) { setObjectMember(store[key], name, value); storage.write(store); },
|
|
37
|
+
load(name) { return Object.hasOwn(store[key], name) ? store[key][name] : undefined; },
|
|
36
38
|
remove(name) { delete store[key][name]; storage.write(store); },
|
|
37
39
|
names() { return Object.keys(store[key]).sort(); },
|
|
38
40
|
all() { return store[key]; },
|
package/src/forms.js
CHANGED
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
* JavaScript at the DOM boundary (APP-FORMAT §5.4). A host that renders
|
|
24
24
|
* these controls MUST register them.
|
|
25
25
|
*
|
|
26
|
-
* Remaining
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Remaining limitations: a cleared number input writes `null` (which
|
|
27
|
+
* surfaces as a validation error, not a dispatch error), and standard
|
|
28
|
+
* actions require intermediate object/array containers to exist in the
|
|
29
|
+
* data. `createInitialData` supplies those containers for a new form;
|
|
30
|
+
* loaded documents must supply them too.
|
|
29
31
|
*/
|
|
30
32
|
|
|
31
33
|
/** The default action names shared by both factories. */
|
|
@@ -111,6 +113,21 @@ export function createFormView(options = {}) {
|
|
|
111
113
|
const ctl = (control) => `${root}..[?@.control == '${control}']`;
|
|
112
114
|
|
|
113
115
|
const disabled = { $not: '$.enabled' };
|
|
116
|
+
// Selects, checkboxes and collection controls have no readonly mode.
|
|
117
|
+
// Disabling their fieldset also prevents edits through child controls.
|
|
118
|
+
const writeDisabled = { $or: [disabled, '$.readOnly'] };
|
|
119
|
+
|
|
120
|
+
/** One removal control for scalar and collection array elements. */
|
|
121
|
+
const remove = { $if: ['$.removable',
|
|
122
|
+
['button', {
|
|
123
|
+
type: 'button',
|
|
124
|
+
class: `${cls}-remove`,
|
|
125
|
+
// the glyph is decoration; the accessible name is the label
|
|
126
|
+
'aria-label': labels.removeItem,
|
|
127
|
+
title: labels.removeItem,
|
|
128
|
+
disabled: writeDisabled,
|
|
129
|
+
on: { click: { action: act.remove, with: { pointer: '$.pointer' } } },
|
|
130
|
+
}, options.removeLabel ?? '×']] };
|
|
114
131
|
|
|
115
132
|
/** The shared field chrome around one control vnode. */
|
|
116
133
|
const field = (control) => ['div', { class: `${cls}-field`, 'data-pointer': '$.pointer' },
|
|
@@ -121,15 +138,7 @@ export function createFormView(options = {}) {
|
|
|
121
138
|
],
|
|
122
139
|
{ $if: ['$.description', ['p', { class: `${cls}-description` }, '$.description']] },
|
|
123
140
|
[{ $apply: '$.errors[*]' }],
|
|
124
|
-
|
|
125
|
-
['button', {
|
|
126
|
-
type: 'button',
|
|
127
|
-
class: `${cls}-remove`,
|
|
128
|
-
// the glyph is decoration; the accessible name is the label
|
|
129
|
-
'aria-label': labels.removeItem,
|
|
130
|
-
title: labels.removeItem,
|
|
131
|
-
on: { click: { action: act.remove, with: { pointer: '$.pointer' } } },
|
|
132
|
-
}, options.removeLabel ?? '×']] },
|
|
141
|
+
remove,
|
|
133
142
|
];
|
|
134
143
|
|
|
135
144
|
// every write binding carries the element flag: the standard actions
|
|
@@ -179,16 +188,17 @@ export function createFormView(options = {}) {
|
|
|
179
188
|
// nested objects: a fieldset group
|
|
180
189
|
{
|
|
181
190
|
match: ctl('object'),
|
|
182
|
-
body: ['fieldset', { class: `${cls}-group`, 'data-pointer': '$.pointer' },
|
|
191
|
+
body: ['fieldset', { class: `${cls}-group`, 'data-pointer': '$.pointer', disabled: writeDisabled },
|
|
183
192
|
{ $if: ['$.label', ['legend', {}, '$.label']] },
|
|
184
193
|
[{ $apply: '$.children[*]' }],
|
|
185
194
|
[{ $apply: '$.errors[*]' }],
|
|
195
|
+
remove,
|
|
186
196
|
],
|
|
187
197
|
},
|
|
188
198
|
// arrays: expanded items plus the add-item button
|
|
189
199
|
{
|
|
190
200
|
match: ctl('array'),
|
|
191
|
-
body: ['fieldset', { class: `${cls}-array`, 'data-pointer': '$.pointer' },
|
|
201
|
+
body: ['fieldset', { class: `${cls}-array`, 'data-pointer': '$.pointer', disabled: writeDisabled },
|
|
192
202
|
{ $if: ['$.label', ['legend', {}, '$.label']] },
|
|
193
203
|
[{ $apply: '$.items[*]' }],
|
|
194
204
|
{ $if: [{ $exists: '$.addValue' },
|
|
@@ -197,9 +207,11 @@ export function createFormView(options = {}) {
|
|
|
197
207
|
class: `${cls}-add`,
|
|
198
208
|
'aria-label': labels.addItem,
|
|
199
209
|
title: labels.addItem,
|
|
210
|
+
disabled: writeDisabled,
|
|
200
211
|
on: { click: { action: act.add, with: { pointer: '$.pointer', value: '$.addValue' } } },
|
|
201
212
|
}, options.addLabel ?? '+']] },
|
|
202
213
|
[{ $apply: '$.errors[*]' }],
|
|
214
|
+
remove,
|
|
203
215
|
],
|
|
204
216
|
},
|
|
205
217
|
// one error line per message
|
|
@@ -216,7 +228,7 @@ export function createFormView(options = {}) {
|
|
|
216
228
|
{
|
|
217
229
|
match: ctl('select'),
|
|
218
230
|
body: field(['select', {
|
|
219
|
-
disabled,
|
|
231
|
+
disabled: writeDisabled,
|
|
220
232
|
on: {
|
|
221
233
|
change: { action: act.json, with: writeWith, event: [JSON_FIELD] },
|
|
222
234
|
},
|
|
@@ -228,7 +240,7 @@ export function createFormView(options = {}) {
|
|
|
228
240
|
body: field(['input', {
|
|
229
241
|
type: 'checkbox',
|
|
230
242
|
checked: '$.value',
|
|
231
|
-
disabled,
|
|
243
|
+
disabled: writeDisabled,
|
|
232
244
|
on: { change: { action: act.check, with: writeWith } },
|
|
233
245
|
}]),
|
|
234
246
|
},
|