halfcab 15.0.0 → 15.0.2
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/eventEmitter/test.js +1 -1
- package/example/app.js +148 -0
- package/example/index.html +50 -0
- package/example/ssr-browser-stub.js +9 -0
- package/halfcab.mjs +110 -37
- package/package.json +7 -5
- package/test.js +31 -30
package/eventEmitter/test.js
CHANGED
|
@@ -2,7 +2,7 @@ import chai from 'chai'
|
|
|
2
2
|
import dirtyChai from 'dirty-chai'
|
|
3
3
|
import sinon from 'sinon'
|
|
4
4
|
import sinonChai from 'sinon-chai'
|
|
5
|
-
import eventEmitter from './index'
|
|
5
|
+
import eventEmitter from './index.mjs'
|
|
6
6
|
import jsdomGlobal from 'jsdom-global'
|
|
7
7
|
|
|
8
8
|
const {expect} = chai
|
package/example/app.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import halfcab, {
|
|
2
|
+
html,
|
|
3
|
+
css,
|
|
4
|
+
injectMarkdown,
|
|
5
|
+
defineRoute,
|
|
6
|
+
gotoRoute,
|
|
7
|
+
updateState,
|
|
8
|
+
formField,
|
|
9
|
+
formIsValid,
|
|
10
|
+
getRouteComponent,
|
|
11
|
+
state,
|
|
12
|
+
nextTick
|
|
13
|
+
} from '../halfcab.mjs'
|
|
14
|
+
|
|
15
|
+
// Some demo styles using the css tag helper
|
|
16
|
+
const styles = css`
|
|
17
|
+
.container {
|
|
18
|
+
max-width: 880px;
|
|
19
|
+
margin: 0 auto;
|
|
20
|
+
}
|
|
21
|
+
.counter {
|
|
22
|
+
display: inline-flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: 8px;
|
|
25
|
+
padding: 8px 12px;
|
|
26
|
+
border-radius: 8px;
|
|
27
|
+
border: 1px solid #8884;
|
|
28
|
+
}
|
|
29
|
+
.navLink.active {
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
text-decoration: underline;
|
|
32
|
+
}
|
|
33
|
+
.card {
|
|
34
|
+
padding: 12px;
|
|
35
|
+
border: 1px solid #8884;
|
|
36
|
+
border-radius: 8px;
|
|
37
|
+
background: #00000008;
|
|
38
|
+
margin: 12px 0;
|
|
39
|
+
}
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
// Demo components
|
|
43
|
+
const Home = (args) => html`
|
|
44
|
+
<section class="${styles.container}">
|
|
45
|
+
<h2>Welcome to halfcab + lit demo</h2>
|
|
46
|
+
<div class="${styles.card}">
|
|
47
|
+
${injectMarkdown(`
|
|
48
|
+
### Features shown
|
|
49
|
+
- Client-side routing
|
|
50
|
+
- Global state + rerender
|
|
51
|
+
- CSS via css helper
|
|
52
|
+
- Form helpers and validation
|
|
53
|
+
- Markdown injection
|
|
54
|
+
`)}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="${styles.card}">
|
|
58
|
+
<h3>Counter</h3>
|
|
59
|
+
<div class="${styles.counter}">
|
|
60
|
+
<button onclick=${() => updateState({ count: (state.count || 0) - 1 })} aria-label="decrement">–</button>
|
|
61
|
+
<strong>${state.count || 0}</strong>
|
|
62
|
+
<button onclick=${() => updateState({ count: (state.count || 0) + 1 })} aria-label="increment">+</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
`
|
|
67
|
+
|
|
68
|
+
const About = () => html`
|
|
69
|
+
<section class="${styles.container}">
|
|
70
|
+
<h2>About</h2>
|
|
71
|
+
<p>This demo page is here to help you quickly test halfcab components and interactions.</p>
|
|
72
|
+
<p>Source: <code>example/</code> directory.</p>
|
|
73
|
+
</section>
|
|
74
|
+
`
|
|
75
|
+
|
|
76
|
+
const Form = () => {
|
|
77
|
+
// A simple form using formField + validation
|
|
78
|
+
state.form = state.form || { name: '', email: '' }
|
|
79
|
+
const bindName = formField(state.form, 'name')
|
|
80
|
+
const bindEmail = formField(state.form, 'email')
|
|
81
|
+
const submit = (e) => {
|
|
82
|
+
e.preventDefault()
|
|
83
|
+
if (formIsValid(state.form)) {
|
|
84
|
+
alert(`Submitted: ${state.form.name} <${state.form.email}>`)
|
|
85
|
+
} else {
|
|
86
|
+
alert('Form is invalid. Please fix errors.')
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return html`
|
|
90
|
+
<section class="${styles.container}">
|
|
91
|
+
<h2>Form</h2>
|
|
92
|
+
<form onsubmit=${submit} novalidate>
|
|
93
|
+
<div class="${styles.card}">
|
|
94
|
+
<label>
|
|
95
|
+
Name
|
|
96
|
+
<input type="text" required placeholder="Ada Lovelace" oninput=${bindName} value="${state.form.name}" />
|
|
97
|
+
</label>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="${styles.card}">
|
|
100
|
+
<label>
|
|
101
|
+
Email
|
|
102
|
+
<input type="email" required placeholder="ada@example.com" oninput=${bindEmail} value="${state.form.email}" />
|
|
103
|
+
</label>
|
|
104
|
+
</div>
|
|
105
|
+
<button type="submit">Submit</button>
|
|
106
|
+
</form>
|
|
107
|
+
</section>
|
|
108
|
+
`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Define routes
|
|
112
|
+
defineRoute({ path: '/', title: 'Home', component: Home })
|
|
113
|
+
defineRoute({ path: '/about', title: 'About', component: About })
|
|
114
|
+
defineRoute({ path: '/form', title: 'Form', component: Form })
|
|
115
|
+
|
|
116
|
+
// Application shell
|
|
117
|
+
const Shell = () => {
|
|
118
|
+
const pathname = (state.router && state.router.pathname) || '/'
|
|
119
|
+
const NavLink = (to, label) => html`
|
|
120
|
+
<a class="navLink ${pathname === to ? 'active' : ''}"
|
|
121
|
+
href="${to}"
|
|
122
|
+
onclick=${(e) => { e.preventDefault(); gotoRoute(to) }}>${label}</a>
|
|
123
|
+
`
|
|
124
|
+
|
|
125
|
+
const Current = getRouteComponent(pathname) || Home
|
|
126
|
+
|
|
127
|
+
return html`
|
|
128
|
+
<header>
|
|
129
|
+
${NavLink('/', 'Home')}
|
|
130
|
+
${NavLink('/about', 'About')}
|
|
131
|
+
${NavLink('/form', 'Form')}
|
|
132
|
+
</header>
|
|
133
|
+
<main>
|
|
134
|
+
${Current({})}
|
|
135
|
+
</main>
|
|
136
|
+
`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Boot the app
|
|
140
|
+
halfcab({
|
|
141
|
+
el: '#app',
|
|
142
|
+
components: Shell
|
|
143
|
+
}).then(() => {
|
|
144
|
+
// Ensure initial render after possible async state init
|
|
145
|
+
nextTick(() => {})
|
|
146
|
+
}).catch((err) => {
|
|
147
|
+
console.error('Failed to start halfcab demo:', err)
|
|
148
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>halfcab demo</title>
|
|
7
|
+
<base href="/example/">
|
|
8
|
+
<script type="importmap">
|
|
9
|
+
{
|
|
10
|
+
"imports": {
|
|
11
|
+
"shifty-router": "https://esm.sh/shifty-router@0.1.1",
|
|
12
|
+
"shifty-router/href.js": "https://esm.sh/shifty-router@0.1.1/href.js",
|
|
13
|
+
"shifty-router/history.js": "https://esm.sh/shifty-router@0.1.1/history.js",
|
|
14
|
+
"shifty-router/create-location.js": "https://esm.sh/shifty-router@0.1.1/create-location.js",
|
|
15
|
+
|
|
16
|
+
"lit": "https://esm.sh/lit@3.3.2",
|
|
17
|
+
"@lit-labs/ssr-client": "https://esm.sh/@lit-labs/ssr-client@1.1.8",
|
|
18
|
+
"@lit-labs/ssr": "/example/ssr-browser-stub.js",
|
|
19
|
+
|
|
20
|
+
"axios": "https://esm.sh/axios@0.26.1",
|
|
21
|
+
"csjs-inject": "https://esm.sh/csjs-inject@1.0.1",
|
|
22
|
+
"deepmerge": "https://esm.sh/deepmerge@4.0.0",
|
|
23
|
+
"marked": "https://esm.sh/marked@0.7.0",
|
|
24
|
+
"html-entities": "https://esm.sh/html-entities@2.3.2",
|
|
25
|
+
"qs": "https://esm.sh/qs@6.5.2",
|
|
26
|
+
"nanolru": "https://esm.sh/nanolru@1.0.0",
|
|
27
|
+
"nanocomponent": "https://esm.sh/nanocomponent@6.5.2",
|
|
28
|
+
"deep-object-diff": "https://esm.sh/deep-object-diff@1.1.0",
|
|
29
|
+
"fast-clone": "https://esm.sh/fast-clone@1.5.13",
|
|
30
|
+
"event-emitter": "https://esm.sh/event-emitter@0.3.5"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
<style>
|
|
35
|
+
:root { color-scheme: light dark; }
|
|
36
|
+
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; margin: 0; padding: 0; }
|
|
37
|
+
header { padding: 12px 16px; border-bottom: 1px solid #ccc; display: flex; gap: 12px; align-items: center; }
|
|
38
|
+
header a { text-decoration: none; color: inherit; padding: 6px 10px; border-radius: 6px; }
|
|
39
|
+
header a.active { background: rgba(0,0,0,0.08); }
|
|
40
|
+
main { padding: 16px; }
|
|
41
|
+
code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
|
42
|
+
</style>
|
|
43
|
+
</head>
|
|
44
|
+
<body>
|
|
45
|
+
<!-- Optional initial state for router bootstrapping -->
|
|
46
|
+
<div data-initial="eyJyb3V0ZXIiOnsicGF0aG5hbWUiOiIvIn19"></div>
|
|
47
|
+
<div id="app"></div>
|
|
48
|
+
<script type="module" src="./app.js"></script>
|
|
49
|
+
</body>
|
|
50
|
+
</html>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Browser stub for @lit-labs/ssr used by halfcab.mjs during client-side demo.
|
|
2
|
+
// The real SSR renderer is only needed on the server. We export a minimal
|
|
3
|
+
// compatible surface so ESM import resolution succeeds in the browser.
|
|
4
|
+
|
|
5
|
+
export function render() {
|
|
6
|
+
throw new Error("@lit-labs/ssr 'render' is a server-only API and should not be called in the browser.");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default { render };
|
package/halfcab.mjs
CHANGED
|
@@ -2,8 +2,9 @@ import shiftyRouterModule from 'shifty-router'
|
|
|
2
2
|
import hrefModule from 'shifty-router/href.js'
|
|
3
3
|
import historyModule from 'shifty-router/history.js'
|
|
4
4
|
import createLocation from 'shifty-router/create-location.js'
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import { html as litHtml, render } from 'lit'
|
|
6
|
+
import { render as renderSSR } from '@lit-labs/ssr'
|
|
7
|
+
import { hydrate } from '@lit-labs/ssr-client'
|
|
7
8
|
import axios from 'axios'
|
|
8
9
|
import cssInject from 'csjs-inject'
|
|
9
10
|
import merge from 'deepmerge'
|
|
@@ -68,20 +69,88 @@ if (typeof window !== 'undefined') {
|
|
|
68
69
|
|
|
69
70
|
let geb = new eventEmitter({state})
|
|
70
71
|
|
|
72
|
+
const stringsCache = new WeakMap()
|
|
73
|
+
|
|
71
74
|
let html = (strings, ...values) => {
|
|
72
|
-
// fix for allowing csjs to coexist with
|
|
75
|
+
// fix for allowing csjs to coexist with lit-html
|
|
73
76
|
values = values.map(value => {
|
|
74
|
-
if (value && value.hasOwnProperty('toString')) {
|
|
75
|
-
|
|
77
|
+
if (value && value.hasOwnProperty('toString') && !value.hasOwnProperty('_$litType$')) {
|
|
78
|
+
// Check if it's a template result (lit-html object). If not, and has toString (like CSJS object), stringify it.
|
|
79
|
+
if (Array.isArray(value)) return value;
|
|
80
|
+
if (typeof value === 'object' && value !== null) {
|
|
81
|
+
if (value['_$litType$'] !== undefined) return value; // It's a TemplateResult
|
|
82
|
+
// CSJS object:
|
|
83
|
+
if (value.toString && value.toString !== Object.prototype.toString) {
|
|
84
|
+
return value.toString()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
76
87
|
}
|
|
77
88
|
return value
|
|
78
89
|
})
|
|
79
90
|
|
|
80
|
-
|
|
91
|
+
// Conversion for onEvent=${fn} to @event=${fn}
|
|
92
|
+
let newStrings = stringsCache.get(strings)
|
|
93
|
+
if (!newStrings) {
|
|
94
|
+
const newRaw = strings.raw ? [...strings.raw] : [...strings]
|
|
95
|
+
const newVals = [...strings]
|
|
96
|
+
const onEventRegex = /on([a-zA-Z]+)=$/
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < newVals.length; i++) {
|
|
99
|
+
let match = newVals[i].match(onEventRegex)
|
|
100
|
+
if (match) {
|
|
101
|
+
const eventName = match[1]
|
|
102
|
+
const replacement = `@${eventName}=`
|
|
103
|
+
newVals[i] = newVals[i].replace(onEventRegex, replacement)
|
|
104
|
+
newRaw[i] = newRaw[i].replace(onEventRegex, replacement)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
newStrings = newVals
|
|
109
|
+
newStrings.raw = newRaw
|
|
110
|
+
stringsCache.set(strings, newStrings)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return litHtml(newStrings, ...values)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Detect if a container likely contains Lit SSR markers so hydration is safe
|
|
117
|
+
function canHydrateContainer (container) {
|
|
118
|
+
try {
|
|
119
|
+
if (!container || !container.hasChildNodes()) return false
|
|
120
|
+
// Walk comment nodes looking for lit markers inserted by @lit-labs/ssr
|
|
121
|
+
const walker = document.createTreeWalker(
|
|
122
|
+
container,
|
|
123
|
+
NodeFilter.SHOW_COMMENT,
|
|
124
|
+
null,
|
|
125
|
+
false
|
|
126
|
+
)
|
|
127
|
+
let n = walker.nextNode()
|
|
128
|
+
while (n) {
|
|
129
|
+
const data = (n.data || '').toLowerCase()
|
|
130
|
+
if (
|
|
131
|
+
data.includes('lit-part') ||
|
|
132
|
+
data.includes('lit$') ||
|
|
133
|
+
data.includes('lit-ssr')
|
|
134
|
+
) {
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
n = walker.nextNode()
|
|
138
|
+
}
|
|
139
|
+
} catch (e) {
|
|
140
|
+
// If anything goes wrong, err on the safe side and do not hydrate
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
return false
|
|
81
144
|
}
|
|
82
145
|
|
|
83
146
|
function ssr (rootComponent) {
|
|
84
|
-
|
|
147
|
+
// Use @lit-labs/ssr render
|
|
148
|
+
// It returns an iterable
|
|
149
|
+
const resultIterator = renderSSR(rootComponent)
|
|
150
|
+
let componentsString = ''
|
|
151
|
+
for (const chunk of resultIterator) {
|
|
152
|
+
componentsString += chunk
|
|
153
|
+
}
|
|
85
154
|
return {componentsString, stylesString: componentCSSString}
|
|
86
155
|
}
|
|
87
156
|
|
|
@@ -243,11 +312,12 @@ function nextTick (func) {
|
|
|
243
312
|
function stateUpdated () {
|
|
244
313
|
if (rootEl) {
|
|
245
314
|
let startTime = Date.now()
|
|
246
|
-
let
|
|
315
|
+
let newTemplate = components(state)
|
|
247
316
|
console.log(`Component render: ${Date.now() - startTime}`)
|
|
248
317
|
startTime = Date.now()
|
|
249
|
-
|
|
250
|
-
|
|
318
|
+
// Render into the container (rootEl)
|
|
319
|
+
render(newTemplate, rootEl)
|
|
320
|
+
console.log(`DOM update: ${Date.now() - startTime}`)
|
|
251
321
|
}
|
|
252
322
|
}
|
|
253
323
|
|
|
@@ -273,7 +343,8 @@ function updateState (updateObject, options) {
|
|
|
273
343
|
|
|
274
344
|
debounce(stateUpdated)
|
|
275
345
|
|
|
276
|
-
|
|
346
|
+
// Avoid referencing process in browsers without a bundler (process is undefined)
|
|
347
|
+
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production') {
|
|
277
348
|
console.log('------STATE UPDATE------')
|
|
278
349
|
console.log(updateObject)
|
|
279
350
|
console.log(' ')
|
|
@@ -286,31 +357,20 @@ function updateState (updateObject, options) {
|
|
|
286
357
|
}
|
|
287
358
|
|
|
288
359
|
function emptySSRVideos (c) {
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
let autoplayAutoplay = c.querySelectorAll('video[autoplay="autoplay"]')
|
|
293
|
-
let autoplayOn = c.querySelectorAll('video[autoplay="on"]')
|
|
294
|
-
let selectors = [autoplayTrue, autoplayAutoplay, autoplayOn]
|
|
295
|
-
selectors.forEach(selector => {
|
|
296
|
-
Array.from(selector).forEach(video => {
|
|
297
|
-
video.pause()
|
|
298
|
-
Array.from(video.childNodes).forEach(source => {
|
|
299
|
-
source.src && (source.src = '')
|
|
300
|
-
})
|
|
301
|
-
})
|
|
302
|
-
})
|
|
360
|
+
// This was for nanomorph. Lit handles updates differently.
|
|
361
|
+
// If we need to manipulate DOM before render, it's harder with Templates.
|
|
362
|
+
// Leaving empty or deprecated.
|
|
303
363
|
}
|
|
304
364
|
|
|
305
365
|
function injectHTML (htmlString, options) {
|
|
306
366
|
if (options && options.wrapper === false) {
|
|
307
367
|
return html([htmlString])
|
|
308
368
|
}
|
|
309
|
-
return html([`<div>${htmlString}</div>`])
|
|
369
|
+
return html([`<div>${htmlString}</div>`])
|
|
310
370
|
}
|
|
311
371
|
|
|
312
372
|
function injectMarkdown (mdString, options) {
|
|
313
|
-
return injectHTML(decode(marked(mdString)), options)
|
|
373
|
+
return injectHTML(decode(marked(mdString)), options)
|
|
314
374
|
}
|
|
315
375
|
|
|
316
376
|
function gotoRoute (route) {
|
|
@@ -422,18 +482,30 @@ export default (config, {shiftyRouter = shiftyRouterModule, href = hrefModule, h
|
|
|
422
482
|
gotoRoute(location.href)
|
|
423
483
|
})
|
|
424
484
|
|
|
425
|
-
let c = components(state)//
|
|
485
|
+
let c = components(state)// component template
|
|
426
486
|
if (el) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
487
|
+
// rootEl is the container
|
|
488
|
+
rootEl = document.querySelector(el)
|
|
489
|
+
|
|
490
|
+
// Initial render. Only hydrate when container has Lit SSR markers.
|
|
491
|
+
if (canHydrateContainer(rootEl)) {
|
|
492
|
+
try {
|
|
493
|
+
hydrate(c, rootEl)
|
|
494
|
+
} catch (e) {
|
|
495
|
+
// Fallback to render if hydration fails (or if not SSR'd by Lit)
|
|
496
|
+
console.warn('Hydration failed or not applicable, falling back to render', e)
|
|
497
|
+
render(c, rootEl)
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
render(c, rootEl)
|
|
501
|
+
}
|
|
502
|
+
|
|
432
503
|
return resolve({rootEl, state})
|
|
433
504
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
//
|
|
505
|
+
// If no root element provided?
|
|
506
|
+
rootEl = null
|
|
507
|
+
// We return 'c' which is now a TemplateResult.
|
|
508
|
+
resolve({rootEl: c, state})
|
|
437
509
|
})
|
|
438
510
|
}
|
|
439
511
|
|
|
@@ -486,6 +558,7 @@ export {
|
|
|
486
558
|
html,
|
|
487
559
|
defineRoute,
|
|
488
560
|
updateState,
|
|
561
|
+
state,
|
|
489
562
|
formField,
|
|
490
563
|
gotoRoute,
|
|
491
564
|
cssTag as css,
|
|
@@ -499,4 +572,4 @@ export {
|
|
|
499
572
|
LRU,
|
|
500
573
|
cachedComponent,
|
|
501
574
|
PureComponent
|
|
502
|
-
}
|
|
575
|
+
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "halfcab",
|
|
3
|
-
"version": "15.0.
|
|
3
|
+
"version": "15.0.2",
|
|
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",
|
|
7
7
|
"module": "halfcab.mjs",
|
|
8
8
|
"jsnext:main": "halfcab.mjs",
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "mocha
|
|
10
|
+
"test": "mocha './{,!(node_modules)/**}/test.js'",
|
|
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
14
|
"versionbump:feature": "npm version major --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
|
+
"start:example": "npx --yes serve . -l 5173"
|
|
17
18
|
},
|
|
18
19
|
"repository": {
|
|
19
20
|
"type": "git",
|
|
@@ -47,6 +48,8 @@
|
|
|
47
48
|
"sinon-chai": "^3.0.0"
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
51
|
+
"@lit-labs/ssr": "^4.0.0",
|
|
52
|
+
"@lit-labs/ssr-client": "^1.1.8",
|
|
50
53
|
"axios": "^0.26.1",
|
|
51
54
|
"csjs-inject": "^1.0.1",
|
|
52
55
|
"deep-object-diff": "^1.1.0",
|
|
@@ -54,11 +57,10 @@
|
|
|
54
57
|
"event-emitter": "^0.3.5",
|
|
55
58
|
"fast-clone": "^1.5.13",
|
|
56
59
|
"html-entities": "^2.3.2",
|
|
60
|
+
"lit": "^3.3.2",
|
|
57
61
|
"marked": "^0.7.0",
|
|
58
62
|
"nanocomponent": "^6.5.2",
|
|
59
|
-
"nanohtml": "^1.6.3",
|
|
60
63
|
"nanolru": "^1.0.0",
|
|
61
|
-
"nanomorph": "^5.4.0",
|
|
62
64
|
"qs": "^6.5.2",
|
|
63
65
|
"shifty-router": "^0.1.1"
|
|
64
66
|
},
|
package/test.js
CHANGED
|
@@ -3,25 +3,12 @@ import dirtyChai from 'dirty-chai'
|
|
|
3
3
|
import sinon from 'sinon'
|
|
4
4
|
import sinonChai from 'sinon-chai'
|
|
5
5
|
import jsdomGlobal from 'jsdom-global'
|
|
6
|
-
import server from 'nanohtml/lib/server'
|
|
7
6
|
|
|
8
7
|
const {expect} = chai
|
|
9
8
|
chai.use(dirtyChai)
|
|
10
9
|
chai.use(sinonChai)
|
|
11
10
|
chai.use(dirtyChai)
|
|
12
11
|
|
|
13
|
-
let serverHtml = (strings, ...values) => {
|
|
14
|
-
// this duplicates the halfcab html function but uses pelo instead of nanohtml
|
|
15
|
-
values = values.map(value => {
|
|
16
|
-
if (value && value.hasOwnProperty('toString')) {
|
|
17
|
-
return value.toString()
|
|
18
|
-
}
|
|
19
|
-
return value
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
return server(strings, ...values)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
12
|
let halfcab, ssr, html, defineRoute, gotoRoute, formField, cache, updateState, injectMarkdown, formIsValid,
|
|
26
13
|
css, state, getRouteComponent, nextTick
|
|
27
14
|
|
|
@@ -29,6 +16,11 @@ function intialData (dataInitial) {
|
|
|
29
16
|
let el = document.createElement('div')
|
|
30
17
|
el.setAttribute('data-initial', dataInitial)
|
|
31
18
|
document.body.appendChild(el)
|
|
19
|
+
|
|
20
|
+
// Also ensure a root element exists for tests that target #root
|
|
21
|
+
let root = document.createElement('div')
|
|
22
|
+
root.id = 'root'
|
|
23
|
+
document.body.appendChild(root)
|
|
32
24
|
}
|
|
33
25
|
|
|
34
26
|
describe('halfcab', () => {
|
|
@@ -36,8 +28,8 @@ describe('halfcab', () => {
|
|
|
36
28
|
describe('Server', () => {
|
|
37
29
|
before(async () => {
|
|
38
30
|
jsdomGlobal()
|
|
39
|
-
intialData('
|
|
40
|
-
let halfcabModule = await import('./halfcab')
|
|
31
|
+
intialData('eyJjb250YWN0Ijp7InRpdGxlIjoiQ29udGFjdCBVcyJ9LCJyb3V0ZXIiOnsicGF0aG5hbWUiOiIvIn19')
|
|
32
|
+
let halfcabModule = await import('./halfcab.mjs')
|
|
41
33
|
;({
|
|
42
34
|
ssr,
|
|
43
35
|
html,
|
|
@@ -60,8 +52,10 @@ describe('halfcab', () => {
|
|
|
60
52
|
width: 100px;
|
|
61
53
|
}
|
|
62
54
|
`
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
// Use html instead of serverHtml (which depended on nanohtml)
|
|
56
|
+
// lit-html on server via ssr() should return string
|
|
57
|
+
let {componentsString, stylesString} = ssr(html`
|
|
58
|
+
<div class="${style.myStyle}" @input=${() => {
|
|
65
59
|
}}></div>
|
|
66
60
|
`)
|
|
67
61
|
expect(typeof componentsString === 'string').to.be.true()
|
|
@@ -72,8 +66,8 @@ describe('halfcab', () => {
|
|
|
72
66
|
|
|
73
67
|
before(async () => {
|
|
74
68
|
jsdomGlobal()
|
|
75
|
-
intialData('
|
|
76
|
-
let halfcabModule = await import('./halfcab')
|
|
69
|
+
intialData('eyJjb250YWN0Ijp7InRpdGxlIjoiQ29udGFjdCBVcyJ9LCJyb3V0ZXIiOnsicGF0aG5hbWUiOiIvIn19')
|
|
70
|
+
let halfcabModule = await import('./halfcab.mjs')
|
|
77
71
|
;({
|
|
78
72
|
ssr,
|
|
79
73
|
html,
|
|
@@ -91,20 +85,21 @@ describe('halfcab', () => {
|
|
|
91
85
|
halfcab = halfcabModule.default
|
|
92
86
|
})
|
|
93
87
|
|
|
94
|
-
it('Produces
|
|
88
|
+
it('Produces a TemplateResult when rendering', () => {
|
|
95
89
|
let el = html`
|
|
96
|
-
<div
|
|
90
|
+
<div @input=${() => {
|
|
97
91
|
}}></div>
|
|
98
92
|
`
|
|
99
|
-
|
|
93
|
+
// Check for Lit TemplateResult marker
|
|
94
|
+
expect(el['_$litType$']).to.exist()
|
|
100
95
|
})
|
|
101
96
|
|
|
102
|
-
it('Produces
|
|
97
|
+
it('Produces a TemplateResult wrapping as a reusable component', () => {
|
|
103
98
|
let el = args => html`
|
|
104
|
-
<div
|
|
99
|
+
<div @input=${() => {
|
|
105
100
|
}}></div>
|
|
106
101
|
`
|
|
107
|
-
expect(el({})
|
|
102
|
+
expect(el({})['_$litType$']).to.exist()
|
|
108
103
|
})
|
|
109
104
|
|
|
110
105
|
it('Runs halfcab function without error', () => {
|
|
@@ -114,13 +109,14 @@ describe('halfcab', () => {
|
|
|
114
109
|
return html `<div></div>`
|
|
115
110
|
}
|
|
116
111
|
})
|
|
117
|
-
.then(rootEl => {
|
|
112
|
+
.then(({rootEl}) => {
|
|
118
113
|
expect(typeof rootEl === 'object').to.be.true()
|
|
119
114
|
})
|
|
120
115
|
})
|
|
121
116
|
|
|
122
117
|
it('updating state causes a rerender with state', (done) => {
|
|
123
118
|
halfcab({
|
|
119
|
+
el: '#root', // Ensure el is passed so rootEl is the container
|
|
124
120
|
components (args) {
|
|
125
121
|
return html`<div>${args.testing || ''}</div>`
|
|
126
122
|
}
|
|
@@ -137,6 +133,7 @@ describe('halfcab', () => {
|
|
|
137
133
|
|
|
138
134
|
it('updates state without merging arrays when told to', () => {
|
|
139
135
|
return halfcab({
|
|
136
|
+
el: '#root',
|
|
140
137
|
components () {
|
|
141
138
|
return html `<div></div>`
|
|
142
139
|
}
|
|
@@ -163,6 +160,7 @@ describe('halfcab', () => {
|
|
|
163
160
|
}
|
|
164
161
|
`
|
|
165
162
|
return halfcab({
|
|
163
|
+
el: '#root',
|
|
166
164
|
components (args) {
|
|
167
165
|
return html `<div class="${style.myStyle}">${args.testing.inner || ''}</div>`
|
|
168
166
|
}
|
|
@@ -178,6 +176,7 @@ describe('halfcab', () => {
|
|
|
178
176
|
|
|
179
177
|
it('injects external content without error', () => {
|
|
180
178
|
return halfcab({
|
|
179
|
+
el: '#root',
|
|
181
180
|
components (args) {
|
|
182
181
|
return html `<div>${injectMarkdown('### Heading')}</div>`
|
|
183
182
|
}
|
|
@@ -190,6 +189,7 @@ describe('halfcab', () => {
|
|
|
190
189
|
|
|
191
190
|
it('injects markdown without wrapper without error', () => {
|
|
192
191
|
return halfcab({
|
|
192
|
+
el: '#root',
|
|
193
193
|
components (args) {
|
|
194
194
|
return html `<div>${injectMarkdown('### Heading', {wrapper: false})}</div>`
|
|
195
195
|
}
|
|
@@ -362,6 +362,7 @@ describe('halfcab', () => {
|
|
|
362
362
|
})
|
|
363
363
|
|
|
364
364
|
return halfcab({
|
|
365
|
+
el: '#root',
|
|
365
366
|
components () {
|
|
366
367
|
return html `<div></div>`
|
|
367
368
|
}
|
|
@@ -371,8 +372,7 @@ describe('halfcab', () => {
|
|
|
371
372
|
let routing = () => {
|
|
372
373
|
gotoRoute('/testFakeRoute')
|
|
373
374
|
}
|
|
374
|
-
expect(routing).to.not.throw()
|
|
375
|
-
// be fine
|
|
375
|
+
expect(routing).to.not.throw()
|
|
376
376
|
})
|
|
377
377
|
|
|
378
378
|
})
|
|
@@ -380,6 +380,7 @@ describe('halfcab', () => {
|
|
|
380
380
|
it(`Throws an error when a route doesn't exist`, () => {
|
|
381
381
|
|
|
382
382
|
return halfcab({
|
|
383
|
+
el: '#root',
|
|
383
384
|
components () {
|
|
384
385
|
return html `<div></div>`
|
|
385
386
|
}
|
|
@@ -388,8 +389,7 @@ describe('halfcab', () => {
|
|
|
388
389
|
let routing = () => {
|
|
389
390
|
gotoRoute('/thisIsAFakeRoute')
|
|
390
391
|
}
|
|
391
|
-
expect(routing).to.throw()
|
|
392
|
-
// fine
|
|
392
|
+
expect(routing).to.throw()
|
|
393
393
|
})
|
|
394
394
|
|
|
395
395
|
})
|
|
@@ -405,6 +405,7 @@ describe('halfcab', () => {
|
|
|
405
405
|
|
|
406
406
|
it(`Doesn't clone when merging`, (done) => {
|
|
407
407
|
halfcab({
|
|
408
|
+
el: '#root',
|
|
408
409
|
components () {
|
|
409
410
|
return html `<div></div>`
|
|
410
411
|
}
|