`;``.
83 |
84 | This breaks some existing patterns that are common in applications
85 | like the following
86 |
87 | ```js
88 | class Comp extends Tonic {
89 | renderLabel () {
90 | return `
${this.props.label} `
91 | }
92 |
93 | render () {
94 | return this.html`
95 |
96 |
97 | ${this.renderLabel()}
98 |
99 | `
100 | }
101 | }
102 | ```
103 |
104 | In this case the HTML returned from `this.renderLabel()` is now
105 | being escaped which is probably not what you meant.
106 |
107 | You will have to patch the code to use `this.html` for the
108 | implementation of `renderLabel()` like
109 |
110 | ```js
111 | renderLabel () {
112 | return this.html`
${this.props.label} `
113 | }
114 | ```
115 |
116 | Or to call `Tonic.raw()` manually like
117 |
118 | ```js
119 | render () {
120 | return this.html`
121 |
122 |
123 | ${Tonic.raw(this.renderLabel())}
124 |
125 | `
126 | }
127 | ```
128 |
129 | If you want to quickly find all occurences of the above patterns
130 | you can run the following git grep on your codebase.
131 |
132 | ```sh
133 | git grep -C10 '${' | grep ')}'
134 | ```
135 |
136 | The fix is to add `this.html` calls in various places.
137 |
138 | We have updated `@socketsupply/components` and you will have to
139 | update to version `7.4.0` as well
140 |
141 | ```sh
142 | npm install @socketsupply/components@^7.4.0 -ES
143 | ```
144 |
145 | There are other situations in which the increased escaping from
146 | `Tonic.escape()` like for example escaping the `"` character if
147 | you dynamically generate optional attributes
148 |
149 | Like:
150 |
151 | ```js
152 | class Icon extends Tonic {
153 | render () {
154 | return this.html`
155 |
159 | `
160 | }
161 | }
162 | ```
163 |
164 | In the above example we do ``fill ? `fill="${fill}"` : ''`` which
165 | leads to `"` getting escaped to `"` and leads to the value
166 | of `use.getAttribute('fill')` to be `"${fill}"` instead of `${fill}`
167 |
168 | Here is a regex you can use to find the one-liner use cases.
169 |
170 | ```
171 | git grep -E '`(.+)="'
172 | ```
173 |
174 | When building dynamic attribute lists `Tonic` has a spread feature
175 | in the `this.html()` function you can use instead to make it easier.
176 |
177 | For example, you can refactor the above `Icon` class to:
178 |
179 | ```js
180 | class Icon extends Tonic {
181 | render () {
182 | return this.html`
183 |
189 | `
190 | }
191 | }
192 | ```
193 |
194 | Here we use `...${{ ... }}` to expand an object of attributes to
195 | attribute key value pairs in the HTML. You can also pull out the attrs
196 | into a reference if you prefer, like:
197 |
198 | ```js
199 | class Icon extends Tonic {
200 | render () {
201 | const useAttrs = {
202 | width: size,
203 | fill,
204 | color: fill,
205 | height: size
206 | }
207 | return this.html`
208 |
209 | `
210 | }
211 | }
212 | ```
213 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | https://tonicframework.dev
9 |
10 |
11 |
12 | Tonic is a low profile component framework for the web. It's one file, less than 3kb gzipped and has no dependencies. It's designed to be used with modern Javascript and is compatible with all modern browsers and built on top of the Web Components. It was designed to be similar to React but 100x easier to reason about.
13 |
14 | ## Installation
15 |
16 | ```sh
17 | npm install @socketsupply/tonic
18 | ```
19 |
20 | ## Usage
21 |
22 | ```js
23 | import Tonic from '@socketsupply/tonic'
24 | ```
25 |
26 | You can use functions as components. They can be async or even an async generator function.
27 |
28 | ```js
29 | async function MyGreeting () {
30 | const data = await (await fetch('https://example.com/data')).text()
31 | return this.html`
Hello, ${data} `
32 | }
33 | ```
34 |
35 | Or you can use classes. Every class must have a render method.
36 |
37 | ```js
38 | class MyGreeting extends Tonic {
39 | async * render () {
40 | yield this.html`
Loading...
`
41 |
42 | const data = await (await fetch('https://example.com/data')).text()
43 | return this.html`
Hello, ${data}.
`
44 | }
45 | }
46 | ```
47 |
48 | ```js
49 | Tonic.add(MyGreeting, 'my-greeting')
50 | ```
51 |
52 | After adding your Javascript to your HTML, you can use your component anywhere.
53 |
54 | ```html
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | ```
64 |
65 | # Useful links
66 | - [Tonic components](https://github.com/socketsupply/components)
67 | - [Migration from the early versions of Tonic](./MIGRATION.md)
68 | - [API](./API.md)
69 | - [Troubleshooting](./HELP.md)
70 |
71 | Copyright (c) 2023 Socket Supply Co.
72 |
73 | MIT License
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@socketsupply/tonic",
3 | "version": "15.1.2",
4 | "description": "A component framework.",
5 | "scripts": {
6 | "lint": "standard .",
7 | "test": "npm run build && npm run lint && esbuild --bundle test/index.js | tape-run",
8 | "ci:test:tape-run": "esbuild --bundle test/index.js | tape-run",
9 | "test:open": "npm run build && esbuild --bundle test/index.js | tape-run --browser chrome --keep-open",
10 | "build:base": "esbuild src/index.js --define:VERSION=\\\"$npm_package_version\\\" --outfile=index.js",
11 | "build:minify": "esbuild index.js --keep-names --minify --outfile=dist/tonic.min.js",
12 | "build": "npm run build:base && npm run build:minify",
13 | "prepublishOnly": "npm run build",
14 | "pub": "npm run build && npm run test && npm publish --registry=https://registry.npmjs.org && npm publish --registry https://npm.pkg.github.com"
15 | },
16 | "main": "index.js",
17 | "type": "module",
18 | "author": "socketsupply",
19 | "license": "MIT",
20 | "devDependencies": {
21 | "benchmark": "^2.1.4",
22 | "esbuild": "^0.19.0",
23 | "standard": "^17.0.0",
24 | "tape-run": "^11.0.0",
25 | "@socketsupply/tapzero": "^0.8.0",
26 | "uuid": "^9.0.0"
27 | },
28 | "standard": {
29 | "ignore": [
30 | "test/fixtures/*"
31 | ]
32 | },
33 | "directories": {
34 | "test": "test"
35 | },
36 | "repository": {
37 | "type": "git",
38 | "url": "git+https://github.com/socketsupply/tonic.git"
39 | },
40 | "bugs": {
41 | "url": "https://github.com/socketsupply/tonic/issues"
42 | },
43 | "homepage": "https://tonicframework.dev",
44 | "dependencies": {}
45 | }
46 |
--------------------------------------------------------------------------------
/readme-tonic-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/socketsupply/tonic/648b5f7dc80ff86ce36b512d00657298e0c6c855/readme-tonic-dark.png
--------------------------------------------------------------------------------
/readme-tonic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/socketsupply/tonic/648b5f7dc80ff86ce36b512d00657298e0c6c855/readme-tonic.png
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | class TonicTemplate {
3 | constructor (rawText, templateStrings, unsafe) {
4 | this.isTonicTemplate = true
5 | this.unsafe = unsafe
6 | this.rawText = rawText
7 | this.templateStrings = templateStrings
8 | }
9 |
10 | valueOf () { return this.rawText }
11 | toString () { return this.rawText }
12 | }
13 |
14 | export class Tonic extends window.HTMLElement {
15 | static _tags = ''
16 | static _refIds = []
17 | static _data = {}
18 | static _states = {}
19 | static _children = {}
20 | static _reg = {}
21 | static _stylesheetRegistry = []
22 | static _index = 0
23 | // eslint-disable-next-line no-undef
24 | static get version () { return VERSION ?? null }
25 | static get SPREAD () { return /\.\.\.\s?(__\w+__\w+__)/g }
26 | static get ESC () { return /["&'<>`/]/g }
27 | static get AsyncFunctionGenerator () { return async function * () {}.constructor }
28 | static get AsyncFunction () { return async function () {}.constructor }
29 | static get MAP () { return { '"': '"', '&': '&', '\'': ''', '<': '<', '>': '>', '`': '`', '/': '/' } }
30 |
31 | constructor () {
32 | super()
33 | const state = Tonic._states[super.id]
34 | delete Tonic._states[super.id]
35 | this._state = state || {}
36 | this.preventRenderOnReconnect = false
37 | this.props = {}
38 | this.elements = [...this.children]
39 | this.elements.__children__ = true
40 | this.nodes = [...this.childNodes]
41 | this.nodes.__children__ = true
42 | this._events()
43 | }
44 |
45 | get isTonicComponent () {
46 | return true
47 | }
48 |
49 | static _createId () {
50 | return `tonic${Tonic._index++}`
51 | }
52 |
53 | static _normalizeAttrs (o, x = {}) {
54 | [...o].forEach(o => (x[o.name] = o.value))
55 | return x
56 | }
57 |
58 | _checkId () {
59 | const _id = super.id
60 | if (!_id) {
61 | const html = this.outerHTML.replace(this.innerHTML, '...')
62 | throw new Error(`Component: ${html} has no id`)
63 | }
64 | return _id
65 | }
66 |
67 | get state () {
68 | return (this._checkId(), this._state)
69 | }
70 |
71 | set state (newState) {
72 | this._state = (this._checkId(), newState)
73 | }
74 |
75 | _events () {
76 | const hp = Object.getOwnPropertyNames(window.HTMLElement.prototype)
77 | for (const p of this._props) {
78 | if (hp.indexOf('on' + p) === -1) continue
79 | this.addEventListener(p, this)
80 | }
81 | }
82 |
83 | _prop (o) {
84 | const id = this._id
85 | const p = `__${id}__${Tonic._createId()}__`
86 | Tonic._data[id] = Tonic._data[id] || {}
87 | Tonic._data[id][p] = o
88 | return p
89 | }
90 |
91 | _placehold (r) {
92 | const id = this._id
93 | const ref = `placehold:${id}:${Tonic._createId()}__`
94 | Tonic._children[id] = Tonic._children[id] || {}
95 | Tonic._children[id][ref] = r
96 | return ref
97 | }
98 |
99 | static match (el, s) {
100 | if (!el.matches) el = el.parentElement
101 | return el.matches(s) ? el : el.closest(s)
102 | }
103 |
104 | static getTagName (camelName) {
105 | return camelName.match(/[A-Z][a-z0-9]*/g).join('-').toLowerCase()
106 | }
107 |
108 | static getPropertyNames (proto) {
109 | const props = []
110 | while (proto && proto !== Tonic.prototype) {
111 | props.push(...Object.getOwnPropertyNames(proto))
112 | proto = Object.getPrototypeOf(proto)
113 | }
114 | return props
115 | }
116 |
117 | static add (c, htmlName) {
118 | const hasValidName = htmlName || (c.name && c.name.length > 1)
119 | if (!hasValidName) {
120 | throw Error('Mangling. https://bit.ly/2TkJ6zP')
121 | }
122 |
123 | if (!htmlName) htmlName = Tonic.getTagName(c.name)
124 | if (!Tonic.ssr && window.customElements.get(htmlName)) {
125 | throw new Error(`Cannot Tonic.add(${c.name}, '${htmlName}') twice`)
126 | }
127 |
128 | if (!c.prototype || !c.prototype.isTonicComponent) {
129 | const tmp = { [c.name]: class extends Tonic {} }[c.name]
130 | tmp.prototype.render = c
131 | c = tmp
132 | }
133 |
134 | c.prototype._props = Tonic.getPropertyNames(c.prototype)
135 |
136 | Tonic._reg[htmlName] = c
137 | Tonic._tags = Object.keys(Tonic._reg).join()
138 | window.customElements.define(htmlName, c)
139 |
140 | if (typeof c.stylesheet === 'function') {
141 | Tonic.registerStyles(c.stylesheet)
142 | }
143 |
144 | return c
145 | }
146 |
147 | static registerStyles (stylesheetFn) {
148 | if (Tonic._stylesheetRegistry.includes(stylesheetFn)) return
149 | Tonic._stylesheetRegistry.push(stylesheetFn)
150 |
151 | const styleNode = document.createElement('style')
152 | if (Tonic.nonce) styleNode.setAttribute('nonce', Tonic.nonce)
153 | styleNode.appendChild(document.createTextNode(stylesheetFn()))
154 | if (document.head) document.head.appendChild(styleNode)
155 | }
156 |
157 | static escape (s) {
158 | return s.replace(Tonic.ESC, c => Tonic.MAP[c])
159 | }
160 |
161 | static unsafeRawString (s, templateStrings) {
162 | return new TonicTemplate(s, templateStrings, true)
163 | }
164 |
165 | dispatch (eventName, detail = null) {
166 | const opts = { bubbles: true, detail }
167 | this.dispatchEvent(new window.CustomEvent(eventName, opts))
168 | }
169 |
170 | html (strings, ...values) {
171 | const refs = o => {
172 | if (o && o.__children__) return this._placehold(o)
173 | if (o && o.isTonicTemplate) return o.rawText
174 | switch (Object.prototype.toString.call(o)) {
175 | case '[object HTMLCollection]':
176 | case '[object NodeList]': return this._placehold([...o])
177 | case '[object Array]': {
178 | if (o.every(x => x.isTonicTemplate && !x.unsafe)) {
179 | return new TonicTemplate(o.join('\n'), null, false)
180 | }
181 | return this._prop(o)
182 | }
183 | case '[object Object]':
184 | case '[object Function]':
185 | case '[object AsyncFunction]':
186 | case '[object Set]':
187 | case '[object Map]':
188 | case '[object WeakMap]':
189 | case '[object File]':
190 | return this._prop(o)
191 | case '[object NamedNodeMap]':
192 | return this._prop(Tonic._normalizeAttrs(o))
193 | case '[object Number]': return `${o}__float`
194 | case '[object String]': return Tonic.escape(o)
195 | case '[object Boolean]': return `${o}__boolean`
196 | case '[object Null]': return `${o}__null`
197 | case '[object HTMLElement]':
198 | return this._placehold([o])
199 | }
200 | if (
201 | typeof o === 'object' && o && o.nodeType === 1 &&
202 | typeof o.cloneNode === 'function'
203 | ) {
204 | return this._placehold([o])
205 | }
206 | return o
207 | }
208 |
209 | const out = []
210 | for (let i = 0; i < strings.length - 1; i++) {
211 | out.push(strings[i], refs(values[i]))
212 | }
213 | out.push(strings[strings.length - 1])
214 |
215 | const htmlStr = out.join('').replace(Tonic.SPREAD, (_, p) => {
216 | const o = Tonic._data[p.split('__')[1]][p]
217 | return Object.entries(o).map(([key, value]) => {
218 | const k = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
219 | if (value === true) return k
220 | else if (value) return `${k}="${Tonic.escape(String(value))}"`
221 | else return ''
222 | }).filter(Boolean).join(' ')
223 | })
224 | return new TonicTemplate(htmlStr, strings, false)
225 | }
226 |
227 | scheduleReRender (oldProps) {
228 | if (this.pendingReRender) return this.pendingReRender
229 |
230 | this.pendingReRender = new Promise(resolve => setTimeout(() => {
231 | if (!this.isInDocument(this.shadowRoot || this)) return
232 | const p = this._set(this.shadowRoot || this, this.render)
233 | this.pendingReRender = null
234 |
235 | if (p && p.then) {
236 | return p.then(() => {
237 | this.updated && this.updated(oldProps)
238 | resolve(this)
239 | })
240 | }
241 |
242 | this.updated && this.updated(oldProps)
243 | resolve(this)
244 | }, 0))
245 |
246 | return this.pendingReRender
247 | }
248 |
249 | reRender (o = this.props) {
250 | const oldProps = { ...this.props }
251 | this.props = typeof o === 'function' ? o(oldProps) : o
252 | return this.scheduleReRender(oldProps)
253 | }
254 |
255 | handleEvent (e) {
256 | this[e.type](e)
257 | }
258 |
259 | _drainIterator (target, iterator) {
260 | return iterator.next().then((result) => {
261 | this._set(target, null, result.value)
262 | if (result.done) return
263 | return this._drainIterator(target, iterator)
264 | })
265 | }
266 |
267 | _set (target, render, content = '') {
268 | this.willRender && this.willRender()
269 | for (const node of target.querySelectorAll(Tonic._tags)) {
270 | if (!node.isTonicComponent) continue
271 |
272 | const id = node.getAttribute('id')
273 | if (!id || !Tonic._refIds.includes(id)) continue
274 | Tonic._states[id] = node.state
275 | }
276 |
277 | if (render instanceof Tonic.AsyncFunction) {
278 | return (render
279 | .call(this, this.html, this.props)
280 | .then(content => this._apply(target, content))
281 | )
282 | } else if (render instanceof Tonic.AsyncFunctionGenerator) {
283 | return this._drainIterator(target, render.call(this))
284 | } else if (render === null) {
285 | this._apply(target, content)
286 | } else if (render instanceof Function) {
287 | this._apply(target, render.call(this, this.html, this.props) || '')
288 | }
289 | }
290 |
291 | _apply (target, content) {
292 | if (content && content.isTonicTemplate) {
293 | content = content.rawText
294 | } else if (typeof content === 'string') {
295 | content = Tonic.escape(content)
296 | }
297 |
298 | if (typeof content === 'string') {
299 | if (this.stylesheet) {
300 | content = `${content}`
301 | }
302 |
303 | target.innerHTML = content
304 |
305 | if (this.styles) {
306 | const styles = this.styles()
307 | for (const node of target.querySelectorAll('[styles]')) {
308 | for (const s of node.getAttribute('styles').split(/\s+/)) {
309 | Object.assign(node.style, styles[s.trim()])
310 | }
311 | }
312 | }
313 |
314 | const children = Tonic._children[this._id] || {}
315 |
316 | const walk = (node, fn) => {
317 | if (node.nodeType === 3) {
318 | const id = node.textContent.trim()
319 | if (children[id]) fn(node, children[id], id)
320 | }
321 |
322 | const childNodes = node.childNodes
323 | if (!childNodes) return
324 |
325 | for (let i = 0; i < childNodes.length; i++) {
326 | walk(childNodes[i], fn)
327 | }
328 | }
329 |
330 | walk(target, (node, children, id) => {
331 | for (const child of children) {
332 | node.parentNode.insertBefore(child, node)
333 | }
334 | delete Tonic._children[this._id][id]
335 | node.parentNode.removeChild(node)
336 | })
337 | } else {
338 | target.innerHTML = ''
339 | target.appendChild(content.cloneNode(true))
340 | }
341 | }
342 |
343 | connectedCallback () {
344 | this.root = this.shadowRoot || this // here for back compat
345 |
346 | if (super.id && !Tonic._refIds.includes(super.id)) {
347 | Tonic._refIds.push(super.id)
348 | }
349 | const cc = s => s.replace(/-(.)/g, (_, m) => m.toUpperCase())
350 |
351 | for (const { name: _name, value } of this.attributes) {
352 | const name = cc(_name)
353 | const p = this.props[name] = value
354 |
355 | if (/__\w+__\w+__/.test(p)) {
356 | const { 1: root } = p.split('__')
357 | this.props[name] = Tonic._data[root][p]
358 | } else if (/\d+__float/.test(p)) {
359 | this.props[name] = parseFloat(p, 10)
360 | } else if (p === 'null__null') {
361 | this.props[name] = null
362 | } else if (/\w+__boolean/.test(p)) {
363 | this.props[name] = p.includes('true')
364 | } else if (/placehold:\w+:\w+__/.test(p)) {
365 | const { 1: root } = p.split(':')
366 | this.props[name] = Tonic._children[root][p][0]
367 | }
368 | }
369 |
370 | this.props = Object.assign(
371 | this.defaults ? this.defaults() : {},
372 | this.props
373 | )
374 |
375 | this._id = this._id || Tonic._createId()
376 |
377 | this.willConnect && this.willConnect()
378 |
379 | if (!this.isInDocument(this.root)) return
380 | if (!this.preventRenderOnReconnect) {
381 | if (!this._source) {
382 | this._source = this.innerHTML
383 | } else {
384 | this.innerHTML = this._source
385 | }
386 | const p = this._set(this.root, this.render)
387 | if (p && p.then) return p.then(() => this.connected && this.connected())
388 | }
389 |
390 | this.connected && this.connected()
391 | }
392 |
393 | isInDocument (target) {
394 | const root = target.getRootNode()
395 | return root === document || root.toString() === '[object ShadowRoot]'
396 | }
397 |
398 | disconnectedCallback () {
399 | this.disconnected && this.disconnected()
400 | delete Tonic._data[this._id]
401 | delete Tonic._children[this._id]
402 | }
403 | }
404 |
405 | export default Tonic
406 |
--------------------------------------------------------------------------------
/test/fixtures/htm.js:
--------------------------------------------------------------------------------
1 | var e=function(){},t={},n=[],o=[];function r(t,r){var i,l,a,s,p=arguments,u=o;for(s=arguments.length;s-- >2;)n.push(p[s]);for(r&&null!=r.children&&(n.length||n.push(r.children),delete r.children);n.length;)if((l=n.pop())&&void 0!==l.pop)for(s=l.length;s--;)n.push(l[s]);else"boolean"==typeof l&&(l=null),(a="function"!=typeof t)&&(null==l?l="":"number"==typeof l?l=String(l):"string"!=typeof l&&(a=!1)),a&&i?u[u.length-1]+=l:u===o?u=[l]:u.push(l),i=a;var c=new e;return c.nodeName=t,c.children=u,c.attributes=null==r?void 0:r,c.key=null==r?void 0:r.key,c}function i(e,t){for(var n in t)e[n]=t[n];return e}function l(e,t){null!=e&&("function"==typeof e?e(t):e.current=t)}var a="function"==typeof Promise?Promise.resolve().then.bind(Promise.resolve()):setTimeout,s=/acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i,p=[];function u(e){!e._dirty&&(e._dirty=!0)&&1==p.push(e)&&a(c)}function c(){for(var e;e=p.pop();)e._dirty&&U(e)}function f(e,t){return e.normalizedNodeName===t||e.nodeName.toLowerCase()===t.toLowerCase()}function d(e){var t=i({},e.attributes);t.children=e.children;var n=e.nodeName.defaultProps;if(void 0!==n)for(var o in n)void 0===t[o]&&(t[o]=n[o]);return t}function v(e){var t=e.parentNode;t&&t.removeChild(e)}function h(e,t,n,o,r){if("className"===t&&(t="class"),"key"===t);else if("ref"===t)l(n,null),l(o,e);else if("class"!==t||r)if("style"===t){if(o&&"string"!=typeof o&&"string"!=typeof n||(e.style.cssText=o||""),o&&"object"==typeof o){if("string"!=typeof n)for(var i in n)i in o||(e.style[i]="");for(var i in o)e.style[i]="number"==typeof o[i]&&!1===s.test(i)?o[i]+"px":o[i]}}else if("dangerouslySetInnerHTML"===t)o&&(e.innerHTML=o.__html||"");else if("o"==t[0]&&"n"==t[1]){var a=t!==(t=t.replace(/Capture$/,""));t=t.toLowerCase().substring(2),o?n||e.addEventListener(t,m,a):e.removeEventListener(t,m,a),(e._listeners||(e._listeners={}))[t]=o}else if("list"!==t&&"type"!==t&&!r&&t in e){try{e[t]=null==o?"":o}catch(e){}null!=o&&!1!==o||"spellcheck"==t||e.removeAttribute(t)}else{var p=r&&t!==(t=t.replace(/^xlink:?/,""));null==o||!1===o?p?e.removeAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase()):e.removeAttribute(t):"function"!=typeof o&&(p?e.setAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase(),o):e.setAttribute(t,o))}else e.className=o||""}function m(e){return this._listeners[e.type](e)}var _=[],y=0,g=!1,b=!1;function C(){for(var e;e=_.shift();)e.componentDidMount&&e.componentDidMount()}function x(e,t,n,o,r,i){y++||(g=null!=r&&void 0!==r.ownerSVGElement,b=null!=e&&!("__preactattr_"in e));var l=function e(t,n,o,r,i){var l=t,a=g;if(null!=n&&"boolean"!=typeof n||(n=""),"string"==typeof n||"number"==typeof n)return t&&void 0!==t.splitText&&t.parentNode&&(!t._component||i)?t.nodeValue!=n&&(t.nodeValue=n):(l=document.createTextNode(n),t&&(t.parentNode&&t.parentNode.replaceChild(l,t),N(t,!0))),l.__preactattr_=!0,l;var s,p,u=n.nodeName;if("function"==typeof u)return function(e,t,n,o){for(var r=e&&e._component,i=r,l=e,a=r&&e._componentConstructor===t.nodeName,s=a,p=d(t);r&&!s&&(r=r._parentComponent);)s=r.constructor===t.nodeName;return r&&s&&(!o||r._component)?(B(r,p,3,n,o),e=r.base):(i&&!a&&(L(i),e=l=null),r=S(t.nodeName,p,n),e&&!r.nextBase&&(r.nextBase=e,l=null),B(r,p,1,n,o),e=r.base,l&&e!==l&&(l._component=null,N(l,!1))),e}(t,n,o,r);if(g="svg"===u||"foreignObject"!==u&&g,u=String(u),(!t||!f(t,u))&&(s=u,(p=g?document.createElementNS("http://www.w3.org/2000/svg",s):document.createElement(s)).normalizedNodeName=s,l=p,t)){for(;t.firstChild;)l.appendChild(t.firstChild);t.parentNode&&t.parentNode.replaceChild(l,t),N(t,!0)}var c=l.firstChild,m=l.__preactattr_,_=n.children;if(null==m){m=l.__preactattr_={};for(var y=l.attributes,C=y.length;C--;)m[y[C].name]=y[C].value}return!b&&_&&1===_.length&&"string"==typeof _[0]&&null!=c&&void 0!==c.splitText&&null==c.nextSibling?c.nodeValue!=_[0]&&(c.nodeValue=_[0]):(_&&_.length||null!=c)&&function(t,n,o,r,i){var l,a,s,p,u,c,d,h,m=t.childNodes,_=[],y={},g=0,b=0,C=m.length,x=0,w=n?n.length:0;if(0!==C)for(var k=0;k
"===t?(a(),o=1):o&&("="===t?(o=4,n=r,r=""):"/"===t?(a(),3===o&&(l=l[0]),o=l,(l=l[0]).push(o,4),o=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(a(),o=2):r+=t)}return a(),l},D="function"==typeof Map,E=D?new Map:{},V=D?function(e){var t=E.get(e);return t||E.set(e,t=W(e)),t}:function(e){for(var t="",n=0;n1?t:t[0]}.bind(r);export{r as h,H as html,A as render,T as Component};
2 |
--------------------------------------------------------------------------------
/test/fixtures/preact.js:
--------------------------------------------------------------------------------
1 | !function(){"use strict";function e(){}function t(t,n){var o,r,i,l,a=E;for(l=arguments.length;l-- >2;)W.push(arguments[l]);n&&null!=n.children&&(W.length||W.push(n.children),delete n.children);while(W.length)if((r=W.pop())&&void 0!==r.pop)for(l=r.length;l--;)W.push(r[l]);else r!==!0&&r!==!1||(r=null),(i="function"!=typeof t)&&(null==r?r="":"number"==typeof r?r+="":"string"!=typeof r&&(i=!1)),i&&o?a[a.length-1]+=r:a===E?a=[r]:a.push(r),o=i;var _=new e;return _.nodeName=t,_.children=a,_.attributes=null==n?void 0:n,_.key=null==n?void 0:n.key,void 0!==S.vnode&&S.vnode(_),_}function n(e,t){for(var n in t)e[n]=t[n];return e}function o(e,o){return t(e.nodeName,n(n({},e.attributes),o),arguments.length>2?[].slice.call(arguments,2):e.children)}function r(e){!e.__d&&(e.__d=!0)&&1==A.push(e)&&(S.debounceRendering||setTimeout)(i)}function i(){var e,t=A;A=[];while(e=t.pop())e.__d&&k(e)}function l(e,t,n){return"string"==typeof t||"number"==typeof t?void 0!==e.splitText:"string"==typeof t.nodeName?!e._componentConstructor&&a(e,t.nodeName):n||e._componentConstructor===t.nodeName}function a(e,t){return e.__n===t||e.nodeName.toLowerCase()===t.toLowerCase()}function _(e){var t=n({},e.attributes);t.children=e.children;var o=e.nodeName.defaultProps;if(void 0!==o)for(var r in o)void 0===t[r]&&(t[r]=o[r]);return t}function u(e,t){var n=t?document.createElementNS("http://www.w3.org/2000/svg",e):document.createElement(e);return n.__n=e,n}function c(e){e.parentNode&&e.parentNode.removeChild(e)}function p(e,t,n,o,r){if("className"===t&&(t="class"),"key"===t);else if("ref"===t)n&&n(null),o&&o(e);else if("class"!==t||r)if("style"===t){if(o&&"string"!=typeof o&&"string"!=typeof n||(e.style.cssText=o||""),o&&"object"==typeof o){if("string"!=typeof n)for(var i in n)i in o||(e.style[i]="");for(var i in o)e.style[i]="number"==typeof o[i]&&V.test(i)===!1?o[i]+"px":o[i]}}else if("dangerouslySetInnerHTML"===t)o&&(e.innerHTML=o.__html||"");else if("o"==t[0]&&"n"==t[1]){var l=t!==(t=t.replace(/Capture$/,""));t=t.toLowerCase().substring(2),o?n||e.addEventListener(t,d,l):e.removeEventListener(t,d,l),(e.__l||(e.__l={}))[t]=o}else if("list"!==t&&"type"!==t&&!r&&t in e)s(e,t,null==o?"":o),null!=o&&o!==!1||e.removeAttribute(t);else{var a=r&&t!==(t=t.replace(/^xlink\:?/,""));null==o||o===!1?a?e.removeAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase()):e.removeAttribute(t):"function"!=typeof o&&(a?e.setAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase(),o):e.setAttribute(t,o))}else e.className=o||""}function s(e,t,n){try{e[t]=n}catch(e){}}function d(e){return this.__l[e.type](S.event&&S.event(e)||e)}function f(){var e;while(e=D.pop())S.afterMount&&S.afterMount(e),e.componentDidMount&&e.componentDidMount()}function h(e,t,n,o,r,i){H++||(P=null!=r&&void 0!==r.ownerSVGElement,R=null!=e&&!("__preactattr_"in e));var l=m(e,t,n,o,i);return r&&l.parentNode!==r&&r.appendChild(l),--H||(R=!1,i||f()),l}function m(e,t,n,o,r){var i=e,l=P;if(null==t&&(t=""),"string"==typeof t)return e&&void 0!==e.splitText&&e.parentNode&&(!e._component||r)?e.nodeValue!=t&&(e.nodeValue=t):(i=document.createTextNode(t),e&&(e.parentNode&&e.parentNode.replaceChild(i,e),b(e,!0))),i.__preactattr_=!0,i;if("function"==typeof t.nodeName)return U(e,t,n,o);if(P="svg"===t.nodeName||"foreignObject"!==t.nodeName&&P,(!e||!a(e,t.nodeName+""))&&(i=u(t.nodeName+"",P),e)){while(e.firstChild)i.appendChild(e.firstChild);e.parentNode&&e.parentNode.replaceChild(i,e),b(e,!0)}var _=i.firstChild,c=i.__preactattr_||(i.__preactattr_={}),p=t.children;return!R&&p&&1===p.length&&"string"==typeof p[0]&&null!=_&&void 0!==_.splitText&&null==_.nextSibling?_.nodeValue!=p[0]&&(_.nodeValue=p[0]):(p&&p.length||null!=_)&&v(i,p,n,o,R||null!=c.dangerouslySetInnerHTML),g(i,t.attributes,c),P=l,i}function v(e,t,n,o,r){var i,a,_,u,p=e.childNodes,s=[],d={},f=0,h=0,v=p.length,y=0,g=t?t.length:0;if(0!==v)for(var N=0;N=v?e.appendChild(u):u!==p[N]&&(u===p[N+1]?c(p[N]):e.insertBefore(u,p[N]||null)))}if(f)for(var N in d)void 0!==d[N]&&b(d[N],!1);while(h<=y)void 0!==(u=s[y--])&&b(u,!1)}function b(e,t){var n=e._component;n?L(n):(null!=e.__preactattr_&&e.__preactattr_.ref&&e.__preactattr_.ref(null),t!==!1&&null!=e.__preactattr_||c(e),y(e))}function y(e){e=e.lastChild;while(e){var t=e.previousSibling;b(e,!0),e=t}}function g(e,t,n){var o;for(o in n)t&&null!=t[o]||null==n[o]||p(e,o,n[o],n[o]=void 0,P);for(o in t)"children"===o||"innerHTML"===o||o in n&&t[o]===("value"===o||"checked"===o?e[o]:n[o])||p(e,o,n[o],n[o]=t[o],P)}function N(e){var t=e.constructor.name;(j[t]||(j[t]=[])).push(e)}function w(e,t,n){var o,r=j[e.name];if(e.prototype&&e.prototype.render?(o=new e(t,n),T.call(o,t,n)):(o=new T(t,n),o.constructor=e,o.render=C),r)for(var i=r.length;i--;)if(r[i].constructor===e){o.__b=r[i].__b,r.splice(i,1);break}return o}function C(e,t,n){return this.constructor(e,n)}function x(e,t,n,o,i){e.__x||(e.__x=!0,(e.__r=t.ref)&&delete t.ref,(e.__k=t.key)&&delete t.key,!e.base||i?e.componentWillMount&&e.componentWillMount():e.componentWillReceiveProps&&e.componentWillReceiveProps(t,o),o&&o!==e.context&&(e.__c||(e.__c=e.context),e.context=o),e.__p||(e.__p=e.props),e.props=t,e.__x=!1,0!==n&&(1!==n&&S.syncComponentUpdates===!1&&e.base?r(e):k(e,1,i)),e.__r&&e.__r(e))}function k(e,t,o,r){if(!e.__x){var i,l,a,u=e.props,c=e.state,p=e.context,s=e.__p||u,d=e.__s||c,m=e.__c||p,v=e.base,y=e.__b,g=v||y,N=e._component,C=!1;if(v&&(e.props=s,e.state=d,e.context=m,2!==t&&e.shouldComponentUpdate&&e.shouldComponentUpdate(u,c,p)===!1?C=!0:e.componentWillUpdate&&e.componentWillUpdate(u,c,p),e.props=u,e.state=c,e.context=p),e.__p=e.__s=e.__c=e.__b=null,e.__d=!1,!C){i=e.render(u,c,p),e.getChildContext&&(p=n(n({},p),e.getChildContext()));var U,T,M=i&&i.nodeName;if("function"==typeof M){var W=_(i);l=N,l&&l.constructor===M&&W.key==l.__k?x(l,W,1,p,!1):(U=l,e._component=l=w(M,W,p),l.__b=l.__b||y,l.__u=e,x(l,W,0,p,!1),k(l,1,o,!0)),T=l.base}else a=g,U=N,U&&(a=e._component=null),(g||1===t)&&(a&&(a._component=null),T=h(a,i,p,o||!v,g&&g.parentNode,!0));if(g&&T!==g&&l!==N){var E=g.parentNode;E&&T!==E&&(E.replaceChild(T,g),U||(g._component=null,b(g,!1)))}if(U&&L(U),e.base=T,T&&!r){var V=e,A=e;while(A=A.__u)(V=A).base=T;T._component=V,T._componentConstructor=V.constructor}}if(!v||o?D.unshift(e):C||(f(),e.componentDidUpdate&&e.componentDidUpdate(s,d,m),S.afterUpdate&&S.afterUpdate(e)),null!=e.__h)while(e.__h.length)e.__h.pop().call(e);H||r||f()}}function U(e,t,n,o){var r=e&&e._component,i=r,l=e,a=r&&e._componentConstructor===t.nodeName,u=a,c=_(t);while(r&&!u&&(r=r.__u))u=r.constructor===t.nodeName;return r&&u&&(!o||r._component)?(x(r,c,3,n,o),e=r.base):(i&&!a&&(L(i),e=l=null),r=w(t.nodeName,c,n),e&&!r.__b&&(r.__b=e,l=null),x(r,c,1,n,o),e=r.base,l&&e!==l&&(l._component=null,b(l,!1))),e}function L(e){S.beforeUnmount&&S.beforeUnmount(e);var t=e.base;e.__x=!0,e.componentWillUnmount&&e.componentWillUnmount(),e.base=null;var n=e._component;n?L(n):t&&(t.__preactattr_&&t.__preactattr_.ref&&t.__preactattr_.ref(null),e.__b=t,c(t),N(e),y(t)),e.__r&&e.__r(null)}function T(e,t){this.__d=!0,this.context=t,this.props=e,this.state=this.state||{}}function M(e,t,n){return h(n,e,{},!1,t,!1)}var S={},W=[],E=[],V=/acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i,A=[],D=[],H=0,P=!1,R=!1,j={};n(T.prototype,{setState:function(e,t){var o=this.state;this.__s||(this.__s=n({},o)),n(o,"function"==typeof e?e(o,this.props):e),t&&(this.__h=this.__h||[]).push(t),r(this)},forceUpdate:function(e){e&&(this.__h=this.__h||[]).push(e),k(this,2)},render:function(){}});var I={h:t,createElement:t,cloneElement:o,Component:T,render:M,rerender:i,options:S};"undefined"!=typeof module?module.exports=I:self.preact=I}();
2 | //# sourceMappingURL=preact.min.js.map
3 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import { test } from '@socketsupply/tapzero'
2 | import { v4 as uuid } from 'uuid'
3 | import Tonic from '../index.js'
4 |
5 | const sleep = async t => new Promise(resolve => setTimeout(resolve, t))
6 |
7 | test('sanity', async t => {
8 | t.ok(true)
9 |
10 | const version = Tonic.version
11 | const parts = version.split('.')
12 | t.ok(parseInt(parts[0]) >= 10)
13 | })
14 |
15 | test('pass an async function as an event handler', t => {
16 | t.plan(1)
17 |
18 | class TheApp extends Tonic {
19 | async clicker (msg) {
20 | t.equal(msg, 'hello', 'should get the event')
21 | }
22 |
23 | render () {
24 | return this.html`
25 |
26 |
`
27 | }
28 | }
29 |
30 | class FnExample extends Tonic {
31 | click (ev) {
32 | ev.preventDefault()
33 | this.props.onbtnclick('hello')
34 | }
35 |
36 | render () {
37 | return this.html`
38 | example
39 | clicker
40 |
`
41 | }
42 | }
43 |
44 | document.body.innerHTML = `
45 |
46 | `
47 |
48 | Tonic.add(FnExample)
49 | Tonic.add(TheApp)
50 |
51 | document.getElementById('btn').click()
52 | })
53 |
54 | test('get kebab case from camel case', t => {
55 | const kebab = Tonic.getTagName('MyExample')
56 | t.equal(typeof kebab, 'string', 'should return a string')
57 | t.equal(kebab, 'my-example', 'should create kebab case given camel case')
58 |
59 | class MyExample extends Tonic {
60 | render () {
61 | return this.html`example
`
62 | }
63 | }
64 | t.equal(Tonic.getTagName(MyExample.name), 'my-example',
65 | 'example using a tonic component')
66 | })
67 |
68 | test('attach to dom', async t => {
69 | class ComponentA extends Tonic {
70 | render () {
71 | return this.html`
`
72 | }
73 | }
74 |
75 | document.body.innerHTML = `
76 |
77 | `
78 |
79 | Tonic.add(ComponentA)
80 |
81 | const div = document.querySelector('div')
82 | t.ok(div, 'a div was created and attached')
83 | })
84 |
85 | test('render-only component', async t => {
86 | function ComponentFun () {
87 | return this.html`
88 |
89 |
90 | `
91 | }
92 |
93 | document.body.innerHTML = `
94 |
95 | `
96 |
97 | Tonic.add(ComponentFun)
98 |
99 | const div = document.querySelector('div')
100 | t.ok(div, 'a div was created and attached')
101 | })
102 |
103 | test('render-only component (non-contextual)', async t => {
104 | function ComponentVeryFun (html, props) {
105 | return html`
106 |
107 |
108 | `
109 | }
110 |
111 | document.body.innerHTML = `
112 |
113 | `
114 |
115 | Tonic.add(ComponentVeryFun)
116 |
117 | const div = document.querySelector('div')
118 | t.ok(div, 'a div was created and attached')
119 | t.equal(div.dataset.id, 'okokok')
120 | })
121 |
122 | test('Tonic escapes text', async t => {
123 | class Comp extends Tonic {
124 | render () {
125 | const userInput = this.props.userInput
126 | return this.html`${userInput}
`
127 | }
128 | }
129 | const compName = `x-${uuid()}`
130 | Tonic.add(Comp, compName)
131 |
132 | const userInput = 'lol '
133 | document.body.innerHTML = `
134 | <${compName} user-input="${userInput}">${compName}>
135 | `
136 |
137 | const divs = document.querySelectorAll('div')
138 | t.equal(divs.length, 1)
139 | const div = divs[0]
140 | t.equal(div.childNodes.length, 1)
141 | t.equal(div.childNodes[0].nodeType, 3)
142 | t.equal(div.innerHTML, '<pre>lol</pre>')
143 | t.equal(div.childNodes[0].data, 'lol ')
144 | })
145 |
146 | test('Tonic supports array of templates', async t => {
147 | class Comp1 extends Tonic {
148 | render () {
149 | const options = []
150 | for (const o of ['one', 'two', 'three']) {
151 | options.push(this.html`
152 | ${o}
153 | `)
154 | }
155 |
156 | return this.html`${options} `
157 | }
158 | }
159 | const compName = `x-${uuid()}`
160 | Tonic.add(Comp1, compName)
161 |
162 | document.body.innerHTML = `<${compName}>${compName}>`
163 | const options = document.body.querySelectorAll('option')
164 | t.equal(options.length, 3)
165 | })
166 |
167 | test('Tonic escapes attribute injection', async t => {
168 | class Comp1 extends Tonic {
169 | render () {
170 | const userInput2 = '" onload="console.log(42)'
171 | const userInput = '">'
172 | const userInput3 = 'a" onmouseover="alert(1)"'
173 |
174 | const input = this.props.input === 'script'
175 | ? userInput
176 | : this.props.input === 'space'
177 | ? userInput3
178 | : userInput2
179 |
180 | if (this.props.spread) {
181 | return this.html`
182 |
183 | `
184 | }
185 |
186 | if (this.props.quoted) {
187 | return this.html`
188 |
189 | `
190 | }
191 |
192 | return this.html`
193 |
194 | `
195 | }
196 | }
197 | const compName = `x-${uuid()}`
198 | Tonic.add(Comp1, compName)
199 |
200 | document.body.innerHTML = `
201 | <${compName} input="space" quoted="1">${compName}>
202 | <${compName} input="space" spread="1">${compName}>
203 |
204 | <${compName} input="space">${compName}>
205 | <${compName} input="script" quoted="1">${compName}>
206 | <${compName} input="script" spread="1">${compName}>
207 | <${compName} input="script">${compName}>
208 | <${compName} quoted="1">${compName}>
209 | <${compName} spread="1">${compName}>
210 |
211 | <${compName}>${compName}>
212 | `
213 |
214 | const divs = document.querySelectorAll('div')
215 | t.equal(divs.length, 9)
216 | for (let i = 0; i < divs.length; i++) {
217 | const div = divs[i]
218 | t.equal(div.childNodes.length, 0)
219 | t.equal(div.hasAttribute('onmouseover'), i === 2)
220 | t.equal(div.hasAttribute('onload'), i === 8)
221 | }
222 | })
223 |
224 | test('attach to dom with shadow', async t => {
225 | Tonic.add(class ShadowComponent extends Tonic {
226 | constructor (o) {
227 | super(o)
228 | this.attachShadow({ mode: 'open' })
229 | }
230 |
231 | render () {
232 | return this.html`
233 |
234 |
235 |
236 | `
237 | }
238 | })
239 |
240 | document.body.innerHTML = `
241 |
242 | `
243 |
244 | const c = document.querySelector('shadow-component')
245 | const el = document.querySelector('div')
246 | t.ok(!el, 'no div found in document')
247 | const div = c.shadowRoot.querySelector('div')
248 | t.ok(div, 'a div was created and attached to the shadow root')
249 | t.ok(div.hasAttribute('num'), 'attributes added correctly')
250 | t.ok(div.hasAttribute('str'), 'attributes added correctly')
251 | })
252 |
253 | test('pass props', async t => {
254 | Tonic.add(class ComponentBB extends Tonic {
255 | render () {
256 | return this.html`${this.props.data[0].foo}
`
257 | }
258 | })
259 |
260 | Tonic.add(class ComponentB extends Tonic {
261 | connected () {
262 | this.setAttribute('id', this.props.id)
263 | t.equal(this.props.disabled, '', 'disabled property was found')
264 | t.equal(this.props.empty, '', 'empty property was found')
265 | t.ok(this.props.testItem, 'automatically camelcase props')
266 | }
267 |
268 | render () {
269 | const test = [
270 | { foo: 'hello, world' }
271 | ]
272 |
273 | return this.html`
274 | 'hello, world'}
279 | set=${new Set(['foo'])}
280 | map=${new Map([['bar', 'bar']])}
281 | weakmap=${new WeakMap([[document, 'baz']])}>
282 |
283 | `
284 | }
285 | })
286 |
287 | document.body.innerHTML = `
288 |
293 |
294 | `
295 |
296 | const bb = document.getElementById('y')
297 | {
298 | const props = bb.props
299 | t.equal(props.fn(), 'hello, world', 'passed a function')
300 | t.equal(props.number, 42.42, 'float parsed properly')
301 | t.equal(props.set.has('foo'), true, 'set parsed properly')
302 | t.equal(props.map.get('bar'), 'bar', 'map parsed properly')
303 | t.equal(props.weakmap.get(document), 'baz', 'weak map parsed properly')
304 | }
305 |
306 | const div1 = document.getElementsByTagName('div')[0]
307 | t.equal(div1.textContent, 'hello, world', 'data prop received properly')
308 |
309 | const div2 = document.getElementById('x')
310 | t.ok(div2)
311 |
312 | const props = div2.props
313 | t.equal(props.testItem, 'true', 'correct props')
314 | })
315 |
316 | test('get element by id and set properties via the api', async t => {
317 | document.body.innerHTML = `
318 |
319 | `
320 |
321 | class ComponentC extends Tonic {
322 | willConnect () {
323 | this.setAttribute('id', 'test')
324 | }
325 |
326 | render () {
327 | return this.html`${String(this.props.number)}
`
328 | }
329 | }
330 |
331 | Tonic.add(ComponentC)
332 |
333 | {
334 | const div = document.getElementById('test')
335 | t.ok(div, 'a component was found by its id')
336 | t.equal(div.textContent, '1', 'initial value is set by props')
337 | t.ok(div.reRender, 'a component has the reRender method')
338 | }
339 |
340 | const div = document.getElementById('test')
341 | div.reRender({ number: 2 })
342 |
343 | await sleep(1)
344 | t.equal(div.textContent, '2', 'the value was changed by reRender')
345 | })
346 |
347 | test('inheritance and super.render()', async t => {
348 | class Stuff extends Tonic {
349 | render () {
350 | return this.html`nice stuff
`
351 | }
352 | }
353 |
354 | class SpecificStuff extends Stuff {
355 | render () {
356 | return this.html`
357 |
358 |
359 | ${super.render()}
360 |
361 | `
362 | }
363 | }
364 |
365 | const compName = `x-${uuid()}`
366 | Tonic.add(SpecificStuff, compName)
367 |
368 | document.body.innerHTML = `
369 | <${compName}>${compName}>
370 | `
371 |
372 | const divs = document.querySelectorAll('div')
373 | t.equal(divs.length, 2)
374 |
375 | const first = divs[0]
376 | t.equal(first.childNodes.length, 5)
377 | t.equal(first.childNodes[1].tagName, 'HEADER')
378 | t.equal(first.childNodes[3].tagName, 'DIV')
379 | t.equal(first.childNodes[3].textContent, 'nice stuff')
380 | })
381 |
382 | test('Tonic#html returns raw string', async t => {
383 | class Stuff extends Tonic {
384 | render () {
385 | return this.html`nice stuff
`
386 | }
387 | }
388 |
389 | class SpecificStuff extends Stuff {
390 | render () {
391 | return this.html`
392 |
393 |
394 | ${super.render()}
395 |
396 | `
397 | }
398 | }
399 |
400 | const compName = `x-${uuid()}`
401 | Tonic.add(SpecificStuff, compName)
402 |
403 | document.body.innerHTML = `
404 | <${compName}>${compName}>
405 | `
406 |
407 | const divs = document.querySelectorAll('div')
408 | t.equal(divs.length, 2)
409 |
410 | const first = divs[0]
411 | t.equal(first.childNodes.length, 5)
412 | t.equal(first.childNodes[1].tagName, 'HEADER')
413 | t.equal(first.childNodes[3].tagName, 'DIV')
414 | t.equal(first.childNodes[3].textContent, 'nice stuff')
415 | })
416 |
417 | test('construct from api', async t => {
418 | document.body.innerHTML = ''
419 |
420 | class ComponentD extends Tonic {
421 | render () {
422 | return this.html`
`
423 | }
424 | }
425 |
426 | Tonic.add(ComponentD)
427 | const d = new ComponentD()
428 | document.body.appendChild(d)
429 |
430 | d.reRender({ number: 3 })
431 |
432 | await sleep(1)
433 | const div1 = document.body.querySelector('div')
434 | t.equal(div1.getAttribute('number'), '3', 'attribute was set in component')
435 |
436 | d.reRender({ number: 6 })
437 |
438 | await sleep(1)
439 | const div2 = document.body.querySelector('div')
440 | t.equal(div2.getAttribute('number'), '6', 'attribute was set in component')
441 | })
442 |
443 | test('stylesheets and inline styles', async t => {
444 | document.body.innerHTML = `
445 |
446 | `
447 |
448 | class ComponentF extends Tonic {
449 | stylesheet () {
450 | return 'component-f div { color: red; }'
451 | }
452 |
453 | styles () {
454 | return {
455 | foo: {
456 | color: 'red'
457 | },
458 | bar: {
459 | backgroundColor: 'red'
460 | }
461 | }
462 | }
463 |
464 | render () {
465 | return this.html`
`
466 | }
467 | }
468 |
469 | Tonic.add(ComponentF)
470 |
471 | const expected = 'component-f div { color: red; }'
472 | const style = document.querySelector('component-f style')
473 | t.equal(style.textContent, expected, 'style was prefixed')
474 | const div = document.querySelector('component-f div')
475 | const computed = window.getComputedStyle(div)
476 | t.equal(computed.color, 'rgb(255, 0, 0)', 'inline style was set')
477 | t.equal(computed.backgroundColor, 'rgb(255, 0, 0)', 'inline style was set')
478 | })
479 |
480 | test('static stylesheet', async t => {
481 | document.body.innerHTML = `
482 |
483 |
484 | `
485 |
486 | class ComponentStaticStyles extends Tonic {
487 | static stylesheet () {
488 | return 'component-static-styles div { color: red; }'
489 | }
490 |
491 | render () {
492 | return this.html`RED
`
493 | }
494 | }
495 |
496 | Tonic.add(ComponentStaticStyles)
497 |
498 | const style = document.head.querySelector('style')
499 | t.ok(style, 'has a style tag')
500 | const div = document.querySelector('component-static-styles div')
501 | const computed = window.getComputedStyle(div)
502 | t.equal(computed.color, 'rgb(255, 0, 0)', 'inline style was set')
503 | })
504 |
505 | test('component composition', async t => {
506 | document.body.innerHTML = `
507 | A Few
508 |
509 | Noisy
510 |
511 | Text Nodes
512 | `
513 |
514 | class XFoo extends Tonic {
515 | render () {
516 | return this.html`
`
517 | }
518 | }
519 |
520 | class XBar extends Tonic {
521 | render () {
522 | return this.html`
523 |
524 |
525 |
526 |
527 | `
528 | }
529 | }
530 |
531 | Tonic.add(XFoo)
532 | Tonic.add(XBar)
533 |
534 | t.equal(document.body.querySelectorAll('.bar').length, 2, 'two bar divs')
535 | t.equal(document.body.querySelectorAll('.foo').length, 4, 'four foo divs')
536 | })
537 |
538 | test('sync lifecycle events', async t => {
539 | document.body.innerHTML = ' '
540 | let calledBazzCtor
541 | let disconnectedBazz
542 | let calledQuxxCtor
543 |
544 | class XBazz extends Tonic {
545 | constructor (p) {
546 | super(p)
547 | calledBazzCtor = true
548 | }
549 |
550 | disconnected () {
551 | disconnectedBazz = true
552 | }
553 |
554 | render () {
555 | return this.html`
`
556 | }
557 | }
558 |
559 | class XQuxx extends Tonic {
560 | constructor (p) {
561 | super(p)
562 | calledQuxxCtor = true
563 | }
564 |
565 | willConnect () {
566 | const expectedRE = /<\/x-quxx>/
567 | t.ok(true, 'willConnect event fired')
568 | t.ok(expectedRE.test(document.body.innerHTML), 'nothing added yet')
569 | }
570 |
571 | connected () {
572 | t.ok(true, 'connected event fired')
573 | const expectedRE = /<\/div><\/x-bazz><\/div><\/x-quxx>/
574 | t.ok(expectedRE.test(document.body.innerHTML), 'rendered')
575 | }
576 |
577 | render () {
578 | t.ok(true, 'render event fired')
579 | return this.html`
`
580 | }
581 | }
582 |
583 | Tonic.add(XBazz)
584 | Tonic.add(XQuxx)
585 | const q = document.querySelector('x-quxx')
586 | q.reRender({})
587 | const refsLength = Tonic._refIds.length
588 |
589 | // once again to overwrite the old instances
590 | q.reRender({})
591 | t.equal(Tonic._refIds.length, refsLength, 'Cleanup, refs correct count')
592 |
593 | // once again to check that the refs length is the same
594 | q.reRender({})
595 | t.equal(Tonic._refIds.length, refsLength, 'Cleanup, refs still correct count')
596 |
597 | await sleep(0)
598 |
599 | t.ok(calledBazzCtor, 'calling bazz ctor')
600 | t.ok(calledQuxxCtor, 'calling quxx ctor')
601 | t.ok(disconnectedBazz, 'disconnected event fired')
602 | })
603 |
604 | test('async lifecycle events', async t => {
605 | let bar
606 | document.body.innerHTML = '
'
607 |
608 | class AsyncF extends Tonic {
609 | connected () {
610 | bar = this.querySelector('.bar')
611 | }
612 |
613 | async render () {
614 | return this.html`
`
615 | }
616 | }
617 |
618 | Tonic.add(AsyncF)
619 |
620 | await sleep(10)
621 | t.ok(bar, 'body was ready')
622 | })
623 |
624 | test('async-generator lifecycle events', async t => {
625 | let bar
626 | document.body.innerHTML = '
'
627 |
628 | class AsyncG extends Tonic {
629 | connected () {
630 | bar = this.querySelector('.bar')
631 | }
632 |
633 | async * render () {
634 | yield 'loading...'
635 | yield 'something else....'
636 | return this.html`
`
637 | }
638 | }
639 |
640 | Tonic.add(AsyncG)
641 |
642 | await sleep(10)
643 | t.ok(bar, 'body was ready')
644 | })
645 |
646 | test('compose sugar (this.children)', async t => {
647 | class ComponentG extends Tonic {
648 | render () {
649 | return this.html`
${this.children}
`
650 | }
651 | }
652 |
653 | class ComponentH extends Tonic {
654 | render () {
655 | return this.html`
${this.props.value}
`
656 | }
657 | }
658 |
659 | document.body.innerHTML = `
660 |
661 |
662 |
663 | `
664 |
665 | Tonic.add(ComponentG)
666 | Tonic.add(ComponentH)
667 |
668 | const g = document.querySelector('component-g')
669 | const children = g.querySelectorAll('.child')
670 | t.equal(children.length, 1, 'child element was added')
671 | t.equal(children[0].innerHTML, 'x')
672 |
673 | const h = document.querySelector('component-h')
674 |
675 | h.reRender({
676 | value: 'y'
677 | })
678 |
679 | await sleep(1)
680 | const childrenAfterSetProps = g.querySelectorAll('.child')
681 | t.equal(childrenAfterSetProps.length, 1, 'child element was replaced')
682 | t.equal(childrenAfterSetProps[0].innerHTML, 'y')
683 | })
684 |
685 | test('ensure registration order does not affect rendering', async t => {
686 | class ComposeA extends Tonic {
687 | render () {
688 | return this.html`
689 |
690 | ${this.children}
691 |
692 | `
693 | }
694 | }
695 |
696 | class ComposeB extends Tonic {
697 | render () {
698 | return this.html`
699 |
700 | ${this.childNodes}
701 |
702 | `
703 | }
704 | }
705 |
706 | document.body.innerHTML = `
707 |
708 |
709 | 1
710 | 2
711 | 3
712 |
713 |
714 | `
715 |
716 | Tonic.add(ComposeB)
717 | Tonic.add(ComposeA)
718 |
719 | const select = document.querySelectorAll('.a select')
720 | t.equal(select.length, 1, 'there is only one select')
721 | t.equal(select[0].children.length, 3, 'there are 3 options')
722 | })
723 |
724 | test('check that composed elements use (and re-use) their initial innerHTML correctly', async t => {
725 | class ComponentI extends Tonic {
726 | render () {
727 | return this.html`
728 |
729 |
730 |
731 |
732 |
`
733 | }
734 | }
735 |
736 | class ComponentJ extends Tonic {
737 | render () {
738 | return this.html`
${this.children}
`
739 | }
740 | }
741 |
742 | class ComponentK extends Tonic {
743 | render () {
744 | return this.html`
${this.props.value}
`
745 | }
746 | }
747 |
748 | document.body.innerHTML = `
749 |
750 |
751 | `
752 |
753 | Tonic.add(ComponentJ)
754 | Tonic.add(ComponentK)
755 | Tonic.add(ComponentI)
756 |
757 | t.comment('Uses init() instead of
')
758 |
759 | const i = document.querySelector('component-i')
760 | const kTags = i.getElementsByTagName('component-k')
761 | t.equal(kTags.length, 1)
762 |
763 | const kClasses = i.querySelectorAll('.k')
764 | t.equal(kClasses.length, 1)
765 |
766 | const kText = kClasses[0].textContent
767 | t.equal(kText, 'x', 'The text of the inner-most child was rendered correctly')
768 |
769 | i.reRender({
770 | value: 1
771 | })
772 |
773 | await sleep(1)
774 | const kTagsAfterSetProps = i.getElementsByTagName('component-k')
775 | t.equal(kTagsAfterSetProps.length, 1, 'correct number of components rendered')
776 |
777 | const kClassesAfterSetProps = i.querySelectorAll('.k')
778 | t.equal(kClassesAfterSetProps.length, 1, 'correct number of elements rendered')
779 | const kTextAfterSetProps = kClassesAfterSetProps[0].textContent
780 | t.equal(kTextAfterSetProps, '1', 'The text of the inner-most child was rendered correctly')
781 | })
782 |
783 | test('mixed order declaration', async t => {
784 | class AppXx extends Tonic {
785 | render () {
786 | return this.html`${this.children}
`
787 | }
788 | }
789 |
790 | class ComponentAx extends Tonic {
791 | render () {
792 | return this.html`A
`
793 | }
794 | }
795 |
796 | class ComponentBx extends Tonic {
797 | render () {
798 | return this.html`${this.children}
`
799 | }
800 | }
801 |
802 | class ComponentCx extends Tonic {
803 | render () {
804 | return this.html`${this.children}
`
805 | }
806 | }
807 |
808 | class ComponentDx extends Tonic {
809 | render () {
810 | return this.html`D
`
811 | }
812 | }
813 |
814 | document.body.innerHTML = `
815 |
816 |
817 |
818 |
819 |
820 |
821 |
822 |
823 |
824 |
825 |
826 | `
827 |
828 | Tonic.add(ComponentDx)
829 | Tonic.add(ComponentAx)
830 | Tonic.add(ComponentCx)
831 | Tonic.add(AppXx)
832 | Tonic.add(ComponentBx)
833 |
834 | {
835 | const div = document.querySelector('.app')
836 | t.ok(div, 'a div was created and attached')
837 | }
838 |
839 | {
840 | const div = document.querySelector('body .app .a')
841 | t.ok(div, 'a div was created and attached')
842 | }
843 |
844 | {
845 | const div = document.querySelector('body .app .b')
846 | t.ok(div, 'a div was created and attached')
847 | }
848 |
849 | {
850 | const div = document.querySelector('body .app .b .c')
851 | t.ok(div, 'a div was created and attached')
852 | }
853 |
854 | {
855 | const div = document.querySelector('body .app .b .c .d')
856 | t.ok(div, 'a div was created and attached')
857 | }
858 | })
859 |
860 | test('spread props', async t => {
861 | class SpreadComponent extends Tonic {
862 | render () {
863 | return this.html`
864 |
865 | `
866 | }
867 | }
868 |
869 | class AppContainer extends Tonic {
870 | render () {
871 | const o = {
872 | a: 'testing',
873 | b: 2.2,
874 | FooBar: '"ok"'
875 | }
876 |
877 | const el = document.querySelector('#el').attributes
878 |
879 | return this.html`
880 |
881 |
882 |
883 |
884 |
885 |
886 |
887 | `
888 | }
889 | }
890 |
891 | document.body.innerHTML = `
892 |
893 |
894 | `
895 |
896 | Tonic.add(AppContainer)
897 | Tonic.add(SpreadComponent)
898 |
899 | const component = document.querySelector('spread-component')
900 | t.equal(component.getAttribute('a'), 'testing')
901 | t.equal(component.getAttribute('b'), '2.2')
902 | t.equal(component.getAttribute('foo-bar'), '"ok"')
903 | const div = document.querySelector('div:first-of-type')
904 | const span = document.querySelector('span:first-of-type')
905 | t.equal(div.attributes.length, 3, 'div also got expanded attributes')
906 | t.equal(span.attributes.length, 4, 'span got all attributes from div#el')
907 | })
908 |
909 | test('async render', async t => {
910 | class AsyncRender extends Tonic {
911 | async getSomeData () {
912 | await sleep(100)
913 | return 'Some Data'
914 | }
915 |
916 | async render () {
917 | const value = await this.getSomeData()
918 | return this.html`
919 | ${value}
920 | `
921 | }
922 | }
923 |
924 | Tonic.add(AsyncRender)
925 |
926 | document.body.innerHTML = `
927 |
928 | `
929 |
930 | let ar = document.body.querySelector('async-render')
931 | t.equal(ar.innerHTML, '')
932 |
933 | await sleep(200)
934 |
935 | ar = document.body.querySelector('async-render')
936 | t.equal(ar.innerHTML.trim(), 'Some Data
')
937 | })
938 |
939 | test('async generator render', async t => {
940 | class AsyncGeneratorRender extends Tonic {
941 | async * render () {
942 | yield 'X'
943 |
944 | await sleep(100)
945 |
946 | return 'Y'
947 | }
948 | }
949 |
950 | Tonic.add(AsyncGeneratorRender)
951 |
952 | document.body.innerHTML = `
953 |
954 |
955 | `
956 |
957 | await sleep(10)
958 |
959 | let ar = document.body.querySelector('async-generator-render')
960 | t.equal(ar.innerHTML, 'X')
961 |
962 | await sleep(200)
963 |
964 | ar = document.body.querySelector('async-generator-render')
965 | t.equal(ar.innerHTML, 'Y')
966 | })
967 |
968 | test('pass in references to children', async t => {
969 | const cName = `x-${uuid()}`
970 | const dName = `x-${uuid()}`
971 |
972 | class DividerComponent extends Tonic {
973 | willConnect () {
974 | this.left = this.querySelector('.left')
975 | this.right = this.querySelector('.right')
976 | }
977 |
978 | render () {
979 | return this.html`
980 | ${this.left}
981 | ${this.right}
982 | `
983 | }
984 | }
985 | Tonic.add(DividerComponent, cName)
986 |
987 | class TextComp extends Tonic {
988 | render () {
989 | return this.html`${this.props.text} `
990 | }
991 | }
992 | Tonic.add(TextComp, dName)
993 |
994 | document.body.innerHTML = `
995 | <${cName}>
996 | left
997 | <${dName} class="right" text="right">${dName}>
998 | ${cName}>
999 | `
1000 |
1001 | const pElem = document.querySelector(cName)
1002 |
1003 | const first = pElem.children[0]
1004 | t.ok(first)
1005 | t.equal(first.tagName, 'DIV')
1006 | t.equal(first.className, 'left')
1007 | t.equal(first.innerHTML, 'left ')
1008 |
1009 | const second = pElem.children[1]
1010 | t.ok(second)
1011 | t.equal(second.tagName, 'BR')
1012 |
1013 | const third = pElem.children[2]
1014 | t.ok(third)
1015 | t.equal(third.tagName, dName.toUpperCase())
1016 | t.equal(third.className, 'right')
1017 | t.equal(third.innerHTML, 'right ')
1018 | })
1019 |
1020 | test('pass comp as ref in props', async t => {
1021 | const pName = `x-${uuid()}`
1022 | const cName = `x-${uuid()}`
1023 |
1024 | class ParentComponent extends Tonic {
1025 | constructor (o) {
1026 | super(o)
1027 |
1028 | this.name = 'hello'
1029 | }
1030 |
1031 | render () {
1032 | return this.html`
1033 |
1034 | <${cName} ref=${this}>${cName}>
1035 |
1036 | `
1037 | }
1038 | }
1039 |
1040 | class ChildComponent extends Tonic {
1041 | render () {
1042 | return this.html`
1043 | ${this.props.ref.name}
1044 | `
1045 | }
1046 | }
1047 |
1048 | Tonic.add(ParentComponent, pName)
1049 | Tonic.add(ChildComponent, cName)
1050 |
1051 | document.body.innerHTML = `<${pName}>${pName}`
1052 |
1053 | const pElem = document.querySelector(pName)
1054 | t.ok(pElem)
1055 |
1056 | const cElem = pElem.querySelector(cName)
1057 | t.ok(cElem)
1058 |
1059 | t.equal(cElem.innerHTML.trim(), 'hello
')
1060 | })
1061 |
1062 | test('default props', async t => {
1063 | class InstanceProps extends Tonic {
1064 | constructor () {
1065 | super()
1066 | this.props = { num: 100 }
1067 | }
1068 |
1069 | render () {
1070 | return this.html`${JSON.stringify(this.props)}
`
1071 | }
1072 | }
1073 |
1074 | Tonic.add(InstanceProps)
1075 |
1076 | document.body.innerHTML = `
1077 |
1078 |
1079 | `
1080 |
1081 | const actual = document.body.innerHTML.trim()
1082 |
1083 | const expectedRE = /{"num":100,"str":"0x"}<\/div><\/instance-props>/
1084 |
1085 | t.ok(expectedRE.test(actual), 'elements match')
1086 | })
1087 |
1088 | test('Tonic comp with null prop', async t => {
1089 | class InnerComp extends Tonic {
1090 | render () {
1091 | return this.html`
${String(this.props.foo)}
`
1092 | }
1093 | }
1094 | const innerName = `x-${uuid()}`
1095 | Tonic.add(InnerComp, innerName)
1096 |
1097 | class OuterComp extends Tonic {
1098 | render () {
1099 | return this.html`<${innerName} foo=${null}>${innerName}>`
1100 | }
1101 | }
1102 | const outerName = `x-${uuid()}`
1103 | Tonic.add(OuterComp, outerName)
1104 |
1105 | document.body.innerHTML = `<${outerName}>${outerName}>`
1106 |
1107 | const div = document.body.querySelector('div')
1108 | t.ok(div)
1109 |
1110 | t.equal(div.textContent, 'null')
1111 | })
1112 |
1113 | test('re-render nested component', async t => {
1114 | const pName = `x-${uuid()}`
1115 | const cName = `x-${uuid()}`
1116 | class ParentComponent extends Tonic {
1117 | render () {
1118 | const message = this.props.message
1119 | return this.html`
1120 |
1121 | <${cName} id="persist" message="${message}">${cName}>
1122 |
1123 | `
1124 | }
1125 | }
1126 |
1127 | class ChildStateComponent extends Tonic {
1128 | updateText (newText) {
1129 | this.state.text = newText
1130 | this.reRender()
1131 | }
1132 |
1133 | render () {
1134 | const message = this.props.message
1135 | const text = this.state.text || ''
1136 |
1137 | return this.html`
1138 |
1139 | ${message}
1140 |
1141 |
1142 | `
1143 | }
1144 | }
1145 |
1146 | Tonic.add(ParentComponent, pName)
1147 | Tonic.add(ChildStateComponent, cName)
1148 |
1149 | document.body.innerHTML = `
1150 | <${pName} message="initial">${pName}>
1151 | `
1152 |
1153 | const pElem = document.querySelector(pName)
1154 | t.ok(pElem)
1155 |
1156 | const label = pElem.querySelector('label')
1157 | t.equal(label.textContent, 'initial')
1158 |
1159 | const input = pElem.querySelector('input')
1160 | t.equal(input.value, '')
1161 |
1162 | const cElem = pElem.querySelector(cName)
1163 | cElem.updateText('new text')
1164 |
1165 | async function onUpdate () {
1166 | const label = pElem.querySelector('label')
1167 | t.equal(label.textContent, 'initial')
1168 |
1169 | const input = pElem.querySelector('input')
1170 | t.equal(input.value, 'new text')
1171 |
1172 | pElem.reRender({
1173 | message: 'new message'
1174 | })
1175 | }
1176 |
1177 | function onReRender () {
1178 | const label = pElem.querySelector('label')
1179 | t.equal(label.textContent, 'new message')
1180 |
1181 | const input = pElem.querySelector('input')
1182 | t.equal(input.value, 'new text')
1183 | }
1184 |
1185 | await sleep(1)
1186 | await onUpdate()
1187 | await sleep(1)
1188 | await onReRender()
1189 | })
1190 |
1191 | test('async rendering component', async t => {
1192 | const cName = `x-${uuid()}`
1193 | class AsyncComponent extends Tonic {
1194 | async render () {
1195 | await sleep(100)
1196 |
1197 | return this.html`
${this.props.text}
`
1198 | }
1199 | }
1200 | Tonic.add(AsyncComponent, cName)
1201 | document.body.innerHTML = `<${cName}>${cName}>`
1202 |
1203 | const cElem = document.querySelector(cName)
1204 | t.ok(cElem)
1205 | t.equal(cElem.textContent, '')
1206 |
1207 | cElem.reRender({ text: 'new text' })
1208 | t.equal(cElem.textContent, '')
1209 |
1210 | await cElem.reRender({ text: 'new text2' })
1211 | t.equal(cElem.textContent, 'new text2')
1212 | })
1213 |
1214 | test('alternating component', async t => {
1215 | const cName = `x-${uuid()}`
1216 | const pName = `x-${uuid()}`
1217 |
1218 | class ParentComponent extends Tonic {
1219 | render () {
1220 | return this.html`
1221 | <${cName} id="alternating">
1222 |
Child Text
1223 |
Span Text
1224 | Raw Text Node
1225 | ${cName}>
1226 | `
1227 | }
1228 | }
1229 | Tonic.add(ParentComponent, pName)
1230 |
1231 | class AlternatingComponent extends Tonic {
1232 | constructor () {
1233 | super()
1234 |
1235 | this.state = {
1236 | renderCount: 0,
1237 | ...this.state
1238 | }
1239 | }
1240 |
1241 | render () {
1242 | this.state.renderCount++
1243 | if (this.state.renderCount % 2) {
1244 | return this.html`
1245 |
New content
1246 | `
1247 | } else {
1248 | return this.html`${this.nodes}`
1249 | }
1250 | }
1251 | }
1252 | Tonic.add(AlternatingComponent, cName)
1253 |
1254 | document.body.innerHTML = `<${pName}>${pName}>`
1255 |
1256 | const pElem = document.querySelector(pName)
1257 | t.ok(pElem)
1258 |
1259 | let cElem = document.querySelector(cName)
1260 | t.ok(cElem)
1261 |
1262 | t.equal(cElem.children.length, 1)
1263 | t.equal(cElem.children[0].textContent, 'New content')
1264 |
1265 | await pElem.reRender()
1266 |
1267 | cElem = document.querySelector(cName)
1268 | t.ok(cElem)
1269 |
1270 | t.equal(cElem.children.length, 2)
1271 | t.equal(cElem.children[0].textContent, 'Child Text')
1272 | t.equal(cElem.children[1].textContent, 'Span Text')
1273 |
1274 | t.equal(cElem.childNodes.length, 5)
1275 | t.equal(cElem.childNodes[4].data.trim(), 'Raw Text Node')
1276 |
1277 | await pElem.reRender()
1278 |
1279 | cElem = document.querySelector(cName)
1280 | t.equal(cElem.children.length, 1)
1281 | t.equal(cElem.children[0].textContent, 'New content')
1282 |
1283 | await pElem.reRender()
1284 |
1285 | cElem = document.querySelector(cName)
1286 | t.equal(cElem.children.length, 2)
1287 | t.equal(cElem.children[0].textContent, 'Child Text')
1288 | t.equal(cElem.children[1].textContent, 'Span Text')
1289 |
1290 | const child1Ref = cElem.children[0]
1291 | const child2Ref = cElem.children[1]
1292 |
1293 | await cElem.reRender()
1294 | t.equal(cElem.textContent.trim(), 'New content')
1295 | await cElem.reRender()
1296 | t.equal(
1297 | cElem.textContent.trim().replace(/\s+/g, ' '),
1298 | 'Child Text Span Text Raw Text Node'
1299 | )
1300 |
1301 | t.equal(cElem.children[0], child1Ref)
1302 | t.equal(cElem.children[1], child2Ref)
1303 | })
1304 |
1305 | test('cleanup, ensure exist', async t => {
1306 | document.body.classList.add('finished')
1307 | })
1308 |
--------------------------------------------------------------------------------
/test/perf/perf.js:
--------------------------------------------------------------------------------
1 | import Benchmark from 'benchmark'
2 | import Tonic from '../../dist/tonic.min.js'
3 |
4 | window.Benchmark = Benchmark
5 | const suite = new Benchmark.Suite()
6 |
7 | class XHello extends Tonic {
8 | render () {
9 | return `
${this.props.message} `
10 | }
11 | }
12 |
13 | class XApp extends Tonic {
14 | render () {
15 | return this.html`
16 |
17 |
18 | `
19 | }
20 | }
21 |
22 | document.body.innerHTML = `
23 |
24 |
25 | `
26 |
27 | Tonic.add(XHello)
28 | Tonic.add(XApp)
29 |
30 | document.addEventListener('DOMContentLoaded', () => {
31 | const app = document.querySelector('x-app')
32 | const hello = document.querySelector('x-hello')
33 |
34 | suite
35 | .add('re-render a single component', () => {
36 | hello.reRender({ message: Math.random() })
37 | })
38 | .add('re-render a hierarchy component', () => {
39 | app.reRender()
40 | })
41 | .on('cycle', (event) => {
42 | console.log(String(event.target))
43 | })
44 | .on('complete', function () {
45 | console.log('done')
46 | })
47 | .run()
48 | })
49 |
--------------------------------------------------------------------------------