halfcab 15.0.6 → 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.
- package/README.md +14 -42
- package/halfcab.mjs +58 -16
- package/package.json +2 -2
- package/test.js +50 -0
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Halfcab is no longer built as a common js distribution.
|
|
|
25
25
|
halfcab exposes a bunch of functions and objects that you import from the halfcab module. If you want to grab them all at once ( you don't ), it'd look like this:
|
|
26
26
|
|
|
27
27
|
```js
|
|
28
|
-
import halfcab, { html, css, injectHTML, injectMarkdown, geb, eventEmitter, updateState, rerender, formField, formIsValid, fieldIsTouched, resetTouched, ssr, defineRoute, gotoRoute, http, getRouteComponent, nextTick
|
|
28
|
+
import halfcab, { html, css, injectHTML, injectMarkdown, geb, eventEmitter, updateState, rerender, formField, formIsValid, fieldIsTouched, resetTouched, ssr, defineRoute, gotoRoute, http, getRouteComponent, nextTick } from 'halfcab'
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
## Installation
|
|
@@ -55,21 +55,17 @@ halfcab({
|
|
|
55
55
|
#### Components
|
|
56
56
|
- `html` - creates dom elements from template literals
|
|
57
57
|
- `css` - injects css into html component's class property
|
|
58
|
-
- `
|
|
59
|
-
- `LRU` - nanolru: https://www.npmjs.com/package/nanolru
|
|
60
|
-
- `PureComponent` - Extends Component and only rerenders when arguments change
|
|
61
|
-
- `cachedComponent` - pair with PureComponent to automatically cache components using LRU and only rerender when arguments change
|
|
62
|
-
- `injectHTML` - injects html from a string, much like a triple mustache or React's dangerouslySetInnerHTML
|
|
58
|
+
- `injectHTML` - injects html from a string, much like a triple mustache or React's dangerouslySetInnerHTML. Uses lit-html's unsafeHTML
|
|
63
59
|
- `injectMarkdown` - the same as `injectHTML` but first converts markdown into HTML, making sure HTML entities are not double encoded.
|
|
64
60
|
|
|
65
61
|
Both injectHTML and injectMarkdown have a second argument for options. Currently there's just a single option:
|
|
66
62
|
|
|
67
63
|
```{wrapper: false}``` (default is true)
|
|
68
64
|
|
|
69
|
-
By default injected html will have a wrapping `<div>`. This is to ensure an html element can successfully be made
|
|
65
|
+
By default injected html will have a wrapping `<div>`. This is to ensure an html element can successfully be made.
|
|
70
66
|
If you know that your HTML already has a wrapping element, or it's just a single element, you can set the wrapper to false. Particularly useful when dealing with SVGs.
|
|
71
67
|
|
|
72
|
-
Under the hood, halfcab uses
|
|
68
|
+
Under the hood, halfcab uses lit-html, which turns tagged template literals into elements, and updates the DOM.
|
|
73
69
|
|
|
74
70
|
Here's an example of a simple component:
|
|
75
71
|
```js
|
|
@@ -82,7 +78,7 @@ export default args => html`
|
|
|
82
78
|
<img src="${args.company.logo.url}" />
|
|
83
79
|
</div>
|
|
84
80
|
|
|
85
|
-
<div style="width: 216px; text-align: center;"
|
|
81
|
+
<div style="width: 216px; text-align: center;" ?disabled=${args.disabled}>
|
|
86
82
|
<button onclick=${e => {
|
|
87
83
|
alert('I am a button')}}
|
|
88
84
|
>Log in <i class="material-icons" style="vertical-align: inherit">account_circle</i>
|
|
@@ -93,9 +89,10 @@ export default args => html`
|
|
|
93
89
|
`
|
|
94
90
|
|
|
95
91
|
```
|
|
96
|
-
|
|
97
92
|
This is just regular HTML with one twist - Using event handlers like onclick will use the scope of your component, not the global scope. Just don't use quotation marks around it. Put it within ${ } instead.
|
|
98
93
|
|
|
94
|
+
Note: As of version 15, to conditionally include html flags like `disabled`, halfcab now uses the lit-html syntax starting with a question mark, so like the example above, you'd have: `?disabled=${args.disabled}`, where we used to have the nanohtml syntax like this: `${args.disabled ? {disabled} : ''}`
|
|
95
|
+
|
|
99
96
|
halfcab uses csjs for inline css, like so:
|
|
100
97
|
```js
|
|
101
98
|
import { html, css } from 'halfcab'
|
|
@@ -118,7 +115,7 @@ let styles = css`
|
|
|
118
115
|
`
|
|
119
116
|
|
|
120
117
|
export default args => html`
|
|
121
|
-
<header class
|
|
118
|
+
<header class="${styles.header}">
|
|
122
119
|
<nav>
|
|
123
120
|
<div style="width: 280px;">
|
|
124
121
|
<img src="${args.company.logo.url}" />
|
|
@@ -138,24 +135,7 @@ Notice how you can use media queries, and inject variables using JavaScript! The
|
|
|
138
135
|
|
|
139
136
|
|
|
140
137
|
#### Performance
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
For convenience if you want to write pure components that are cached you can use `PureComponent` and not have to have an update method and return cached components like so:
|
|
144
|
-
|
|
145
|
-
```js
|
|
146
|
-
class DateTimePicker extends PureComponent {
|
|
147
|
-
createElement(args) {
|
|
148
|
-
this.myFunction = args.myFunction
|
|
149
|
-
return html`<div>Datepicker ${args.something}</div>`
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
export default args => cachedComponent(DateTimePicker, args, args.uniqueKey)
|
|
153
|
-
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
Make sure you have a uniqueKey (id) so that the component is properly cached and not referenced by any other call to cachedComponent
|
|
157
|
-
|
|
158
|
-
In the example above, the component that matches the uniqueKey will be extracted from the cache, the new args will be compared against the previous args, and if there's a difference, it will rerender, and if not, you'll just get the existing element from the cache. Note that this does a deep compare of objects and for functions it just copies them across. So make sure that all your functions are copied to `this`. If you see the line with `this.myFunction = args.myFunction` - this is done when the element is created, but it will also automatically be run for anything argument that is a function when performing an update. This is so that if your function argument has closed over any other variables from elsewhere, it always gets the latest function, even if it's not having to rerender the element.
|
|
138
|
+
From version 15 onwards, lit-html is used under the hood (replacing nanohtml and nanomorph) so the old caching system functions have been removed as lit-html is efficient out of the box. This keeps things really simple as you no longer need to provide uniqueKeys for component caching and can just use functions as there's no need for extending classes or managing a cache.
|
|
159
139
|
|
|
160
140
|
#### Events
|
|
161
141
|
- `geb` - global event bus
|
|
@@ -489,8 +469,6 @@ htmlTemplate.mjs
|
|
|
489
469
|
import pack from '../../../package'
|
|
490
470
|
import components from '../../../components'
|
|
491
471
|
import { ssr } from 'halfcab'
|
|
492
|
-
import { minify } from 'html-minifier'
|
|
493
|
-
|
|
494
472
|
|
|
495
473
|
function htmlOutput(data){
|
|
496
474
|
let apiData = data[0]
|
|
@@ -510,7 +488,7 @@ function htmlOutput(data){
|
|
|
510
488
|
</head>
|
|
511
489
|
<body style="padding: 0px; margin: 0px;">
|
|
512
490
|
|
|
513
|
-
|
|
491
|
+
<div id="root">${componentsString}</div>
|
|
514
492
|
|
|
515
493
|
</body>
|
|
516
494
|
</html>
|
|
@@ -518,10 +496,7 @@ function htmlOutput(data){
|
|
|
518
496
|
}
|
|
519
497
|
|
|
520
498
|
|
|
521
|
-
export default data =>
|
|
522
|
-
collapseWhitespace: true,
|
|
523
|
-
minifyCSS: true
|
|
524
|
-
})
|
|
499
|
+
export default data => htmlOutput(data)
|
|
525
500
|
```
|
|
526
501
|
|
|
527
502
|
###### Browser JS structure
|
|
@@ -550,9 +525,6 @@ halfcab({
|
|
|
550
525
|
})
|
|
551
526
|
|
|
552
527
|
```
|
|
553
|
-
Notice:
|
|
554
|
-
1. This browser code is also creating an mock function to add to the cd object, but this time, it's actually importing the real someBrowserOnlyLib library and using it before returning the element.
|
|
555
|
-
2. The halfcab function returns a promise that returns our root element ready for us to use.
|
|
556
528
|
|
|
557
529
|
###### The common file between server and browser - components.mjs
|
|
558
530
|
|
|
@@ -570,8 +542,8 @@ function products(products){
|
|
|
570
542
|
}
|
|
571
543
|
}
|
|
572
544
|
|
|
573
|
-
export default args =>
|
|
574
|
-
<div
|
|
545
|
+
export default args => html`
|
|
546
|
+
<div style="margin-top: 10px; text-align: center;">
|
|
575
547
|
${topNav({
|
|
576
548
|
company: args.company,
|
|
577
549
|
products: products(args.products)
|
|
@@ -584,7 +556,7 @@ export default args => cd.mock(html`
|
|
|
584
556
|
${footer()}
|
|
585
557
|
${injectHTML(args.safeHTMLFromServer)}
|
|
586
558
|
</div>
|
|
587
|
-
`
|
|
559
|
+
`
|
|
588
560
|
```
|
|
589
561
|
|
|
590
562
|
This is our top level component, from here we're also pulling in three other components - topNav, body, and footer. This is the start of the tree-like component structure.
|
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,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(
|
|
364
|
+
Object.assign(rawState, updateObject)
|
|
323
365
|
} 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))
|
|
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(
|
|
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": "
|
|
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,7 +11,7 @@
|
|
|
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
16
|
"npm-publish": "npm publish --tag beta",
|
|
17
17
|
"start:example": "npx --yes serve . -l 5173"
|
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',
|