halfcab 15.0.9 → 16.0.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 (3) hide show
  1. package/halfcab.mjs +58 -16
  2. package/package.json +3 -3
  3. package/test.js +50 -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 state = {}
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
- state = (dataInitial && dataInitial.dataset.initial) && Object.assign({}, JSON.parse(b64DecodeUnicode(dataInitial.dataset.initial)))
42
+ rawState = (dataInitial && dataInitial.dataset.initial) && Object.assign({}, JSON.parse(b64DecodeUnicode(dataInitial.dataset.initial)))
43
43
 
44
- if (!state.router) {
45
- state.router = {}
44
+ if (!rawState.router) {
45
+ rawState.router = {}
46
46
  }
47
47
 
48
- if (!state.router.pathname) {
49
- Object.assign(state.router, {
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,32 @@ if (typeof window !== 'undefined') {
62
62
  }
63
63
  }
64
64
 
65
+ const proxyCache = new WeakMap()
66
+
67
+ function createState (target) {
68
+ if (proxyCache.has(target)) {
69
+ return proxyCache.get(target)
70
+ }
71
+ const proxy = new Proxy(target, {
72
+ get (obj, prop) {
73
+ const value = obj[prop]
74
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
75
+ return createState(value)
76
+ }
77
+ return value
78
+ },
79
+ set (obj, prop, value) {
80
+ obj[prop] = value
81
+ debounce(stateUpdated)
82
+ return true
83
+ }
84
+ })
85
+ proxyCache.set(target, proxy)
86
+ return proxy
87
+ }
88
+
89
+ let state = createState(rawState)
90
+
65
91
  let geb = new eventEmitter({state})
66
92
 
67
93
  const stringsCache = new WeakMap()
@@ -316,19 +342,30 @@ function stateUpdated () {
316
342
  }
317
343
  }
318
344
 
345
+ function mergeInPlace (target, source) {
346
+ // Recursively merge source into target, mutating target in-place so that
347
+ // existing object references (and their Proxy wrappers) are preserved.
348
+ // Arrays are always replaced entirely — merging arrays is complex and error-prone.
349
+ for (const key of Object.keys(source)) {
350
+ const srcVal = source[key]
351
+ const tgtVal = target[key]
352
+ if (srcVal !== null && typeof srcVal === 'object' && !Array.isArray(srcVal) &&
353
+ tgtVal !== null && typeof tgtVal === 'object' && !Array.isArray(tgtVal)) {
354
+ mergeInPlace(tgtVal, srcVal)
355
+ } else {
356
+ target[key] = srcVal
357
+ }
358
+ }
359
+ }
360
+
319
361
  function updateState (updateObject, options) {
320
362
  if (updateObject) {
321
363
  if (options && options.deepMerge === false) {
322
- Object.assign(state, updateObject)
364
+ Object.assign(rawState, updateObject)
323
365
  } else {
324
- let deepMergeOptions = {clone: false}
325
- if (options && options.arrayMerge === false) {
326
- deepMergeOptions.arrayMerge = (destinationArray, sourceArray, options) => {
327
- //don't merge arrays, just return the new one
328
- return sourceArray
329
- }
330
- }
331
- Object.assign(state, merge(state, updateObject, deepMergeOptions))
366
+ // Merge in-place so existing nested object references stay intact,
367
+ // keeping Proxy cache entries valid.
368
+ mergeInPlace(rawState, updateObject, options)
332
369
  }
333
370
  }
334
371
 
@@ -344,7 +381,7 @@ function updateState (updateObject, options) {
344
381
  console.log(updateObject)
345
382
  console.log(' ')
346
383
  console.log('------NEW STATE------')
347
- console.log(state)
384
+ console.log(rawState)
348
385
  console.log(' ')
349
386
  }
350
387
 
@@ -513,6 +550,10 @@ export default (config, {shiftyRouter = shiftyRouterModule, href = hrefModule, h
513
550
  })
514
551
  }
515
552
 
553
+ /**
554
+ * @deprecated The Proxy-based state now triggers rerenders automatically via set traps.
555
+ * This function is kept for backwards compatibility but is no longer needed in most cases.
556
+ */
516
557
  function rerender () {
517
558
  debounce(stateUpdated)
518
559
  }
@@ -529,6 +570,7 @@ export {
529
570
  html,
530
571
  defineRoute,
531
572
  updateState,
573
+ createState,
532
574
  state,
533
575
  formField,
534
576
  gotoRoute,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halfcab",
3
- "version": "15.0.9",
3
+ "version": "16.0.0",
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 major --no-git-tag-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,56 @@ 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
+
406
456
  it(`Doesn't clone when merging`, (done) => {
407
457
  halfcab({
408
458
  el: '#root',