halfcab 14.0.5 → 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/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,11 +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 { html as litHtml, render, nothing, noChange } from 'lit-html'
6
- import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'
7
- import { repeat } from 'lit-html/directives/repeat.js'
8
- import { classMap } from 'lit-html/directives/class-map.js'
9
- import { styleMap } from 'lit-html/directives/style-map.js'
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'
10
8
  import axios from 'axios'
11
9
  import cssInject from 'csjs-inject'
12
10
  import merge from 'deepmerge'
@@ -14,9 +12,12 @@ import marked from 'marked'
14
12
  import { decode } from 'html-entities'
15
13
  import eventEmitter from './eventEmitter/index.mjs'
16
14
  import qs from 'qs'
15
+ import LRU from 'nanolru'
16
+ import Component from 'nanocomponent'
17
17
  import * as deepDiff from 'deep-object-diff'
18
18
  import clone from 'fast-clone'
19
- import LRU from 'nanolru'
19
+
20
+ const cache = LRU(5000)
20
21
 
21
22
  let cssTag = cssInject
22
23
  let componentCSSString = ''
@@ -28,7 +29,6 @@ let rootEl
28
29
  let components
29
30
  let dataInitial
30
31
  let el
31
- let componentIndex = 0
32
32
 
33
33
  marked.setOptions({
34
34
  breaks: true
@@ -69,97 +69,88 @@ if (typeof window !== 'undefined') {
69
69
 
70
70
  let geb = new eventEmitter({state})
71
71
 
72
+ const stringsCache = new WeakMap()
73
+
72
74
  let html = (strings, ...values) => {
73
75
  // fix for allowing csjs to coexist with lit-html
74
76
  values = values.map(value => {
75
- // Check if it's a CSJS object (has custom toString and isn't a TemplateResult)
76
- // TemplateResult usually has 'strings' and 'values' or '_$litType$'
77
- // DirectiveResult (unsafeHTML) uses default toString, so we shouldn't call it.
78
- if (value && typeof value !== 'function' && !Array.isArray(value) && typeof value.toString === 'function' && value.toString !== Object.prototype.toString && !value.strings && !value._$litType$) {
79
- return value.toString()
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
+ }
80
87
  }
81
88
  return value
82
89
  })
83
90
 
84
- return litHtml(strings, ...values)
85
- }
86
-
87
- // Capture directive classes for SSR identification
88
- const RepeatDirective = repeat([], () => {})['_$litDirective$']
89
- const ClassMapDirective = classMap({})['_$litDirective$']
90
- const StyleMapDirective = styleMap({})['_$litDirective$']
91
-
92
- function resolveTemplate (value) {
93
- if (value === nothing || value === noChange) {
94
- return ''
95
- }
96
-
97
- if (Array.isArray(value)) {
98
- return value.map(resolveTemplate).join('')
99
- }
100
- if (value && typeof value === 'object') {
101
- if (value.strings) {
102
- let result = ''
103
- const { strings, values } = value
104
- for (let i = 0; i < strings.length; i++) {
105
- result += strings[i]
106
- if (i < values.length) {
107
- result += resolveTemplate(values[i])
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)
108
105
  }
109
- }
110
- return result
111
106
  }
107
+
108
+ newStrings = newVals
109
+ newStrings.raw = newRaw
110
+ stringsCache.set(strings, newStrings)
111
+ }
112
112
 
113
- if (value['_$litDirective$']) {
114
- const directiveClass = value['_$litDirective$']
115
- if (directiveClass.directiveName === 'unsafeHTML' && value.values && value.values.length > 0) {
116
- return String(value.values[0])
117
- }
118
-
119
- if (directiveClass === RepeatDirective) {
120
- const items = value.values[0]
121
- const templateFn = value.values[value.values.length - 1]
122
- if (items && typeof templateFn === 'function') {
123
- return Array.from(items).map((item, index) => resolveTemplate(templateFn(item, index))).join('')
124
- }
125
- return ''
126
- }
127
-
128
- if (directiveClass === ClassMapDirective) {
129
- const classObj = value.values[0]
130
- if (typeof classObj === 'object') {
131
- return Object.keys(classObj)
132
- .filter(key => classObj[key])
133
- .join(' ')
134
- }
135
- return ''
136
- }
113
+ return litHtml(newStrings, ...values)
114
+ }
137
115
 
138
- if (directiveClass === StyleMapDirective) {
139
- const styleObj = value.values[0]
140
- if (typeof styleObj === 'object') {
141
- return Object.keys(styleObj)
142
- .map(key => `${key}:${styleObj[key]}`)
143
- .join(';')
144
- }
145
- return ''
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
146
136
  }
137
+ n = walker.nextNode()
147
138
  }
139
+ } catch (e) {
140
+ // If anything goes wrong, err on the safe side and do not hydrate
141
+ return false
148
142
  }
149
-
150
- if (typeof value === 'function') {
151
- return ''
152
- }
153
-
154
- return value === undefined || value === null ? '' : String(value)
143
+ return false
155
144
  }
156
145
 
157
146
  function ssr (rootComponent) {
158
- // Simple fallback for SSR since lit-html produces objects
147
+ // Use @lit-labs/ssr render
148
+ // It returns an iterable
149
+ const resultIterator = renderSSR(rootComponent)
159
150
  let componentsString = ''
160
- try {
161
- componentsString = resolveTemplate(rootComponent)
162
- } catch (e) {}
151
+ for (const chunk of resultIterator) {
152
+ componentsString += chunk
153
+ }
163
154
  return {componentsString, stylesString: componentCSSString}
164
155
  }
165
156
 
@@ -320,13 +311,13 @@ function nextTick (func) {
320
311
 
321
312
  function stateUpdated () {
322
313
  if (rootEl) {
323
- componentIndex = 0
324
314
  let startTime = Date.now()
325
- let newEl = components(state)
315
+ let newTemplate = components(state)
326
316
  console.log(`Component render: ${Date.now() - startTime}`)
327
317
  startTime = Date.now()
328
- render(newEl, rootEl)
329
- console.log(`DOM morph: ${Date.now() - startTime}`)
318
+ // Render into the container (rootEl)
319
+ render(newTemplate, rootEl)
320
+ console.log(`DOM update: ${Date.now() - startTime}`)
330
321
  }
331
322
  }
332
323
 
@@ -352,7 +343,8 @@ function updateState (updateObject, options) {
352
343
 
353
344
  debounce(stateUpdated)
354
345
 
355
- if (process.env.NODE_ENV !== 'production') {
346
+ // Avoid referencing process in browsers without a bundler (process is undefined)
347
+ if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production') {
356
348
  console.log('------STATE UPDATE------')
357
349
  console.log(updateObject)
358
350
  console.log(' ')
@@ -365,32 +357,20 @@ function updateState (updateObject, options) {
365
357
  }
366
358
 
367
359
  function emptySSRVideos (c) {
368
- //SSR videos with source tags don't like morphing and you get double audio,
369
- // so remove src from the new one so it never starts
370
- if (!c || !c.querySelectorAll) return
371
- let autoplayTrue = c.querySelectorAll('video[autoplay="true"]')
372
- let autoplayAutoplay = c.querySelectorAll('video[autoplay="autoplay"]')
373
- let autoplayOn = c.querySelectorAll('video[autoplay="on"]')
374
- let selectors = [autoplayTrue, autoplayAutoplay, autoplayOn]
375
- selectors.forEach(selector => {
376
- Array.from(selector).forEach(video => {
377
- video.pause()
378
- Array.from(video.childNodes).forEach(source => {
379
- source.src && (source.src = '')
380
- })
381
- })
382
- })
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.
383
363
  }
384
364
 
385
365
  function injectHTML (htmlString, options) {
386
366
  if (options && options.wrapper === false) {
387
- return unsafeHTML(htmlString)
367
+ return html([htmlString])
388
368
  }
389
- return html`<div>${unsafeHTML(htmlString)}</div>`
369
+ return html([`<div>${htmlString}</div>`])
390
370
  }
391
371
 
392
372
  function injectMarkdown (mdString, options) {
393
- return injectHTML(decode(marked(mdString)), options)
373
+ return injectHTML(decode(marked(mdString)), options)
394
374
  }
395
375
 
396
376
  function gotoRoute (route) {
@@ -502,26 +482,30 @@ export default (config, {shiftyRouter = shiftyRouterModule, href = hrefModule, h
502
482
  gotoRoute(location.href)
503
483
  })
504
484
 
505
- componentIndex = 0
506
- let c = components(state)//root element generated by components
485
+ let c = components(state)// component template
507
486
  if (el) {
508
-
509
- // emptySSRVideos(c)
510
-
511
- let r = document.querySelector(el)
512
- if (!r) {
513
- // Fallback if element not found
514
- rootEl = document.createElement('div')
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
+ }
515
499
  } else {
516
- rootEl = r
500
+ render(c, rootEl)
517
501
  }
518
- render(c, rootEl)
502
+
519
503
  return resolve({rootEl, state})
520
504
  }
521
- rootEl = document.createElement('div')
522
- render(c, rootEl)
523
- resolve({rootEl, state})//if no root element provided, just return the root
524
- // component and the state
505
+ // If no root element provided?
506
+ rootEl = null
507
+ // We return 'c' which is now a TemplateResult.
508
+ resolve({rootEl: c, state})
525
509
  })
526
510
  }
