kempo-ui 0.4.25 → 0.4.26
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/dist/components/Context.js +1 -1
- package/docs/components/context.html +17 -0
- package/docs/src/components/Context.js +1 -1
- package/docs-src/components/context.page.html +17 -0
- package/package.json +1 -1
- package/src/components/Context.js +44 -1
- package/tests/components/Context.browser-test.js +74 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{html as t}from"../lit-all.min.js";import e from"./LightComponent.js";export default class
|
|
1
|
+
import{html as t}from"../lit-all.min.js";import e from"./LightComponent.js";const s=Symbol("persist");export default class i extends e{static properties={data:{type:String,reflect:!0},persistentId:{type:String,reflect:!0,attribute:"persistent-id"}};constructor(){super(),this[s]=()=>{if(this.persistentId&&window?.localStorage)try{window.localStorage.setItem(`context-persistent-id-${this.persistentId}`,this.data)}catch{}},this.data="{}",this.persistentId=null}connectedCallback(){if(super.connectedCallback(),this.style.display="contents",this.persistentId&&window?.localStorage){const t=window.localStorage.getItem(`context-persistent-id-${this.persistentId}`);t&&(this.data=t)}}set(t,e){const i=JSON.parse(this.data||"{}"),a=!(t in i),n=i[t];i[t]=e,this.data=JSON.stringify(i),this[s](),this.dispatchEvent(new CustomEvent(a?"context:create":"context:set",{detail:a?{key:t,value:e}:{key:t,value:e,oldValue:n},bubbles:!0,composed:!0}))}get(t){return JSON.parse(this.data||"{}")[t]}has(t){return t in JSON.parse(this.data||"{}")}delete(t){const e=JSON.parse(this.data||"{}");if(!(t in e))return;const i=e[t];delete e[t],this.data=JSON.stringify(e),this[s](),this.dispatchEvent(new CustomEvent("context:delete",{detail:{key:t,value:i},bubbles:!0,composed:!0}))}clear(){for(const t of Object.keys(JSON.parse(this.data||"{}")))this.delete(t)}getData(){return JSON.parse(this.data||"{}")}renderLightDom(){return t``}}customElements.define("k-context",i);
|
|
@@ -505,6 +505,7 @@
|
|
|
505
505
|
<a href="#basicUsage">Basic Usage</a><br />
|
|
506
506
|
<a href="#events">Listening to Events</a><br />
|
|
507
507
|
<a href="#usingInComponents">Using in Components</a><br />
|
|
508
|
+
<a href="#persistence">Persistence</a><br />
|
|
508
509
|
<a href="#interactiveDemo">Interactive Demo</a><br />
|
|
509
510
|
|
|
510
511
|
<h6 class="mt"><a href="#jsRef" class="no-link">JavaScript Reference</a></h6>
|
|
@@ -565,6 +566,17 @@
|
|
|
565
566
|
</div>
|
|
566
567
|
<p>To react to context changes from inside a shadow-DOM component, listen on the context element directly — events bubble and are <code>composed: true</code> so they also cross shadow boundaries.</p>
|
|
567
568
|
|
|
569
|
+
<h3 id="persistence"><a href="#persistence" class="no-link">Persistence</a></h3>
|
|
570
|
+
<p>Set a <code>persistent-id</code> to automatically save the store to <code>localStorage</code> and restore it on the next page load — the same convention used by <code>Split</code>, <code>Accordion</code>, <code>Tabs</code> and <code>Aside</code>. The data is reloaded in <code>connectedCallback</code> (before descendants read it) and re-saved on every <code>set()</code>, <code>delete()</code> and <code>clear()</code>.</p>
|
|
571
|
+
<div class="row -mx">
|
|
572
|
+
<div class="col m-span-12 px">
|
|
573
|
+
<k-card label="HTML">
|
|
574
|
+
<pre><code class="hljs html"><span class="hljs-tag"><<span class="hljs-name">k-context</span> <span class="hljs-attr">persistent-id</span>=<span class="hljs-string">"app-settings"</span>></span><span class="hljs-tag"></<span class="hljs-name">k-context</span>></span></code></pre>
|
|
575
|
+
</k-card>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
<p>Because the entire store is JSON-serialized into a single value, keep persisted contexts to small, serializable state (settings, selections, ids) — not large or non-JSON data. Scope what you persist by giving config its own <code>persistent-id</code> context and leaving transient state in a separate, non-persistent <code>k-context</code>.</p>
|
|
579
|
+
|
|
568
580
|
<h3 id="interactiveDemo"><a href="#interactiveDemo" class="no-link">Interactive Demo</a></h3>
|
|
569
581
|
<div class="row -mx">
|
|
570
582
|
<div class="col m-span-12 t-span-6 px">
|
|
@@ -630,6 +642,11 @@
|
|
|
630
642
|
<td><code>String</code> (JSON)</td>
|
|
631
643
|
<td>A JSON string representing the initial store state. Reflects back to the attribute as state changes via the component's methods. Defaults to <code>'{}'</code>.</td>
|
|
632
644
|
</tr>
|
|
645
|
+
<tr>
|
|
646
|
+
<td><code>persistent-id</code></td>
|
|
647
|
+
<td><code>String</code></td>
|
|
648
|
+
<td>When set, the store is auto-saved to <code>localStorage</code> (key <code>context-persistent-id-<id></code>) on every change and restored on connect. Defaults to <code>null</code> (no persistence).</td>
|
|
649
|
+
</tr>
|
|
633
650
|
</tbody>
|
|
634
651
|
</table>
|
|
635
652
|
</div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{html as t}from"../lit-all.min.js";import e from"./LightComponent.js";export default class
|
|
1
|
+
import{html as t}from"../lit-all.min.js";import e from"./LightComponent.js";const s=Symbol("persist");export default class i extends e{static properties={data:{type:String,reflect:!0},persistentId:{type:String,reflect:!0,attribute:"persistent-id"}};constructor(){super(),this[s]=()=>{if(this.persistentId&&window?.localStorage)try{window.localStorage.setItem(`context-persistent-id-${this.persistentId}`,this.data)}catch{}},this.data="{}",this.persistentId=null}connectedCallback(){if(super.connectedCallback(),this.style.display="contents",this.persistentId&&window?.localStorage){const t=window.localStorage.getItem(`context-persistent-id-${this.persistentId}`);t&&(this.data=t)}}set(t,e){const i=JSON.parse(this.data||"{}"),a=!(t in i),n=i[t];i[t]=e,this.data=JSON.stringify(i),this[s](),this.dispatchEvent(new CustomEvent(a?"context:create":"context:set",{detail:a?{key:t,value:e}:{key:t,value:e,oldValue:n},bubbles:!0,composed:!0}))}get(t){return JSON.parse(this.data||"{}")[t]}has(t){return t in JSON.parse(this.data||"{}")}delete(t){const e=JSON.parse(this.data||"{}");if(!(t in e))return;const i=e[t];delete e[t],this.data=JSON.stringify(e),this[s](),this.dispatchEvent(new CustomEvent("context:delete",{detail:{key:t,value:i},bubbles:!0,composed:!0}))}clear(){for(const t of Object.keys(JSON.parse(this.data||"{}")))this.delete(t)}getData(){return JSON.parse(this.data||"{}")}renderLightDom(){return t``}}customElements.define("k-context",i);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<a href="#basicUsage">Basic Usage</a><br />
|
|
10
10
|
<a href="#events">Listening to Events</a><br />
|
|
11
11
|
<a href="#usingInComponents">Using in Components</a><br />
|
|
12
|
+
<a href="#persistence">Persistence</a><br />
|
|
12
13
|
<a href="#interactiveDemo">Interactive Demo</a><br />
|
|
13
14
|
|
|
14
15
|
<h6 class="mt"><a href="#jsRef" class="no-link">JavaScript Reference</a></h6>
|
|
@@ -69,6 +70,17 @@
|
|
|
69
70
|
</div>
|
|
70
71
|
<p>To react to context changes from inside a shadow-DOM component, listen on the context element directly — events bubble and are <code>composed: true</code> so they also cross shadow boundaries.</p>
|
|
71
72
|
|
|
73
|
+
<h3 id="persistence"><a href="#persistence" class="no-link">Persistence</a></h3>
|
|
74
|
+
<p>Set a <code>persistent-id</code> to automatically save the store to <code>localStorage</code> and restore it on the next page load — the same convention used by <code>Split</code>, <code>Accordion</code>, <code>Tabs</code> and <code>Aside</code>. The data is reloaded in <code>connectedCallback</code> (before descendants read it) and re-saved on every <code>set()</code>, <code>delete()</code> and <code>clear()</code>.</p>
|
|
75
|
+
<div class="row -mx">
|
|
76
|
+
<div class="col m-span-12 px">
|
|
77
|
+
<k-card label="HTML">
|
|
78
|
+
<pre><code class="hljs html"><span class="hljs-tag"><<span class="hljs-name">k-context</span> <span class="hljs-attr">persistent-id</span>=<span class="hljs-string">"app-settings"</span>></span><span class="hljs-tag"></<span class="hljs-name">k-context</span>></span></code></pre>
|
|
79
|
+
</k-card>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<p>Because the entire store is JSON-serialized into a single value, keep persisted contexts to small, serializable state (settings, selections, ids) — not large or non-JSON data. Scope what you persist by giving config its own <code>persistent-id</code> context and leaving transient state in a separate, non-persistent <code>k-context</code>.</p>
|
|
83
|
+
|
|
72
84
|
<h3 id="interactiveDemo"><a href="#interactiveDemo" class="no-link">Interactive Demo</a></h3>
|
|
73
85
|
<div class="row -mx">
|
|
74
86
|
<div class="col m-span-12 t-span-6 px">
|
|
@@ -134,6 +146,11 @@
|
|
|
134
146
|
<td><code>String</code> (JSON)</td>
|
|
135
147
|
<td>A JSON string representing the initial store state. Reflects back to the attribute as state changes via the component's methods. Defaults to <code>'{}'</code>.</td>
|
|
136
148
|
</tr>
|
|
149
|
+
<tr>
|
|
150
|
+
<td><code>persistent-id</code></td>
|
|
151
|
+
<td><code>String</code></td>
|
|
152
|
+
<td>When set, the store is auto-saved to <code>localStorage</code> (key <code>context-persistent-id-<id></code>) on every change and restored on connect. Defaults to <code>null</code> (no persistence).</td>
|
|
153
|
+
</tr>
|
|
137
154
|
</tbody>
|
|
138
155
|
</table>
|
|
139
156
|
</div>
|
package/package.json
CHANGED
|
@@ -4,18 +4,51 @@
|
|
|
4
4
|
A non-rendering state container for sharing data between sibling and descendant
|
|
5
5
|
components, similar in purpose to React's useContext. Place it as an ancestor
|
|
6
6
|
of components that need shared state; children locate it via closest('k-context').
|
|
7
|
+
|
|
8
|
+
Set a `persistent-id` to auto-save/load the data to localStorage (the same
|
|
9
|
+
convention used by Split, Accordion, Tabs and Aside).
|
|
7
10
|
*/
|
|
8
11
|
import { html } from '../lit-all.min.js';
|
|
9
12
|
import LightComponent from './LightComponent.js';
|
|
10
13
|
|
|
14
|
+
/*
|
|
15
|
+
Symbols
|
|
16
|
+
*/
|
|
17
|
+
const persist = Symbol('persist');
|
|
18
|
+
|
|
11
19
|
export default class Context extends LightComponent {
|
|
20
|
+
/*
|
|
21
|
+
Reactive Properties / Attributes
|
|
22
|
+
*/
|
|
12
23
|
static properties = {
|
|
13
|
-
data: { type: String, reflect: true }
|
|
24
|
+
data: { type: String, reflect: true },
|
|
25
|
+
persistentId: { type: String, reflect: true, attribute: 'persistent-id' }
|
|
14
26
|
};
|
|
15
27
|
|
|
28
|
+
/*
|
|
29
|
+
Constructor
|
|
30
|
+
*/
|
|
16
31
|
constructor() {
|
|
17
32
|
super();
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
Private Methods
|
|
36
|
+
*/
|
|
37
|
+
// Mirror the data blob into localStorage whenever a persistent-id is set, so the
|
|
38
|
+
// next page load can restore it. Guarded so a full/blocked store (private mode,
|
|
39
|
+
// quota) never throws out of a set/delete.
|
|
40
|
+
this[persist] = () => {
|
|
41
|
+
if(!this.persistentId || !window?.localStorage) return;
|
|
42
|
+
try {
|
|
43
|
+
window.localStorage.setItem(`context-persistent-id-${this.persistentId}`, this.data);
|
|
44
|
+
} catch { /* storage unavailable — keep working in-memory */ }
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
Init Props
|
|
49
|
+
*/
|
|
18
50
|
this.data = '{}';
|
|
51
|
+
this.persistentId = null;
|
|
19
52
|
}
|
|
20
53
|
|
|
21
54
|
/*
|
|
@@ -24,6 +57,14 @@ export default class Context extends LightComponent {
|
|
|
24
57
|
connectedCallback() {
|
|
25
58
|
super.connectedCallback();
|
|
26
59
|
this.style.display = 'contents';
|
|
60
|
+
// Restore persisted state up front, before descendants read it on their own
|
|
61
|
+
// connect. Setting data directly (rather than per-key set) intentionally skips
|
|
62
|
+
// events: the context is an ancestor, so children read the loaded values via
|
|
63
|
+
// closest('k-context').get() when they first render.
|
|
64
|
+
if(this.persistentId && window?.localStorage){
|
|
65
|
+
const stored = window.localStorage.getItem(`context-persistent-id-${this.persistentId}`);
|
|
66
|
+
if(stored) this.data = stored;
|
|
67
|
+
}
|
|
27
68
|
}
|
|
28
69
|
|
|
29
70
|
/*
|
|
@@ -35,6 +76,7 @@ export default class Context extends LightComponent {
|
|
|
35
76
|
const oldValue = obj[key];
|
|
36
77
|
obj[key] = value;
|
|
37
78
|
this.data = JSON.stringify(obj);
|
|
79
|
+
this[persist]();
|
|
38
80
|
this.dispatchEvent(new CustomEvent(isNew ? 'context:create' : 'context:set', {
|
|
39
81
|
detail: isNew ? { key, value } : { key, value, oldValue },
|
|
40
82
|
bubbles: true,
|
|
@@ -56,6 +98,7 @@ export default class Context extends LightComponent {
|
|
|
56
98
|
const value = obj[key];
|
|
57
99
|
delete obj[key];
|
|
58
100
|
this.data = JSON.stringify(obj);
|
|
101
|
+
this[persist]();
|
|
59
102
|
this.dispatchEvent(new CustomEvent('context:delete', {
|
|
60
103
|
detail: { key, value },
|
|
61
104
|
bubbles: true,
|
|
@@ -10,6 +10,17 @@ const createContext = async () => {
|
|
|
10
10
|
return { container, el };
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
const createPersistentContext = async (persistentId) => {
|
|
14
|
+
const container = document.createElement('div');
|
|
15
|
+
container.innerHTML = `<k-context persistent-id="${persistentId}"></k-context>`;
|
|
16
|
+
document.body.appendChild(container);
|
|
17
|
+
const el = container.querySelector('k-context');
|
|
18
|
+
await el.updateComplete;
|
|
19
|
+
return { container, el };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const storageKey = (id) => `context-persistent-id-${id}`;
|
|
23
|
+
|
|
13
24
|
const cleanup = (container) => {
|
|
14
25
|
if(container && container.parentNode){
|
|
15
26
|
container.parentNode.removeChild(container);
|
|
@@ -234,5 +245,68 @@ export default {
|
|
|
234
245
|
}
|
|
235
246
|
cleanup(container);
|
|
236
247
|
pass('getData() returns a shallow copy of all data');
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
/*
|
|
251
|
+
persistent-id (localStorage auto save/load)
|
|
252
|
+
*/
|
|
253
|
+
'set() writes data to localStorage when persistent-id is set': async ({pass, fail}) => {
|
|
254
|
+
const id = 'persist-set-' + Date.now();
|
|
255
|
+
window.localStorage.removeItem(storageKey(id));
|
|
256
|
+
const { container, el } = await createPersistentContext(id);
|
|
257
|
+
el.set('theme', 'dark');
|
|
258
|
+
let stored;
|
|
259
|
+
try { stored = JSON.parse(window.localStorage.getItem(storageKey(id))); } catch(e) {
|
|
260
|
+
cleanup(container); window.localStorage.removeItem(storageKey(id));
|
|
261
|
+
return fail('localStorage value is not valid JSON');
|
|
262
|
+
}
|
|
263
|
+
cleanup(container); window.localStorage.removeItem(storageKey(id));
|
|
264
|
+
if(!stored || stored.theme !== 'dark'){
|
|
265
|
+
return fail(`Expected stored {theme:'dark'}, got ${JSON.stringify(stored)}`);
|
|
266
|
+
}
|
|
267
|
+
pass('set() persisted to localStorage');
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
'restores data from localStorage on connect': async ({pass, fail}) => {
|
|
271
|
+
const id = 'persist-load-' + Date.now();
|
|
272
|
+
window.localStorage.setItem(storageKey(id), JSON.stringify({ count: 7 }));
|
|
273
|
+
const { container, el } = await createPersistentContext(id);
|
|
274
|
+
const value = el.get('count');
|
|
275
|
+
cleanup(container); window.localStorage.removeItem(storageKey(id));
|
|
276
|
+
if(value !== 7){
|
|
277
|
+
return fail(`Expected restored count to be 7, got ${value}`);
|
|
278
|
+
}
|
|
279
|
+
pass('data restored from localStorage on connect');
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
'delete() updates localStorage': async ({pass, fail}) => {
|
|
283
|
+
const id = 'persist-delete-' + Date.now();
|
|
284
|
+
window.localStorage.removeItem(storageKey(id));
|
|
285
|
+
const { container, el } = await createPersistentContext(id);
|
|
286
|
+
el.set('a', 1);
|
|
287
|
+
el.set('b', 2);
|
|
288
|
+
el.delete('a');
|
|
289
|
+
let stored;
|
|
290
|
+
try { stored = JSON.parse(window.localStorage.getItem(storageKey(id))); } catch(e) {
|
|
291
|
+
cleanup(container); window.localStorage.removeItem(storageKey(id));
|
|
292
|
+
return fail('localStorage value is not valid JSON');
|
|
293
|
+
}
|
|
294
|
+
cleanup(container); window.localStorage.removeItem(storageKey(id));
|
|
295
|
+
if(!stored || 'a' in stored || stored.b !== 2){
|
|
296
|
+
return fail(`Expected stored to drop 'a' and keep b:2, got ${JSON.stringify(stored)}`);
|
|
297
|
+
}
|
|
298
|
+
pass('delete() updated localStorage');
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
'does not write to localStorage without a persistent-id': async ({pass, fail}) => {
|
|
302
|
+
const before = window.localStorage.length;
|
|
303
|
+
const { container, el } = await createContext();
|
|
304
|
+
el.set('x', 1);
|
|
305
|
+
const after = window.localStorage.length;
|
|
306
|
+
cleanup(container);
|
|
307
|
+
if(after !== before){
|
|
308
|
+
return fail('A context without persistent-id should not touch localStorage');
|
|
309
|
+
}
|
|
310
|
+
pass('no persistent-id means no localStorage writes');
|
|
237
311
|
}
|
|
238
312
|
};
|