halfcab 15.0.9 → 16.0.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/halfcab.mjs +72 -16
- package/package.json +3 -3
- package/test.js +98 -0
package/halfcab.mjs
CHANGED
|
@@ -18,7 +18,7 @@ let cssTag = cssInject
|
|
|
18
18
|
let componentCSSString = ''
|
|
19
19
|
let routesArray = []
|
|
20
20
|
let externalRoutes = []
|
|
21
|
-
let
|
|
21
|
+
let rawState = {}
|
|
22
22
|
let router
|
|
23
23
|
let rootEl
|
|
24
24
|
let components
|
|
@@ -39,14 +39,14 @@ function b64DecodeUnicode (str) {
|
|
|
39
39
|
if (typeof window !== 'undefined') {
|
|
40
40
|
dataInitial = document.querySelector('[data-initial]')
|
|
41
41
|
if (!!dataInitial) {
|
|
42
|
-
|
|
42
|
+
rawState = (dataInitial && dataInitial.dataset.initial) && Object.assign({}, JSON.parse(b64DecodeUnicode(dataInitial.dataset.initial)))
|
|
43
43
|
|
|
44
|
-
if (!
|
|
45
|
-
|
|
44
|
+
if (!rawState.router) {
|
|
45
|
+
rawState.router = {}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
if (!
|
|
49
|
-
Object.assign(
|
|
48
|
+
if (!rawState.router.pathname) {
|
|
49
|
+
Object.assign(rawState.router, {
|
|
50
50
|
pathname: window.location.pathname,
|
|
51
51
|
hash: window.location.hash,
|
|
52
52
|
query: qs.parse(window.location.search)
|
|
@@ -62,6 +62,46 @@ if (typeof window !== 'undefined') {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
const proxyCache = new WeakMap()
|
|
66
|
+
|
|
67
|
+
const ARRAY_MUTATING_METHODS = new Set(['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill', 'copyWithin'])
|
|
68
|
+
|
|
69
|
+
function createState (target) {
|
|
70
|
+
if (proxyCache.has(target)) {
|
|
71
|
+
return proxyCache.get(target)
|
|
72
|
+
}
|
|
73
|
+
const proxy = new Proxy(target, {
|
|
74
|
+
get (obj, prop) {
|
|
75
|
+
const value = obj[prop]
|
|
76
|
+
if (Array.isArray(obj) && typeof value === 'function' && ARRAY_MUTATING_METHODS.has(prop)) {
|
|
77
|
+
return function (...args) {
|
|
78
|
+
const result = Array.prototype[prop].apply(obj, args)
|
|
79
|
+
debounce(stateUpdated)
|
|
80
|
+
return result
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (value !== null && typeof value === 'object') {
|
|
84
|
+
return createState(value)
|
|
85
|
+
}
|
|
86
|
+
return value
|
|
87
|
+
},
|
|
88
|
+
set (obj, prop, value) {
|
|
89
|
+
obj[prop] = value
|
|
90
|
+
debounce(stateUpdated)
|
|
91
|
+
return true
|
|
92
|
+
},
|
|
93
|
+
deleteProperty (obj, prop) {
|
|
94
|
+
delete obj[prop]
|
|
95
|
+
debounce(stateUpdated)
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
proxyCache.set(target, proxy)
|
|
100
|
+
return proxy
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let state = createState(rawState)
|
|
104
|
+
|
|
65
105
|
let geb = new eventEmitter({state})
|
|
66
106
|
|
|
67
107
|
const stringsCache = new WeakMap()
|
|
@@ -316,19 +356,30 @@ function stateUpdated () {
|
|
|
316
356
|
}
|
|
317
357
|
}
|
|
318
358
|
|
|
359
|
+
function mergeInPlace (target, source) {
|
|
360
|
+
// Recursively merge source into target, mutating target in-place so that
|
|
361
|
+
// existing object references (and their Proxy wrappers) are preserved.
|
|
362
|
+
// Arrays are always replaced entirely — merging arrays is complex and error-prone.
|
|
363
|
+
for (const key of Object.keys(source)) {
|
|
364
|
+
const srcVal = source[key]
|
|
365
|
+
const tgtVal = target[key]
|
|
366
|
+
if (srcVal !== null && typeof srcVal === 'object' && !Array.isArray(srcVal) &&
|
|
367
|
+
tgtVal !== null && typeof tgtVal === 'object' && !Array.isArray(tgtVal)) {
|
|
368
|
+
mergeInPlace(tgtVal, srcVal)
|
|
369
|
+
} else {
|
|
370
|
+
target[key] = srcVal
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
319
375
|
function updateState (updateObject, options) {
|
|
320
376
|
if (updateObject) {
|
|
321
377
|
if (options && options.deepMerge === false) {
|
|
322
|
-
Object.assign(
|
|
378
|
+
Object.assign(rawState, updateObject)
|
|
323
379
|
} else {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
//don't merge arrays, just return the new one
|
|
328
|
-
return sourceArray
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
Object.assign(state, merge(state, updateObject, deepMergeOptions))
|
|
380
|
+
// Merge in-place so existing nested object references stay intact,
|
|
381
|
+
// keeping Proxy cache entries valid.
|
|
382
|
+
mergeInPlace(rawState, updateObject, options)
|
|
332
383
|
}
|
|
333
384
|
}
|
|
334
385
|
|
|
@@ -344,7 +395,7 @@ function updateState (updateObject, options) {
|
|
|
344
395
|
console.log(updateObject)
|
|
345
396
|
console.log(' ')
|
|
346
397
|
console.log('------NEW STATE------')
|
|
347
|
-
console.log(
|
|
398
|
+
console.log(rawState)
|
|
348
399
|
console.log(' ')
|
|
349
400
|
}
|
|
350
401
|
|
|
@@ -513,6 +564,10 @@ export default (config, {shiftyRouter = shiftyRouterModule, href = hrefModule, h
|
|
|
513
564
|
})
|
|
514
565
|
}
|
|
515
566
|
|
|
567
|
+
/**
|
|
568
|
+
* @deprecated The Proxy-based state now triggers rerenders automatically via set traps.
|
|
569
|
+
* This function is kept for backwards compatibility but is no longer needed in most cases.
|
|
570
|
+
*/
|
|
516
571
|
function rerender () {
|
|
517
572
|
debounce(stateUpdated)
|
|
518
573
|
}
|
|
@@ -529,6 +584,7 @@ export {
|
|
|
529
584
|
html,
|
|
530
585
|
defineRoute,
|
|
531
586
|
updateState,
|
|
587
|
+
createState,
|
|
532
588
|
state,
|
|
533
589
|
formField,
|
|
534
590
|
gotoRoute,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "halfcab",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "16.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A simple universal JavaScript framework focused on making use of es2015 template strings to build components.",
|
|
6
6
|
"main": "halfcab.mjs",
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
"test:coverage": "c8 --reporter=html --check-coverage --lines 75 --functions 75 --branches 75 npm test",
|
|
12
12
|
"test:coveralls": "c8 npm test && c8 report --reporter=text-lcov | coveralls",
|
|
13
13
|
"versionbump:fix": "npm version patch --no-git-tag-version",
|
|
14
|
-
"versionbump:feature": "npm version
|
|
14
|
+
"versionbump:feature": "npm version minor --no-git-tag-version",
|
|
15
15
|
"versionbump:breakingchanges": "npm version major --no-git-tag-version",
|
|
16
|
-
"npm-publish": "npm publish",
|
|
16
|
+
"npm-publish": "npm publish --tag beta",
|
|
17
17
|
"start:example": "npx --yes serve . -l 5173"
|
|
18
18
|
},
|
|
19
19
|
"repository": {
|
package/test.js
CHANGED
|
@@ -403,6 +403,104 @@ describe('halfcab', () => {
|
|
|
403
403
|
.true()
|
|
404
404
|
})
|
|
405
405
|
|
|
406
|
+
it('Keeps sub-object references live after overwriting via updateState', (done) => {
|
|
407
|
+
halfcab({
|
|
408
|
+
el: '#root',
|
|
409
|
+
components () {
|
|
410
|
+
return html `<div></div>`
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
.then(({rootEl, state}) => {
|
|
414
|
+
updateState({
|
|
415
|
+
user: { name: 'Original Name', age: 25 }
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
nextTick(() => {
|
|
419
|
+
// Grab a reference to the sub-object proxy
|
|
420
|
+
const user = state.user
|
|
421
|
+
|
|
422
|
+
// Wipe out the user object entirely via a merge
|
|
423
|
+
updateState({ user: { name: 'New Name', age: 30 } })
|
|
424
|
+
|
|
425
|
+
nextTick(() => {
|
|
426
|
+
// The reference should reflect the new values
|
|
427
|
+
expect(user.name).to.equal('New Name')
|
|
428
|
+
expect(user.age).to.equal(30)
|
|
429
|
+
done()
|
|
430
|
+
})
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('Replaces arrays entirely instead of merging them', (done) => {
|
|
436
|
+
halfcab({
|
|
437
|
+
el: '#root',
|
|
438
|
+
components () {
|
|
439
|
+
return html `<div></div>`
|
|
440
|
+
}
|
|
441
|
+
})
|
|
442
|
+
.then(({rootEl, state}) => {
|
|
443
|
+
updateState({ list: [1, 2, 3] })
|
|
444
|
+
|
|
445
|
+
nextTick(() => {
|
|
446
|
+
updateState({ list: [4, 5] })
|
|
447
|
+
|
|
448
|
+
nextTick(() => {
|
|
449
|
+
expect(state.list).to.deep.equal([4, 5])
|
|
450
|
+
done()
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('Triggers rerender when a property is deleted from state', (done) => {
|
|
457
|
+
halfcab({
|
|
458
|
+
el: '#root',
|
|
459
|
+
components () {
|
|
460
|
+
return html `<div></div>`
|
|
461
|
+
}
|
|
462
|
+
})
|
|
463
|
+
.then(({rootEl, state}) => {
|
|
464
|
+
updateState({ toDelete: 'bye' })
|
|
465
|
+
|
|
466
|
+
nextTick(() => {
|
|
467
|
+
expect(state.toDelete).to.equal('bye')
|
|
468
|
+
delete state.toDelete
|
|
469
|
+
|
|
470
|
+
nextTick(() => {
|
|
471
|
+
expect(state.toDelete).to.be.undefined()
|
|
472
|
+
done()
|
|
473
|
+
})
|
|
474
|
+
})
|
|
475
|
+
})
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('Triggers rerender when array mutation methods are used on state', (done) => {
|
|
479
|
+
halfcab({
|
|
480
|
+
el: '#root',
|
|
481
|
+
components () {
|
|
482
|
+
return html `<div></div>`
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
.then(({rootEl, state}) => {
|
|
486
|
+
updateState({ items: [1, 2, 3] })
|
|
487
|
+
|
|
488
|
+
nextTick(() => {
|
|
489
|
+
state.items.push(4)
|
|
490
|
+
|
|
491
|
+
nextTick(() => {
|
|
492
|
+
expect(state.items).to.deep.equal([1, 2, 3, 4])
|
|
493
|
+
state.items.pop()
|
|
494
|
+
|
|
495
|
+
nextTick(() => {
|
|
496
|
+
expect(state.items).to.deep.equal([1, 2, 3])
|
|
497
|
+
done()
|
|
498
|
+
})
|
|
499
|
+
})
|
|
500
|
+
})
|
|
501
|
+
})
|
|
502
|
+
})
|
|
503
|
+
|
|
406
504
|
it(`Doesn't clone when merging`, (done) => {
|
|
407
505
|
halfcab({
|
|
408
506
|
el: '#root',
|