527
511
 
@@ -529,20 +513,39 @@ function rerender () {
529
513
  debounce(stateUpdated)
530
514
  }
531
515
 
532
- class Component {
533
- render(args) {
534
- if (this.createElement) return this.createElement(args)
535
- return html``
516
+ class PureComponent extends Component {
517
+ createElement (args) {
518
+ this.args = clone(args)
536
519
  }
537
- }
538
520
 
539
- class PureComponent extends Component {}
521
+ update (args) {
522
+ let diff = deepDiff.diff(this.args, args)
523
+ Object.keys(diff).forEach(key => {
524
+ if (typeof diff[key] === 'function') {
525
+ this[key] = args[key]
526
+ }
527
+ })
528
+ return !!Object.keys(diff).find(key => typeof diff[key] !== 'function')
529
+ }
530
+ }
540
531
 
541
532
  function cachedComponent (Class, args, id) {
542
- return new Class().render(args)
533
+ let instance
534
+ if (id) {
535
+ let found = cache.get(id)
536
+ if (found) {
537
+ instance = found
538
+ } else {
539
+ instance = new Class()
540
+ cache.set(id, instance)
541
+ }
542
+ return instance.render(args)
543
+ } else {
544
+ instance = new Class()
545
+ return instance.createElement(args)
546
+ }
543
547
  }
544
548
 
545
-
546
549
  export {
547
550
  getRouteComponent,
548
551
  rerender,
@@ -555,6 +558,7 @@ export {
555
558
  html,
556
559
  defineRoute,
557
560
  updateState,
561
+ state,
558
562
  formField,
559
563
  gotoRoute,
560
564
  cssTag as css,
@@ -568,4 +572,4 @@ export {
568
572
  LRU,
569
573
  cachedComponent,
570
574
  PureComponent
571
- }
575
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halfcab",
3
- "version": "14.0.5",
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",
@@ -11,9 +11,10 @@
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 minor --no-git-tag-version",
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,12 +57,10 @@
54
57
  "event-emitter": "^0.3.5",
55
58
  "fast-clone": "^1.5.13",
56
59
  "html-entities": "^2.3.2",
57
- "lit-html": "^3.3.1",
60
+ "lit": "^3.3.2",
58
61
  "marked": "^0.7.0",
59
62
  "nanocomponent": "^6.5.2",
60
- "nanohtml": "^1.6.3",
61
63
  "nanolru": "^1.0.0",
62
- "nanomorph": "^5.4.0",
63
64
  "qs": "^6.5.2",
64
65
  "shifty-router": "^0.1.1"
65
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.js'
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,7 +28,7 @@ describe('halfcab', () => {
36
28
  describe('Server', () => {
37
29
  before(async () => {
38
30
  jsdomGlobal()
39
- intialData('eyJjb250YWN0Rm9ybSI6eyJzZW5kRGlzYWJsZWQiOmZhbHNlLCJzaG93VGhhbmtzIjpmYWxzZX0sImxvZ2luIjp7ImRpc2FibGVkIjpmYWxzZX0sImxvYWRpbmciOmZhbHNlLCJzaG93Q29udGFjdCI6dHJ1ZSwicHJvZHVjdHMiOlt7Im5hbWUiOiJTZWVNb25zdGVyIiwicHJvZHVjdFR5cGUiOiJEaWdpdGFsIFNpZ25hZ2UiLCJkZXNjcmlwdGlvbiI6IkRpZ2l0YWwgc2lnbmFnZSBidWlsdCBmb3Igc2tpIGFyZWFzLiIsInByaWNlIjoiJDMsMjAwIFVTRCBwZXIgd2ludGVyIiwibG9nbyI6eyJ1cmwiOiIvL2ltYWdlcy5jb250ZW50ZnVsLmNvbS82cDhvaHhmaWthazEvNmdaY1A2NG1UbTIybTAyWWFvTUdPUS9kN2ZlZjkwZWVhZjI5MmQwNDcwYzNiNjlhODJiMmM2NS9zZWVtb25zdGVyLnN2ZyIsIm5hbWUiOiJTZWVNb25zdGVyIExvZ28ifSwiZGV0YWlsU2VjdGlvbnMiOlt7Im5hbWUiOiJXaGF0IGlzIFNlZU1vbnN0ZXIiLCJkZXNjcmlwdGlvbiI6IiMjIyBTZWVNb25zdGVyIGVuYWJsZXMgeW91IHRvIGRpc3BsYXkgZHluYW1pYyBtZXNzYWdlcyBvbiBzY3JlZW5zIGFyb3VuZCB5b3VyIHNraSBhcmVhIGFuZCBvbiBob3RlbCByb29tIFRWcywgYWxsIG1hbmFnZWQgb3ZlciB0aGUgSW50ZXJuZXQuIiwibGlzdCI6IiIsIm1lZGlhIjp7ImZpbGUiOiIvL2ltYWdlcy5jb250ZW50ZnVsLmNvbS82cDhvaHhmaWthazEvYmZtQU5QdE1mUUNRdVE0UWthV1VFLzI0OTY0MjU3OGJmYTBlYzE3M2JlMzhjYTkzMjRhYTEzL3NlZW1vbnN0ZXItMS0yLTMucG5nIiwid2lkdGgiOjg2Nn0sImxpbmsiOiIifSx7Im5hbWUiOiJTZWVNb25zdGVyIEluY2x1ZGVzIiwiZGVzY3JpcHRpb24iOiIiLCJsaXN0IjpbIlBsYXlzIG9uIGFueSBjb21wdXRlciAmIFRWIGNvbWJvIiwiQmFuZHdpZHRoLCBzdG9yYWdlICYgc3VwcG9ydCBpbmNsdWRlZCIsIkVhc3kgZHJhZyBhbmQgZHJvcCBhZG1pbi4iLCJCdWlsdCBpbiBhbmltYXRpb24uIiwiVXBsb2FkIGltYWdlcyBhbmQgdmlkZW8gaW4gYW55IGZvcm1hdCIsIlVzZSB3aXRoIGJvdGggZGlnaXRhbCBzaWducyBhbmQgaW4tcm9vbSBUViBzeXN0ZW1zIiwiVXNlIGxpdmUgZGF0YSB0byBwb3B1bGF0ZSB0ZXh0IGZpZWxkcyAmIHN3YXAgb3V0IGltYWdlcyIsIlJlbW90ZWx5IGNvbnRyb2wgeW91ciBzY3JlZW5zIiwiU2NoZWR1bGUgY29udGVudCIsIlNvY2lhbCBtZWRpYSBmZWVkcyIsIkRpc3BsYXkgdmlkZW9zIGFuZCBwaG90b3MiLCJNYXBzLCBsaWZ0ICYgdHJhaWwgc3RhdHVzIl0sIm1lZGlhIjp7ImZpbGUiOiIvL2ltYWdlcy5jb250ZW50ZnVsLmNvbS82cDhvaHhmaWthazEvM3kwQWFQUE16Q0dLaXVLY011ZWEyUS9mNTJiNTc5ZGM5YTIwODBlNDg0OWVlODcyMjYxMjRjMi9RVC13ZWF0aGVyZm9yZWNhc3Qtc2NyZWVuLmpwZyIsIndpZHRoIjo2Njd9LCJsaW5rIjoiIn1dfSx7Im5hbWUiOiJ2aWNvTWFwIiwicHJvZHVjdFR5cGUiOiJJbnRlcmFjdGl2ZSBUcmFpbCBNYXAiLCJkZXNjcmlwdGlvbiI6IipUaGUqIGludGVyYWN0aXZlIG1hcCBmb3Igc2tpIGFyZWFzLiIsInByaWNlIjoiJDIsODAwIFVTRCBwZXIgd2ludGVyIiwibG9nbyI6eyJ1cmwiOiIvL2ltYWdlcy5jb250ZW50ZnVsLmNvbS82cDhvaHhmaWthazEvMXNDOU4xV0hXODJBV2tvZUljS1FZMi82ODhjYmI5NmU3YmU2MDJkZDFmOWJmYmFmZjI0ZTI5MC92aWNvbWFwLnN2ZyIsIm5hbWUiOiJ2aWNvTWFwIExvZ28ifSwiZGV0YWlsU2VjdGlvbnMiOlt7Im5hbWUiOiJEZW1vIiwiZGVzY3JpcHRpb24iOiIqVGhlIGJlc3Qgd2F5IHRvIGNoZWNrIG91dCB2aWNvTWFwLCBpcyB0byBqdXN0IHN0YXJ0IHVzaW5nIGl0ISBIYXZlIGEgZ28gd2l0aCB0aGlzIG9uZSBhbmQgbGV0IHVzIGtub3cgd2hhdCB5b3UgdGhpbmsuKiIsImxpc3QiOiIiLCJtZWRpYSI6IiIsImxpbmsiOiJodHRwczovL3ZpY29tYXAtY2RuLnJlc29ydHMtaW50ZXJhY3RpdmUuY29tL21hcC8xMiJ9LHsibmFtZSI6InZpY29NYXAgaW5jbHVkZXMiLCJkZXNjcmlwdGlvbiI6IiIsImxpc3QiOlsiUmVzcG9uc2l2ZSAod29ya3Mgb24gZGVza3RvcCBhbmQgbW9iaWxlIGRldmljZXMpIiwiTGl2ZSBsaWZ0IGFuZCB0cmFpbCBkYXRhIChmcmVlIGJhc2ljIFJlcG9ydCBQYWwgY29ubmVjdG9yIGluY2x1ZGVkKSIsIkNsb3VkIGhvc3RlZCAoQVdTKSIsIkJhbmR3aWR0aCwgc3RvcmFnZSAmIHN1cHBvcnQgaW5jbHVkZWQiLCJFYXN5IHdlYnNpdGUgZW1iZWQiLCJObyBzZXR1cCBjb3N0IC0gYnVpbHQgZnJvbSB5b3VyIGV4aXN0aW5nIElsbHVzdHJhdG9yIGZpbGUiXSwibWVkaWEiOiIiLCJsaW5rIjoiIn1dfSx7Im5hbWUiOiJSZXBvcnQgUGFsIiwicHJvZHVjdFR5cGUiOiJTbm93IFJlcG9ydGluZyIsImRlc2NyaXB0aW9uIjoiRWFzeSBzbm93IHJlcG9ydGluZyBpbiB0aGUgY2xvdWQuIiwicHJpY2UiOiIkMywwMDAgVVNEIHBlciB3aW50ZXIiLCJsb2dvIjp7InVybCI6Ii8vaW1hZ2VzLmNvbnRlbnRmdWwuY29tLzZwOG9oeGZpa2FrMS82SnBwR2RBV3pLYzhtTThzbUU4dzhtL2FlNWE1ZDQzOTI4MDI3ODY5ZjMyNGFhYTQ2YjZmOTY4L3JlcG9ydHBhbC5zdmciLCJuYW1lIjoiUmVwb3J0IFBhbCBMb2dvIn0sImRldGFpbFNlY3Rpb25zIjpbeyJuYW1lIjoiUmVwb3J0IFBhbCBTdW1tYXJ5IiwiZGVzY3JpcHRpb24iOiJUaGUgaWRlYSBiZWhpbmQgUmVwb3J0IHBhbCBpcyBhIHNpbXBsZSBvbmUgLSBtb3ZlIHNub3cgcmVwb3J0aW5nIGF3YXkgZnJvbSB5b3VyIHdlYnNpdGUgQ01TIGFuZCBpbnRvIHRoZSBjbG91ZC5cblxuSW5zdGVhZCBvZiBjb250cm9sbGluZyB5b3VyIHNub3cgcmVwb3J0IGZyb20geW91ciB3ZWJzaXRlICh3aGljaCBjYW4gY2hhbmdlIGV2ZXJ5IGNvdXBsZSBvZiB5ZWFycywgcmVzdWx0aW5nIGluIG1vcmUgY29zdHMgdG8gYnVpbGQsIHRpbWUgcmUtdHJhaW5pbmcgc3RhZmYgYW5kIGhhdmluZyB0byByZS1pbnRlZ3JhdGUgd2l0aCAzcmQgcGFydGllcyksIG1vdmUgaXQgaW50byB0aGUgY2xvdWQgYW5kIGhhdmUgYSBjb25zaXN0ZW50IHBsYXRmb3JtLCBzZWFzb24gYWZ0ZXIgc2Vhc29uLlxuXG5SZXBvcnQgUGFsIG1ha2VzIHVzZSBvZiB0aGUgbGF0ZXN0IHRlY2hub2xvZ2llcyBvbiBvZmZlciBmcm9tIEFtYXpvbiBXZWIgU2VydmljZXMsIGdpdmluZyB5b3UgYSByb2NrIHNvbGlkLCBhbHdheXMgb24gc25vdyByZXBvcnRpbmcgcGxhdGZvcm0gdGhhdCdzIGxpZ2h0bmluZyBmYXN0LlxuXG5FdmVyeSBza2kgcmVzb3J0IGlzIGRpZmZlcmVudCwgc28gUmVwb3J0IFBhbCBoYXMgYmVlbiBidWlsdCBmcm9tIHRoZSBncm91bmQgdXAgc28gdGhhdCB3ZSBjYW4gY3VzdG9taXNlIGl0IHRvIHN1aXQgeW91LiBObyBtb3JlIHdvcmtpbmcgeW91ciB3YXkgYXJvdW5kIGxpbWl0YXRpb25zIG9mIHlvdXIgd2Vic2l0ZSBDTVMsIHlvdSBjYW4gaW5wdXQgYW5kIG91dHB1dCB3aGF0IHlvdSBsaWtlLlxuXG5XZSdsbCBzZXR1cCB5b3VyIG91dHB1dCBpbiBbTVROLlhNTF0oaHR0cDovL210bnhtbC5vcmcgXCJNVE4uWE1MXCIpIGZvcm1hdCAtIHRoZSBpbmR1c3RyeSBzdGFuZGFyZC4gWW91ciBkYXRhIHdpbGwgYmUgZWFzaWx5IHNoYXJlZCB3aXRoIDNyZCBwYXJ0aWVzLiIsImxpc3QiOiIiLCJtZWRpYSI6eyJmaWxlIjoiLy9pbWFnZXMuY29udGVudGZ1bC5jb20vNnA4b2h4ZmlrYWsxLzFaUHlDVFhjMDh3NlU4Z0MyazZTZ20vNmI1YjJhNzczOTVlYzE5NjVlMDI0NmJmOGJlOGU3NGEvU2NyZWVuLVNob3QtMjAxNi0xMi0xMS1hdC00LjE4LjM0LXBtLnBuZyIsIndpZHRoIjozODV9LCJsaW5rIjoiIn0seyJuYW1lIjoiUmVwb3J0IFBhbCBJbmNsdWRlcyIsImRlc2NyaXB0aW9uIjoiIiwibGlzdCI6WyJVbmxpbWl0ZWQgdXNlcnMiLCJJbnRlbGxpZ2VudCByZXBvcnQgbWVyZ2luZyIsIlNjaGVkdWxlZCByZXBvcnRzIiwiU29jaWFsIG1lZGlhIGludGVncmF0aW9uIiwiTVROLlhNTCBvdXRwdXQiLCJSZXBvcnQgaGlzdG9yeSBhbmQgcm9sbGJhY2siLCJBdXRvbWF0ZWQgbGlmdCAmIHRyYWlsIG9wZW4gY291bnRzLCBhY3JlYWdlIGFuZCBsZW5ndGgiLCJSb2NrIHNvbGlkIC0gY2xvdWQgaG9zdGVkIG9uIEFtYXpvbiBXZWIgU2VydmljZXMiXSwibWVkaWEiOiIiLCJsaW5rIjoiIn1dfV0sImNvbXBhbnkiOnsibmFtZSI6IlJlc29ydHMgSW50ZXJhY3RpdmUiLCJkZXNjcmlwdGlvbiI6IlJlc29ydHMgSW50ZXJhY3RpdmUgaGFzIGJlZW4gbWFraW5nIGNsb3VkIHNvZnR3YXJlIGZvciB0aGUgc2tpIGluZHVzdHJ5IGZvciBzaW5jZSAyMDA0LiBUaGUgd2F5IG91ciBzb2Z0d2FyZSBpcyBidWlsdCBhbGxvd3MgdXMgdG8gZXZvbHZlIG92ZXIgdGltZSBhbmQgZW1icmFjZSBjaGFuZ2luZyB0ZWNobm9sb2dpZXMg4oCTIGEgbXVzdCBoYXZlIHF1YWxpdHkgb2YgYW55IGNsb3VkIGJhc2VkIHBhcnRuZXIuIE91ciBjdXJyZW50IHN1aXRlIG9mIFJlcG9ydCBQYWwsIHZpY29NYXAgYW5kIFNlZU1vbnN0ZXIgY292ZXJzIGEgYnJvYWQgcmFuZ2Ugb2Ygc2tpIGFyZWEgbWFya2V0aW5nIGFuZCBvcGVyYXRpb25zIGFuZCBhbGxvd3MgeW91IHRvIGdldCBiYWNrIHRvIHdoYXTigJlzIGltcG9ydGFudCAobGlrZSB0YWtpbmcgYSBmZXcgcnVucyBvbiB5b3VyIGx1bmNoIGJyZWFrISkgR2V0IGluIHRvdWNoIHRvZGF5LCB3ZeKAmWQgbG92ZSB0byB0YWxrLiIsImxvZ28iOnsidXJsIjoiLy9pbWFnZXMuY29udGVudGZ1bC5jb20vNnA4b2h4ZmlrYWsxLzZydjJ0QnBZNXlpRVVveUlDdTBBNFcvNGFiOTE1MTM0MzY4Nzg5YzEzNDNjMzQ0NDAwNWUzYWEvcmVzb3J0c2ludGVyYWN0aXZlLnN2ZyIsIm5hbWUiOiJSZXNvcnRzIEludGVyYWN0aXZlIExvZ28ifSwiY29tcGFueURldGFpbFNlY3Rpb25zIjpbeyJpbWFnZSI6Ii8vaW1hZ2VzLmNvbnRlbnRmdWwuY29tLzZwOG9oeGZpa2FrMS82SW1WemxCWFNFaUtTZWVxZ0VRa3NnL2M3NDhhZjU3NDkxZjc4MDQ4NDUwMzhiNGM2YTE3N2E4L3Bvd2Rlcl9zaG90LmpwZyIsInRpdGxlIjoiUmVzb3J0cyBJbnRlcmFjdGl2ZSB3ZWJzaXRlIiwiYm9keSI6IiJ9LHsiaW1hZ2UiOiIvL2ltYWdlcy5jb250ZW50ZnVsLmNvbS82cDhvaHhmaWthazEvM3kwQWFQUE16Q0dLaXVLY011ZWEyUS9mNTJiNTc5ZGM5YTIwODBlNDg0OWVlODcyMjYxMjRjMi9RVC13ZWF0aGVyZm9yZWNhc3Qtc2NyZWVuLmpwZyIsInRpdGxlIjoiV2hhdCBzZXRzIHVzIGFwYXJ0PyIsImJvZHkiOiJSZXNvcnRzIEludGVyYWN0aXZlIGhhcyBiZWVuIG1ha2luZyBjbG91ZCBzb2Z0d2FyZSBmb3IgdGhlIHNraSBpbmR1c3RyeSBzaW5jZSAyMDA1IHNvIHdlIGtub3cgYSB0aGluZyBvciB0d28gYWJvdXQgdGhlIHN1YmplY3QhIFRoZSB3YXkgb3VyIHNvZnR3YXJlIGlzIGJ1aWx0IGFsbG93cyB1cyB0byBldm9sdmUgb3ZlciB0aW1lIGFuZCBlbWJyYWNlIGNoYW5naW5nIHRlY2hub2xvZ2llcyDigJMgYSBtdXN0IGhhdmUgcXVhbGl0eSBvZiBhbnkgY2xvdWQgYmFzZWQgcGFydG5lci4gT3VyIGN1cnJlbnQgc3VpdGUgb2YgUmVwb3J0IFBhbCwgdmljb01hcCBhbmQgU2VlTW9uc3RlciBjb3ZlcnMgYSBicm9hZCByYW5nZSBvZiBza2kgYXJlYSBtYXJrZXRpbmcgYW5kIG9wZXJhdGlvbnMgYW5kIGFsbG93cyB5b3UgdG8gZ2V0IGJhY2sgdG8gd2hhdOKAmXMgaW1wb3J0YW50IChsaWtlIHRha2luZyBhIGZldyBydW5zIG9uIHlvdXIgbHVuY2ggYnJlYWshKSBbR2V0IGluIHRvdWNoIHRvZGF5LCB3ZeKAmWQgbG92ZSB0byB0YWxrLl0obWFpbHRvOmluZm9AcmVzb3J0cy1pbnRlcmFjdGl2ZS5jb20pIn0seyJpbWFnZSI6Ii8vaW1hZ2VzLmNvbnRlbnRmdWwuY29tLzZwOG9oeGZpa2FrMS80ZVJ3ZDkyZ293b21NS0FZT2dnY3cwLzlmODNjMmYyYjJhN2VhZWIxMzJhOWRmZTYwNTA5YmNhL0RldmljZXMuanBnIiwidGl0bGUiOiJDbGV2ZXIgU29mdHdhcmUgTWFraW5nIExpZmUgRWFzeSIsImJvZHkiOiItIFlvdSB3b3JrIGluIHRoZSBza2kgaW5kdXN0cnkgYmVjYXVzZSB5b3UgbG92ZSBpdCwgc28gd2UgYnVpbGQgc29mdHdhcmUgdG8gc2F2ZSB5b3UgdGltZSBhbmQgZ2V0IHlvdSBiYWNrIG9uIHRoZSBoaWxsXG4tIENsb3VkIGJhc2VkIHNvZnR3YXJlIGlzIGRlc2lnbmVkIHRvIGJlIHVzZWQgYW55d2hlcmUg4oCTIGRlc2t0b3AsIG1vYmlsZSwgQ29sb3JhZG8sIFRpbWJ1a3R1LiBJZiB5b3UgY2FuIGdldCBhbiBJbnRlcm5ldCBjb25uZWN0aW9uLCB3ZeKAmXZlIGdvdCB5b3UgY292ZXJlZFxuLSBTaW1wbGljaXR5IGFuZCBzdGFiaWxpdHkgZm9ybSBhIGNvcmUgcGFydCBvZiBvdXIgc29mdHdhcmUgYXJjaGl0ZWN0dXJlLiBPdXIgcHJvZHVjdHMgYXJlIGhvc3RlZCBvbiBBbWF6b24gV2ViIFNlcnZpY2VzIGFuZCBtYWtlIHVzZSBvZiB0aGVpciBiZXN0IGFuZCBsYXRlc3QgdGVjaG5vbG9naWVzIGxpa2UgQ2xvdWRGcm9udCwgQXVyb3JhLCBhbmQgRWxhc3RpYyBCZWFuc3RhbGsgdG8gbWFrZSBzdXJlIHdl4oCZcmUgYWx3YXlzIGZhc3QgYW5kIGF2YWlsYWJsZVxuLSBJbmNsdWRlIGxpdmUgZGF0YSBhbmQgaW1hZ2VzIGZyb20gc29jaWFsIG1lZGlhLCB3ZWF0aGVyLCBuZXdzIGFuZCBvdGhlciB3ZWJzaXRlc1xuLSBVcGxvYWQgdmlkZW8gaW4gYW55IGZvcm1hdCBhbmQgaXTigJlsbCBiZSBjb252ZXJ0ZWQgZm9yIHlvdSJ9XSwiaWNvbiI6eyJzeXMiOnsic3BhY2UiOnsic3lzIjp7InR5cGUiOiJMaW5rIiwibGlua1R5cGUiOiJTcGFjZSIsImlkIjoiNnA4b2h4ZmlrYWsxIn19LCJpZCI6ImttRzBYM2ZGUWNFc2VpbWNlVWtJcyIsInR5cGUiOiJBc3NldCIsImNyZWF0ZWRBdCI6IjIwMTctMDYtMThUMTA6MjQ6NTQuNTgwWiIsInVwZGF0ZWRBdCI6IjIwMTctMDYtMThUMTA6MjQ6NTQuNTgwWiIsInJldmlzaW9uIjoxLCJsb2NhbGUiOiJlbi1OWiJ9LCJmaWVsZHMiOnsidGl0bGUiOiJTZW1pLWZsYWtlLXBuZyIsImRlc2NyaXB0aW9uIjoiUE5HIHZlcnNpb24gb2Ygc2VtaS1mbGFrZSIsImZpbGUiOnsidXJsIjoiLy9pbWFnZXMuY29udGVudGZ1bC5jb20vNnA4b2h4ZmlrYWsxL2ttRzBYM2ZGUWNFc2VpbWNlVWtJcy85NDNjMDQ3OTFhMmQ0NDJmMWY1MDgwMmEzODY4ZTI5ZS9zZW1pZmxha2UucG5nIiwiZGV0YWlscyI6eyJzaXplIjoyNDE5MCwiaW1hZ2UiOnsid2lkdGgiOjUxMiwiaGVpZ2h0Ijo1MTJ9fSwiZmlsZU5hbWUiOiJzZW1pZmxha2UucG5nIiwiY29udGVudFR5cGUiOiJpbWFnZS9wbmcifX19fX0=')
31
+ intialData('eyJjb250YWN0Ijp7InRpdGxlIjoiQ29udGFjdCBVcyJ9LCJyb3V0ZXIiOnsicGF0aG5hbWUiOiIvIn19')
40
32
  let halfcabModule = await import('./halfcab.mjs')
41
33
  ;({
42
34
  ssr,
@@ -60,8 +52,10 @@ describe('halfcab', () => {
60
52
  width: 100px;
61
53
  }
62
54
  `
63
- let {componentsString, stylesString} = ssr(serverHtml`
64
- <div class="${style.myStyle}" oninput=${() => {
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,7 +66,7 @@ describe('halfcab', () => {
72
66
 
73
67
  before(async () => {
74
68
  jsdomGlobal()
75
- intialData('eyJjb250YWN0Rm9ybSI6eyJzZW5kRGlzYWJsZWQiOmZhbHNlLCJzaG93VGhhbmtzIjpmYWxzZX0sImxvZ2luIjp7ImRpc2FibGVkIjpmYWxzZX0sImxvYWRpbmciOmZhbHNlLCJzaG93Q29udGFjdCI6dHJ1ZSwicHJvZHVjdHMiOlt7Im5hbWUiOiJTZWVNb25zdGVyIiwicHJvZHVjdFR5cGUiOiJEaWdpdGFsIFNpZ25hZ2UiLCJkZXNjcmlwdGlvbiI6IkRpZ2l0YWwgc2lnbmFnZSBidWlsdCBmb3Igc2tpIGFyZWFzLiIsInByaWNlIjoiJDMsMjAwIFVTRCBwZXIgd2ludGVyIiwibG9nbyI6eyJ1cmwiOiIvL2ltYWdlcy5jb250ZW50ZnVsLmNvbS82cDhvaHhmaWthazEvNmdaY1A2NG1UbTIybTAyWWFvTUdPUS9kN2ZlZjkwZWVhZjI5MmQwNDcwYzNiNjlhODJiMmM2NS9zZWVtb25zdGVyLnN2ZyIsIm5hbWUiOiJTZWVNb25zdGVyIExvZ28ifSwiZGV0YWlsU2VjdGlvbnMiOlt7Im5hbWUiOiJXaGF0IGlzIFNlZU1vbnN0ZXIiLCJkZXNjcmlwdGlvbiI6IiMjIyBTZWVNb25zdGVyIGVuYWJsZXMgeW91IHRvIGRpc3BsYXkgZHluYW1pYyBtZXNzYWdlcyBvbiBzY3JlZW5zIGFyb3VuZCB5b3VyIHNraSBhcmVhIGFuZCBvbiBob3RlbCByb29tIFRWcywgYWxsIG1hbmFnZWQgb3ZlciB0aGUgSW50ZXJuZXQuIiwibGlzdCI6IiIsIm1lZGlhIjp7ImZpbGUiOiIvL2ltYWdlcy5jb250ZW50ZnVsLmNvbS82cDhvaHhmaWthazEvYmZtQU5QdE1mUUNRdVE0UWthV1VFLzI0OTY0MjU3OGJmYTBlYzE3M2JlMzhjYTkzMjRhYTEzL3NlZW1vbnN0ZXItMS0yLTMucG5nIiwid2lkdGgiOjg2Nn0sImxpbmsiOiIifSx7Im5hbWUiOiJTZWVNb25zdGVyIEluY2x1ZGVzIiwiZGVzY3JpcHRpb24iOiIiLCJsaXN0IjpbIlBsYXlzIG9uIGFueSBjb21wdXRlciAmIFRWIGNvbWJvIiwiQmFuZHdpZHRoLCBzdG9yYWdlICYgc3VwcG9ydCBpbmNsdWRlZCIsIkVhc3kgZHJhZyBhbmQgZHJvcCBhZG1pbi4iLCJCdWlsdCBpbiBhbmltYXRpb24uIiwiVXBsb2FkIGltYWdlcyBhbmQgdmlkZW8gaW4gYW55IGZvcm1hdCIsIlVzZSB3aXRoIGJvdGggZGlnaXRhbCBzaWducyBhbmQgaW4tcm9vbSBUViBzeXN0ZW1zIiwiVXNlIGxpdmUgZGF0YSB0byBwb3B1bGF0ZSB0ZXh0IGZpZWxkcyAmIHN3YXAgb3V0IGltYWdlcyIsIlJlbW90ZWx5IGNvbnRyb2wgeW91ciBzY3JlZW5zIiwiU2NoZWR1bGUgY29udGVudCIsIlNvY2lhbCBtZWRpYSBmZWVkcyIsIkRpc3BsYXkgdmlkZW9zIGFuZCBwaG90b3MiLCJNYXBzLCBsaWZ0ICYgdHJhaWwgc3RhdHVzIl0sIm1lZGlhIjp7ImZpbGUiOiIvL2ltYWdlcy5jb250ZW50ZnVsLmNvbS82cDhvaHhmaWthazEvM3kwQWFQUE16Q0dLaXVLY011ZWEyUS9mNTJiNTc5ZGM5YTIwODBlNDg0OWVlODcyMjYxMjRjMi9RVC13ZWF0aGVyZm9yZWNhc3Qtc2NyZWVuLmpwZyIsIndpZHRoIjo2Njd9LCJsaW5rIjoiIn1dfSx7Im5hbWUiOiJ2aWNvTWFwIiwicHJvZHVjdFR5cGUiOiJJbnRlcmFjdGl2ZSBUcmFpbCBNYXAiLCJkZXNjcmlwdGlvbiI6IipUaGUqIGludGVyYWN0aXZlIG1hcCBmb3Igc2tpIGFyZWFzLiIsInByaWNlIjoiJDIsODAwIFVTRCBwZXIgd2ludGVyIiwibG9nbyI6eyJ1cmwiOiIvL2ltYWdlcy5jb250ZW50ZnVsLmNvbS82cDhvaHhmaWthazEvMXNDOU4xV0hXODJBV2tvZUljS1FZMi82ODhjYmI5NmU3YmU2MDJkZDFmOWJmYmFmZjI0ZTI5MC92aWNvbWFwLnN2ZyIsIm5hbWUiOiJ2aWNvTWFwIExvZ28ifSwiZGV0YWlsU2VjdGlvbnMiOlt7Im5hbWUiOiJEZW1vIiwiZGVzY3JpcHRpb24iOiIqVGhlIGJlc3Qgd2F5IHRvIGNoZWNrIG91dCB2aWNvTWFwLCBpcyB0byBqdXN0IHN0YXJ0IHVzaW5nIGl0ISBIYXZlIGEgZ28gd2l0aCB0aGlzIG9uZSBhbmQgbGV0IHVzIGtub3cgd2hhdCB5b3UgdGhpbmsuKiIsImxpc3QiOiIiLCJtZWRpYSI6IiIsImxpbmsiOiJodHRwczovL3ZpY29tYXAtY2RuLnJlc29ydHMtaW50ZXJhY3RpdmUuY29tL21hcC8xMiJ9LHsibmFtZSI6InZpY29NYXAgaW5jbHVkZXMiLCJkZXNjcmlwdGlvbiI6IiIsImxpc3QiOlsiUmVzcG9uc2l2ZSAod29ya3Mgb24gZGVza3RvcCBhbmQgbW9iaWxlIGRldmljZXMpIiwiTGl2ZSBsaWZ0IGFuZCB0cmFpbCBkYXRhIChmcmVlIGJhc2ljIFJlcG9ydCBQYWwgY29ubmVjdG9yIGluY2x1ZGVkKSIsIkNsb3VkIGhvc3RlZCAoQVdTKSIsIkJhbmR3aWR0aCwgc3RvcmFnZSAmIHN1cHBvcnQgaW5jbHVkZWQiLCJFYXN5IHdlYnNpdGUgZW1iZWQiLCJObyBzZXR1cCBjb3N0IC0gYnVpbHQgZnJvbSB5b3VyIGV4aXN0aW5nIElsbHVzdHJhdG9yIGZpbGUiXSwibWVkaWEiOiIiLCJsaW5rIjoiIn1dfSx7Im5hbWUiOiJSZXBvcnQgUGFsIiwicHJvZHVjdFR5cGUiOiJTbm93IFJlcG9ydGluZyIsImRlc2NyaXB0aW9uIjoiRWFzeSBzbm93IHJlcG9ydGluZyBpbiB0aGUgY2xvdWQuIiwicHJpY2UiOiIkMywwMDAgVVNEIHBlciB3aW50ZXIiLCJsb2dvIjp7InVybCI6Ii8vaW1hZ2VzLmNvbnRlbnRmdWwuY29tLzZwOG9oeGZpa2FrMS82SnBwR2RBV3pLYzhtTThzbUU4dzhtL2FlNWE1ZDQzOTI4MDI3ODY5ZjMyNGFhYTQ2YjZmOTY4L3JlcG9ydHBhbC5zdmciLCJuYW1lIjoiUmVwb3J0IFBhbCBMb2dvIn0sImRldGFpbFNlY3Rpb25zIjpbeyJuYW1lIjoiUmVwb3J0IFBhbCBTdW1tYXJ5IiwiZGVzY3JpcHRpb24iOiJUaGUgaWRlYSBiZWhpbmQgUmVwb3J0IHBhbCBpcyBhIHNpbXBsZSBvbmUgLSBtb3ZlIHNub3cgcmVwb3J0aW5nIGF3YXkgZnJvbSB5b3VyIHdlYnNpdGUgQ01TIGFuZCBpbnRvIHRoZSBjbG91ZC5cblxuSW5zdGVhZCBvZiBjb250cm9sbGluZyB5b3VyIHNub3cgcmVwb3J0IGZyb20geW91ciB3ZWJzaXRlICh3aGljaCBjYW4gY2hhbmdlIGV2ZXJ5IGNvdXBsZSBvZiB5ZWFycywgcmVzdWx0aW5nIGluIG1vcmUgY29zdHMgdG8gYnVpbGQsIHRpbWUgcmUtdHJhaW5pbmcgc3RhZmYgYW5kIGhhdmluZyB0byByZS1pbnRlZ3JhdGUgd2l0aCAzcmQgcGFydGllcyksIG1vdmUgaXQgaW50byB0aGUgY2xvdWQgYW5kIGhhdmUgYSBjb25zaXN0ZW50IHBsYXRmb3JtLCBzZWFzb24gYWZ0ZXIgc2Vhc29uLlxuXG5SZXBvcnQgUGFsIG1ha2VzIHVzZSBvZiB0aGUgbGF0ZXN0IHRlY2hub2xvZ2llcyBvbiBvZmZlciBmcm9tIEFtYXpvbiBXZWIgU2VydmljZXMsIGdpdmluZyB5b3UgYSByb2NrIHNvbGlkLCBhbHdheXMgb24gc25vdyByZXBvcnRpbmcgcGxhdGZvcm0gdGhhdCdzIGxpZ2h0bmluZyBmYXN0LlxuXG5FdmVyeSBza2kgcmVzb3J0IGlzIGRpZmZlcmVudCwgc28gUmVwb3J0IFBhbCBoYXMgYmVlbiBidWlsdCBmcm9tIHRoZSBncm91bmQgdXAgc28gdGhhdCB3ZSBjYW4gY3VzdG9taXNlIGl0IHRvIHN1aXQgeW91LiBObyBtb3JlIHdvcmtpbmcgeW91ciB3YXkgYXJvdW5kIGxpbWl0YXRpb25zIG9mIHlvdXIgd2Vic2l0ZSBDTVMsIHlvdSBjYW4gaW5wdXQgYW5kIG91dHB1dCB3aGF0IHlvdSBsaWtlLlxuXG5XZSdsbCBzZXR1cCB5b3VyIG91dHB1dCBpbiBbTVROLlhNTF0oaHR0cDovL210bnhtbC5vcmcgXCJNVE4uWE1MXCIpIGZvcm1hdCAtIHRoZSBpbmR1c3RyeSBzdGFuZGFyZC4gWW91ciBkYXRhIHdpbGwgYmUgZWFzaWx5IHNoYXJlZCB3aXRoIDNyZCBwYXJ0aWVzLiIsImxpc3QiOiIiLCJtZWRpYSI6eyJmaWxlIjoiLy9pbWFnZXMuY29udGVudGZ1bC5jb20vNnA4b2h4ZmlrYWsxLzFaUHlDVFhjMDh3NlU4Z0MyazZTZ20vNmI1YjJhNzczOTVlYzE5NjVlMDI0NmJmOGJlOGU3NGEvU2NyZWVuLVNob3QtMjAxNi0xMi0xMS1hdC00LjE4LjM0LXBtLnBuZyIsIndpZHRoIjozODV9LCJsaW5rIjoiIn0seyJuYW1lIjoiUmVwb3J0IFBhbCBJbmNsdWRlcyIsImRlc2NyaXB0aW9uIjoiIiwibGlzdCI6WyJVbmxpbWl0ZWQgdXNlcnMiLCJJbnRlbGxpZ2VudCByZXBvcnQgbWVyZ2luZyIsIlNjaGVkdWxlZCByZXBvcnRzIiwiU29jaWFsIG1lZGlhIGludGVncmF0aW9uIiwiTVROLlhNTCBvdXRwdXQiLCJSZXBvcnQgaGlzdG9yeSBhbmQgcm9sbGJhY2siLCJBdXRvbWF0ZWQgbGlmdCAmIHRyYWlsIG9wZW4gY291bnRzLCBhY3JlYWdlIGFuZCBsZW5ndGgiLCJSb2NrIHNvbGlkIC0gY2xvdWQgaG9zdGVkIG9uIEFtYXpvbiBXZWIgU2VydmljZXMiXSwibWVkaWEiOiIiLCJsaW5rIjoiIn1dfV0sImNvbXBhbnkiOnsibmFtZSI6IlJlc29ydHMgSW50ZXJhY3RpdmUiLCJkZXNjcmlwdGlvbiI6IlJlc29ydHMgSW50ZXJhY3RpdmUgaGFzIGJlZW4gbWFraW5nIGNsb3VkIHNvZnR3YXJlIGZvciB0aGUgc2tpIGluZHVzdHJ5IGZvciBzaW5jZSAyMDA0LiBUaGUgd2F5IG91ciBzb2Z0d2FyZSBpcyBidWlsdCBhbGxvd3MgdXMgdG8gZXZvbHZlIG92ZXIgdGltZSBhbmQgZW1icmFjZSBjaGFuZ2luZyB0ZWNobm9sb2dpZXMg4oCTIGEgbXVzdCBoYXZlIHF1YWxpdHkgb2YgYW55IGNsb3VkIGJhc2VkIHBhcnRuZXIuIE91ciBjdXJyZW50IHN1aXRlIG9mIFJlcG9ydCBQYWwsIHZpY29NYXAgYW5kIFNlZU1vbnN0ZXIgY292ZXJzIGEgYnJvYWQgcmFuZ2Ugb2Ygc2tpIGFyZWEgbWFya2V0aW5nIGFuZCBvcGVyYXRpb25zIGFuZCBhbGxvd3MgeW91IHRvIGdldCBiYWNrIHRvIHdoYXTigJlzIGltcG9ydGFudCAobGlrZSB0YWtpbmcgYSBmZXcgcnVucyBvbiB5b3VyIGx1bmNoIGJyZWFrISkgR2V0IGluIHRvdWNoIHRvZGF5LCB3ZeKAmWQgbG92ZSB0byB0YWxrLiIsImxvZ28iOnsidXJsIjoiLy9pbWFnZXMuY29udGVudGZ1bC5jb20vNnA4b2h4ZmlrYWsxLzZydjJ0QnBZNXlpRVVveUlDdTBBNFcvNGFiOTE1MTM0MzY4Nzg5YzEzNDNjMzQ0NDAwNWUzYWEvcmVzb3J0c2ludGVyYWN0aXZlLnN2ZyIsIm5hbWUiOiJSZXNvcnRzIEludGVyYWN0aXZlIExvZ28ifSwiY29tcGFueURldGFpbFNlY3Rpb25zIjpbeyJpbWFnZSI6Ii8vaW1hZ2VzLmNvbnRlbnRmdWwuY29tLzZwOG9oeGZpa2FrMS82SW1WemxCWFNFaUtTZWVxZ0VRa3NnL2M3NDhhZjU3NDkxZjc4MDQ4NDUwMzhiNGM2YTE3N2E4L3Bvd2Rlcl9zaG90LmpwZyIsInRpdGxlIjoiUmVzb3J0cyBJbnRlcmFjdGl2ZSB3ZWJzaXRlIiwiYm9keSI6IiJ9LHsiaW1hZ2UiOiIvL2ltYWdlcy5jb250ZW50ZnVsLmNvbS82cDhvaHhmaWthazEvM3kwQWFQUE16Q0dLaXVLY011ZWEyUS9mNTJiNTc5ZGM5YTIwODBlNDg0OWVlODcyMjYxMjRjMi9RVC13ZWF0aGVyZm9yZWNhc3Qtc2NyZWVuLmpwZyIsInRpdGxlIjoiV2hhdCBzZXRzIHVzIGFwYXJ0PyIsImJvZHkiOiJSZXNvcnRzIEludGVyYWN0aXZlIGhhcyBiZWVuIG1ha2luZyBjbG91ZCBzb2Z0d2FyZSBmb3IgdGhlIHNraSBpbmR1c3RyeSBzaW5jZSAyMDA1IHNvIHdlIGtub3cgYSB0aGluZyBvciB0d28gYWJvdXQgdGhlIHN1YmplY3QhIFRoZSB3YXkgb3VyIHNvZnR3YXJlIGlzIGJ1aWx0IGFsbG93cyB1cyB0byBldm9sdmUgb3ZlciB0aW1lIGFuZCBlbWJyYWNlIGNoYW5naW5nIHRlY2hub2xvZ2llcyDigJMgYSBtdXN0IGhhdmUgcXVhbGl0eSBvZiBhbnkgY2xvdWQgYmFzZWQgcGFydG5lci4gT3VyIGN1cnJlbnQgc3VpdGUgb2YgUmVwb3J0IFBhbCwgdmljb01hcCBhbmQgU2VlTW9uc3RlciBjb3ZlcnMgYSBicm9hZCByYW5nZSBvZiBza2kgYXJlYSBtYXJrZXRpbmcgYW5kIG9wZXJhdGlvbnMgYW5kIGFsbG93cyB5b3UgdG8gZ2V0IGJhY2sgdG8gd2hhdOKAmXMgaW1wb3J0YW50IChsaWtlIHRha2luZyBhIGZldyBydW5zIG9uIHlvdXIgbHVuY2ggYnJlYWshKSBbR2V0IGluIHRvdWNoIHRvZGF5LCB3ZeKAmWQgbG92ZSB0byB0YWxrLl0obWFpbHRvOmluZm9AcmVzb3J0cy1pbnRlcmFjdGl2ZS5jb20pIn0seyJpbWFnZSI6Ii8vaW1hZ2VzLmNvbnRlbnRmdWwuY29tLzZwOG9oeGZpa2FrMS80ZVJ3ZDkyZ293b21NS0FZT2dnY3cwLzlmODNjMmYyYjJhN2VhZWIxMzJhOWRmZTYwNTA5YmNhL0RldmljZXMuanBnIiwidGl0bGUiOiJDbGV2ZXIgU29mdHdhcmUgTWFraW5nIExpZmUgRWFzeSIsImJvZHkiOiItIFlvdSB3b3JrIGluIHRoZSBza2kgaW5kdXN0cnkgYmVjYXVzZSB5b3UgbG92ZSBpdCwgc28gd2UgYnVpbGQgc29mdHdhcmUgdG8gc2F2ZSB5b3UgdGltZSBhbmQgZ2V0IHlvdSBiYWNrIG9uIHRoZSBoaWxsXG4tIENsb3VkIGJhc2VkIHNvZnR3YXJlIGlzIGRlc2lnbmVkIHRvIGJlIHVzZWQgYW55d2hlcmUg4oCTIGRlc2t0b3AsIG1vYmlsZSwgQ29sb3JhZG8sIFRpbWJ1a3R1LiBJZiB5b3UgY2FuIGdldCBhbiBJbnRlcm5ldCBjb25uZWN0aW9uLCB3ZeKAmXZlIGdvdCB5b3UgY292ZXJlZFxuLSBTaW1wbGljaXR5IGFuZCBzdGFiaWxpdHkgZm9ybSBhIGNvcmUgcGFydCBvZiBvdXIgc29mdHdhcmUgYXJjaGl0ZWN0dXJlLiBPdXIgcHJvZHVjdHMgYXJlIGhvc3RlZCBvbiBBbWF6b24gV2ViIFNlcnZpY2VzIGFuZCBtYWtlIHVzZSBvZiB0aGVpciBiZXN0IGFuZCBsYXRlc3QgdGVjaG5vbG9naWVzIGxpa2UgQ2xvdWRGcm9udCwgQXVyb3JhLCBhbmQgRWxhc3RpYyBCZWFuc3RhbGsgdG8gbWFrZSBzdXJlIHdl4oCZcmUgYWx3YXlzIGZhc3QgYW5kIGF2YWlsYWJsZVxuLSBJbmNsdWRlIGxpdmUgZGF0YSBhbmQgaW1hZ2VzIGZyb20gc29jaWFsIG1lZGlhLCB3ZWF0aGVyLCBuZXdzIGFuZCBvdGhlciB3ZWJzaXRlc1xuLSBVcGxvYWQgdmlkZW8gaW4gYW55IGZvcm1hdCBhbmQgaXTigJlsbCBiZSBjb252ZXJ0ZWQgZm9yIHlvdSJ9XSwiaWNvbiI6eyJzeXMiOnsic3BhY2UiOnsic3lzIjp7InR5cGUiOiJMaW5rIiwibGlua1R5cGUiOiJTcGFjZSIsImlkIjoiNnA4b2h4ZmlrYWsxIn19LCJpZCI6ImttRzBYM2ZGUWNFc2VpbWNlVWtJcyIsInR5cGUiOiJBc3NldCIsImNyZWF0ZWRBdCI6IjIwMTctMDYtMThUMTA6MjQ6NTQuNTgwWiIsInVwZGF0ZWRBdCI6IjIwMTctMDYtMThUMTA6MjQ6NTQuNTgwWiIsInJldmlzaW9uIjoxLCJsb2NhbGUiOiJlbi1OWiJ9LCJmaWVsZHMiOnsidGl0bGUiOiJTZW1pLWZsYWtlLXBuZyIsImRlc2NyaXB0aW9uIjoiUE5HIHZlcnNpb24gb2Ygc2VtaS1mbGFrZSIsImZpbGUiOnsidXJsIjoiLy9pbWFnZXMuY29udGVudGZ1bC5jb20vNnA4b2h4ZmlrYWsxL2ttRzBYM2ZGUWNFc2VpbWNlVWtJcy85NDNjMDQ3OTFhMmQ0NDJmMWY1MDgwMmEzODY4ZTI5ZS9zZW1pZmxha2UucG5nIiwiZGV0YWlscyI6eyJzaXplIjoyNDE5MCwiaW1hZ2UiOnsid2lkdGgiOjUxMiwiaGVpZ2h0Ijo1MTJ9fSwiZmlsZU5hbWUiOiJzZW1pZmxha2UucG5nIiwiY29udGVudFR5cGUiOiJpbWFnZS9wbmcifX19fX0=')
69
+ intialData('eyJjb250YWN0Ijp7InRpdGxlIjoiQ29udGFjdCBVcyJ9LCJyb3V0ZXIiOnsicGF0aG5hbWUiOiIvIn19')
76
70
  let halfcabModule = await import('./halfcab.mjs')
77
71
  ;({
78
72
  ssr,
@@ -93,20 +87,19 @@ describe('halfcab', () => {
93
87
 
94
88
  it('Produces a TemplateResult when rendering', () => {
95
89
  let el = html`
96
- <div oninput=${() => {
90
+ <div @input=${() => {
97
91
  }}></div>
98
92
  `
99
- expect(typeof el === 'object').to.be.true()
100
- expect(el).to.have.property('strings')
93
+ // Check for Lit TemplateResult marker
94
+ expect(el['_$litType$']).to.exist()
101
95
  })
102
96
 
103
97
  it('Produces a TemplateResult wrapping as a reusable component', () => {
104
98
  let el = args => html`
105
- <div oninput=${() => {
99
+ <div @input=${() => {
106
100
  }}></div>
107
101
  `
108
- expect(typeof el({}) === 'object').to.be.true()
109
- expect(el({})).to.have.property('strings')
102
+ expect(el({})['_$litType$']).to.exist()
110
103
  })
111
104
 
112
105
  it('Runs halfcab function without error', () => {
@@ -116,13 +109,14 @@ describe('halfcab', () => {
116
109
  return html `<div></div>`
117
110
  }
118
111
  })
119
- .then(rootEl => {
112
+ .then(({rootEl}) => {
120
113
  expect(typeof rootEl === 'object').to.be.true()
121
114
  })
122
115
  })
123
116
 
124
117
  it('updating state causes a rerender with state', (done) => {
125
118
  halfcab({
119
+ el: '#root', // Ensure el is passed so rootEl is the container
126
120
  components (args) {
127
121
  return html`<div>${args.testing || ''}</div>`
128
122
  }
@@ -139,6 +133,7 @@ describe('halfcab', () => {
139
133
 
140
134
  it('updates state without merging arrays when told to', () => {
141
135
  return halfcab({
136
+ el: '#root',
142
137
  components () {
143
138
  return html `<div></div>`
144
139
  }
@@ -165,6 +160,7 @@ describe('halfcab', () => {
165
160
  }
166
161
  `
167
162
  return halfcab({
163
+ el: '#root',
168
164
  components (args) {
169
165
  return html `<div class="${style.myStyle}">${args.testing.inner || ''}</div>`
170
166
  }
@@ -180,6 +176,7 @@ describe('halfcab', () => {
180
176
 
181
177
  it('injects external content without error', () => {
182
178
  return halfcab({
179
+ el: '#root',
183
180
  components (args) {
184
181
  return html `<div>${injectMarkdown('### Heading')}</div>`
185
182
  }
@@ -192,6 +189,7 @@ describe('halfcab', () => {
192
189
 
193
190
  it('injects markdown without wrapper without error', () => {
194
191
  return halfcab({
192
+ el: '#root',
195
193
  components (args) {
196
194
  return html `<div>${injectMarkdown('### Heading', {wrapper: false})}</div>`
197
195
  }
@@ -364,6 +362,7 @@ describe('halfcab', () => {
364
362
  })
365
363
 
366
364
  return halfcab({
365
+ el: '#root',
367
366
  components () {
368
367
  return html `<div></div>`
369
368
  }
@@ -373,8 +372,7 @@ describe('halfcab', () => {
373
372
  let routing = () => {
374
373
  gotoRoute('/testFakeRoute')
375
374
  }
376
- expect(routing).to.not.throw()//made it out of the try catch, must
377
- // be fine
375
+ expect(routing).to.not.throw()
378
376
  })
379
377
 
380
378
  })
@@ -382,6 +380,7 @@ describe('halfcab', () => {
382
380
  it(`Throws an error when a route doesn't exist`, () => {
383
381
 
384
382
  return halfcab({
383
+ el: '#root',
385
384
  components () {
386
385
  return html `<div></div>`
387
386
  }
@@ -390,8 +389,7 @@ describe('halfcab', () => {
390
389
  let routing = () => {
391
390
  gotoRoute('/thisIsAFakeRoute')
392
391
  }
393
- expect(routing).to.throw()//made it out of the try catch, must be
394
- // fine
392
+ expect(routing).to.throw()
395
393
  })
396
394
 
397
395
  })
@@ -407,6 +405,7 @@ describe('halfcab', () => {
407
405
 
408
406
  it(`Doesn't clone when merging`, (done) => {
409
407
  halfcab({
408
+ el: '#root',
410
409
  components () {
411
410
  return html `<div></div>`
412
411
  }
@@ -0,0 +1,33 @@
1
+ import { ssr, html } from './halfcab.mjs';
2
+ import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
3
+ import { repeat } from 'lit-html/directives/repeat.js';
4
+ import { classMap } from 'lit-html/directives/class-map.js';
5
+ import { styleMap } from 'lit-html/directives/style-map.js';
6
+
7
+ console.log('--- Testing SSR ---');
8
+
9
+ const items = ['A', 'B', 'C'];
10
+ const classes = { active: true, disabled: false };
11
+ const styles = { color: 'red', 'font-size': '20px' };
12
+ const fn = () => console.log('clicked');
13
+
14
+ const template = html`
15
+ <div class="container">
16
+ <h1>Hello SSR</h1>
17
+ <div class=${classMap(classes)} style=${styleMap(styles)}>Styled</div>
18
+ <ul>
19
+ ${repeat(items, (item) => html`<li>${item}</li>`)}
20
+ </ul>
21
+ <div>${unsafeHTML('<span>Unsafe</span>')}</div>
22
+ <button onclick=${fn}>Click</button>
23
+ <button disabled=${null}>Disabled Null</button>
24
+ </div>
25
+ `;
26
+
27
+ try {
28
+ const result = ssr(template);
29
+ console.log('Result componentsString:');
30
+ console.log(result.componentsString);
31
+ } catch (e) {
32
+ console.error('SSR Error:', e);
33
+ }