@knowark/componarkjs 1.14.0 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +57 -45
  2. package/lib/base/component/component.js +127 -21
  3. package/lib/base/component/component.test.js +296 -3
  4. package/lib/base/component/index.js +3 -0
  5. package/lib/base/styles/index.js +4 -1
  6. package/lib/base/utils/define.js +2 -1
  7. package/lib/base/utils/format.js +12 -6
  8. package/lib/base/utils/helpers.js +31 -5
  9. package/lib/base/utils/index.js +1 -0
  10. package/lib/base/utils/slots.js +3 -2
  11. package/lib/base/utils/uuid.js +1 -1
  12. package/lib/components/audio/components/audio.js +17 -2
  13. package/lib/components/audio/index.js +1 -0
  14. package/lib/components/audio/styles/index.js +5 -1
  15. package/lib/components/camera/components/camera.js +10 -0
  16. package/lib/components/camera/index.js +1 -0
  17. package/lib/components/camera/styles/index.js +5 -1
  18. package/lib/components/capture/components/capture.js +18 -2
  19. package/lib/components/capture/index.js +1 -0
  20. package/lib/components/droparea/components/droparea-preview.js +58 -13
  21. package/lib/components/droparea/components/droparea-preview.test.js +82 -0
  22. package/lib/components/droparea/components/droparea.js +41 -2
  23. package/lib/components/droparea/index.js +1 -0
  24. package/lib/components/droparea/styles/index.js +5 -1
  25. package/lib/components/emit/components/emit.js +11 -1
  26. package/lib/components/emit/index.js +1 -0
  27. package/lib/components/index.js +2 -1
  28. package/lib/components/list/components/item.js +6 -0
  29. package/lib/components/list/components/list.js +18 -4
  30. package/lib/components/list/index.js +1 -0
  31. package/lib/components/paginator/components/paginator.js +34 -8
  32. package/lib/components/paginator/index.js +1 -0
  33. package/lib/components/paginator/styles/index.js +5 -1
  34. package/lib/components/spinner/components/spinner.js +10 -0
  35. package/lib/components/spinner/index.js +1 -0
  36. package/lib/components/spinner/styles/index.js +5 -1
  37. package/lib/components/splitview/components/splitview.detail.js +10 -1
  38. package/lib/components/splitview/components/splitview.js +18 -3
  39. package/lib/components/splitview/components/splitview.master.js +10 -0
  40. package/lib/components/splitview/index.js +1 -0
  41. package/lib/components/translate/components/translate.js +42 -11
  42. package/lib/components/translate/components/translate.test.js +169 -1
  43. package/lib/components/translate/index.js +1 -0
  44. package/lib/index.js +3 -0
  45. package/package.json +2 -1
  46. package/tsconfig.json +1 -1
  47. package/types/base/component/component.d.ts +43 -8
  48. package/types/base/component/component.d.ts.map +1 -1
  49. package/types/base/component/index.d.ts +4 -6
  50. package/types/base/component/index.d.ts.map +1 -1
  51. package/types/base/styles/index.d.ts +3 -2
  52. package/types/base/styles/index.d.ts.map +1 -1
  53. package/types/base/utils/define.d.ts +3 -2
  54. package/types/base/utils/define.d.ts.map +1 -1
  55. package/types/base/utils/format.d.ts +12 -6
  56. package/types/base/utils/format.d.ts.map +1 -1
  57. package/types/base/utils/helpers.d.ts +27 -7
  58. package/types/base/utils/helpers.d.ts.map +1 -1
  59. package/types/base/utils/slots.d.ts +8 -10
  60. package/types/base/utils/slots.d.ts.map +1 -1
  61. package/types/base/utils/uuid.d.ts +1 -1
  62. package/types/base/utils/uuid.d.ts.map +1 -1
  63. package/types/components/audio/components/audio.d.ts +23 -9
  64. package/types/components/audio/components/audio.d.ts.map +1 -1
  65. package/types/components/audio/styles/index.d.ts +3 -2
  66. package/types/components/audio/styles/index.d.ts.map +1 -1
  67. package/types/components/camera/components/camera.d.ts +11 -3
  68. package/types/components/camera/components/camera.d.ts.map +1 -1
  69. package/types/components/camera/styles/index.d.ts +3 -2
  70. package/types/components/camera/styles/index.d.ts.map +1 -1
  71. package/types/components/capture/components/capture.d.ts +23 -3
  72. package/types/components/capture/components/capture.d.ts.map +1 -1
  73. package/types/components/droparea/components/droparea-preview.d.ts +64 -11
  74. package/types/components/droparea/components/droparea-preview.d.ts.map +1 -1
  75. package/types/components/droparea/components/droparea.d.ts +58 -13
  76. package/types/components/droparea/components/droparea.d.ts.map +1 -1
  77. package/types/components/droparea/styles/index.d.ts +3 -2
  78. package/types/components/droparea/styles/index.d.ts.map +1 -1
  79. package/types/components/emit/components/emit.d.ts +15 -3
  80. package/types/components/emit/components/emit.d.ts.map +1 -1
  81. package/types/components/list/components/item.d.ts +8 -1
  82. package/types/components/list/components/item.d.ts.map +1 -1
  83. package/types/components/list/components/list.d.ts +23 -5
  84. package/types/components/list/components/list.d.ts.map +1 -1
  85. package/types/components/paginator/components/paginator.d.ts +32 -8
  86. package/types/components/paginator/components/paginator.d.ts.map +1 -1
  87. package/types/components/paginator/styles/index.d.ts +3 -2
  88. package/types/components/paginator/styles/index.d.ts.map +1 -1
  89. package/types/components/spinner/components/spinner.d.ts +14 -3
  90. package/types/components/spinner/components/spinner.d.ts.map +1 -1
  91. package/types/components/spinner/styles/index.d.ts +3 -2
  92. package/types/components/spinner/styles/index.d.ts.map +1 -1
  93. package/types/components/splitview/components/splitview.d.ts +22 -4
  94. package/types/components/splitview/components/splitview.d.ts.map +1 -1
  95. package/types/components/splitview/components/splitview.detail.d.ts +12 -2
  96. package/types/components/splitview/components/splitview.detail.d.ts.map +1 -1
  97. package/types/components/splitview/components/splitview.master.d.ts +12 -1
  98. package/types/components/splitview/components/splitview.master.d.ts.map +1 -1
  99. package/types/components/translate/components/translate.d.ts +44 -10
  100. package/types/components/translate/components/translate.d.ts.map +1 -1
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  <p align="center">
2
2
  <a href="https://codecov.io/gh/librark/componark">
3
- <img src="https://codecov.io/gh/librark/componark/graph/badge.svg?token=IWNapsPUch"/>
3
+ <img src="https://codecov.io/gh/librark/componark/graph/badge.svg?token=IWNapsPUch" alt="codecov" />
4
4
  </a>
5
5
  </p>
6
6
  <p align="center">
7
7
  <a href="https://codecov.io/gh/librark/componark">
8
- <img src="https://codecov.io/gh/librark/componark/graphs/sunburst.svg?token=IWNapsPUch"/>
8
+ <img src="https://codecov.io/gh/librark/componark/graphs/sunburst.svg?token=IWNapsPUch" alt="coverage sunburst" />
9
9
  </a>
10
10
  </p>
11
11
 
@@ -13,46 +13,58 @@
13
13
 
14
14
  Pragmatic Web Components Library
15
15
 
16
- Introduction
17
- ============
18
-
19
- Componark is a collection of ready to use web components to propel the creation
20
- of user interfaces. A common *Component* base class is provided to ensure
21
- inter-component compatibility and common development idioms. This class is only
22
- a thin wrapper on top of the *HTMLElement* custom elements class, and might be
23
- extended in your own application code to create new components.
24
-
25
- Reference
26
- =========
27
-
28
- - `Base Component <lib/base/component>`_
29
-
30
- Components library
31
- ------------------
32
-
33
- - `<ark-accordion> <lib/components/accordion>`_
34
- - `<ark-alert> <lib/components/alert>`_
35
- - `<ark-audio> <lib/components/audio>`_
36
- - `<ark-button> <lib/components/button>`_
37
- - `<ark-camera> <lib/components/camera>`_
38
- - `<ark-card> <lib/components/card>`_
39
- - `<ark-chart> <lib/components/chart>`_
40
- - `<ark-checkbox> <lib/components/checkbox>`_
41
- - `<ark-droparea> <lib/components/droparea>`_
42
- - `<ark-gallery> <lib/components/gallery>`_
43
- - `<ark-icon> <lib/components/icon>`_
44
- - `<ark-list> <lib/components/list>`_
45
- - `<ark-input> <lib/components/input>`_
46
- - `<ark-location> <lib/components/location>`_
47
- - `<ark-map> <lib/components/map>`_
48
- - `<ark-modal> <lib/components/modal>`_
49
- - `<ark-multiselect> <lib/components/multiselect>`_
50
- - `<ark-paginator> <lib/components/paginator>`_
51
- - `<ark-radio> <lib/components/radio>`_
52
- - `<ark-select> <lib/components/select>`_
53
- - `<ark-sidebar> <lib/components/sidebar>`_
54
- - `<ark-signature> <lib/components/signature>`_
55
- - `<ark-spinner> <lib/components/spinner>`_
56
- - `<ark-splitview> <lib/components/splitview>`_
57
- - `<ark-tabs> <lib/components/tabs>`_
58
- - `<ark-tooltip> <lib/components/tooltip>`_
16
+ ## Introduction
17
+
18
+ ComponArk is a lightweight Web Components library with a shared base class for
19
+ consistency and event-driven composition.
20
+
21
+ The library is organized as custom elements that can be used directly in HTML or
22
+ combined with your application code.
23
+
24
+ ## Reference
25
+
26
+ - [Base Component](lib/base/component)
27
+ - [Showcase](showcase)
28
+
29
+ ## Components library
30
+
31
+ > Components marked in **bold** are available in this repository.
32
+
33
+ - **`ark-audio`** ([docs](lib/components/audio/README.md))
34
+ - **`ark-camera`** ([docs](lib/components/camera/README.md))
35
+ - **`ark-capture`** ([docs](lib/components/capture/README.md))
36
+ - **`ark-droparea`** ([docs](lib/components/droparea/README.md))
37
+ - **`ark-emit`** ([docs](lib/components/emit/README.md))
38
+ - **`ark-list`** ([docs](lib/components/list/README.md))
39
+ - **`ark-paginator`** ([docs](lib/components/paginator/README.md))
40
+ - **`ark-spinner`** ([docs](lib/components/spinner/README.md))
41
+ - **`ark-splitview`** ([docs](lib/components/splitview/README.md))
42
+ - **`ark-translate`** ([docs](lib/components/translate/README.md))
43
+
44
+ ## Why this exists
45
+
46
+ - Minimal, reusable base (`Component`) with lifecycle hooks and dependency
47
+ resolution.
48
+ - Template/event binding helper (`listen`) with custom attribute syntax.
49
+ - Lightweight styling support with constructor stylesheet + fallback support.
50
+ - Small test surface included with native Node test runner.
51
+
52
+ ## Basic usage
53
+
54
+ ```html
55
+ <ark-translate languages="en,es"></ark-translate>
56
+
57
+ <span data-i18n="hello">Hello</span>
58
+ ```
59
+
60
+ ## Development
61
+
62
+ - Run tests: `npm test`
63
+ - Build production bundle: `npm run prod`
64
+ - Start local dev server: `npm run dev`
65
+
66
+ ## Notes
67
+
68
+ Some older docs in the previous release referenced components that are not in the
69
+ current snapshot of this repository. If you need one of those modules, check
70
+ the release tags or open a request with expected parity.
@@ -2,21 +2,31 @@ import { define, listen, reflect, slot, keys } from '../utils/index.js'
2
2
  import styles from '../styles/index.js'
3
3
 
4
4
  const tag = 'ark-component'
5
+ /**
6
+ * Base composable UI component.
7
+ * @extends {globalThis.HTMLElement}
8
+ */
5
9
  export class Component extends globalThis.HTMLElement {
6
10
  constructor () {
7
11
  super()
8
12
  this.binding = 'listen'
9
13
  this.local = {}
10
14
  this._isConnected = false
15
+ this._cleanupCallbacks = []
16
+ this._needsBinding = true
17
+ this.global = globalThis
11
18
  reflect(this, this.reflectedProperties())
12
19
  }
13
20
 
14
21
  /**
15
- * @param {string} tag
22
+ * Register a custom element and optional CSS for the same tag.
23
+ * @param {string} tagName
16
24
  * @param {CustomElementConstructor} element
17
- * @param {string} styles **/
18
- static define (tag, element, styles = null) {
19
- define(tag, element, styles)
25
+ * @param {string} [styles]
26
+ * @returns {void}
27
+ */
28
+ static define (tagName, element, styles = null) {
29
+ define(tagName, element, styles)
20
30
  }
21
31
 
22
32
  /**
@@ -28,7 +38,7 @@ export class Component extends globalThis.HTMLElement {
28
38
 
29
39
  /**
30
40
  * @param {object} context
31
- * @return {Component} */
41
+ * @return {this} */
32
42
  init (context = {}) {
33
43
  return this
34
44
  }
@@ -45,6 +55,7 @@ export class Component extends globalThis.HTMLElement {
45
55
  /** @param {string} content */
46
56
  set content (content) {
47
57
  this.innerHTML = content
58
+ this._needsBinding = true
48
59
  }
49
60
 
50
61
  /** @return {string} */
@@ -52,41 +63,62 @@ export class Component extends globalThis.HTMLElement {
52
63
  return this.innerHTML
53
64
  }
54
65
 
66
+ /** @returns {void} */
55
67
  connectedCallback () {
56
68
  this._isConnected = true
57
69
  try {
58
70
  !Boolean(Object.keys(this.local).length) && this.init({})
59
71
  this.render()
60
72
  } catch (error) {
61
- this.emit('error', error)
73
+ this.emit('error', this._enhanceError(error, 'init-render'))
62
74
  throw error
63
75
  }
64
76
  try {
65
77
  const load = this.load({})
66
78
  if (load && typeof load.catch === 'function') {
67
79
  load.catch(error => {
68
- this.emit('error', error)
80
+ this.emit('error', this._enhanceError(error, 'load'))
69
81
  })
70
82
  }
71
83
  } catch (error) {
72
- this.emit('error', error)
84
+ this.emit('error', this._enhanceError(error, 'load'))
73
85
  throw error
74
86
  }
75
87
  }
76
88
 
89
+ /** @returns {void} */
77
90
  disconnectedCallback () {
78
91
  this._isConnected = false
92
+ this._cleanup()
93
+ }
94
+
95
+ /**
96
+ * @param {Function} callback
97
+ * @return {Function} */
98
+ registerCleanup (callback) {
99
+ if (!callback || typeof callback !== 'function') return () => {}
100
+
101
+ this._cleanupCallbacks.push(callback)
102
+ return () => {
103
+ const index = this._cleanupCallbacks.indexOf(callback)
104
+ if (index === -1) return
105
+ this._cleanupCallbacks.splice(index, 1)
106
+ }
79
107
  }
80
108
 
81
- /** @return {Component} */
109
+ /** @return {this} */
82
110
  render () {
83
111
  this.classList.add(this.tagName.toLowerCase())
84
- listen(this)
112
+ if (this._needsBinding) {
113
+ listen(this)
114
+ this._needsBinding = false
115
+ }
85
116
  return this
86
117
  }
87
118
 
88
- /** @param {object} context */
89
- async load (context = {}) {}
119
+ /** @param {object} context
120
+ * @returns {void | Promise<void>} */
121
+ load (context = {}) {}
90
122
 
91
123
  /**
92
124
  * @param {string} selectors
@@ -108,26 +140,100 @@ export class Component extends globalThis.HTMLElement {
108
140
  * @param {string} type
109
141
  * @param {any} detail */
110
142
  emit (type, detail) {
111
- this.dispatchEvent(
112
- new globalThis.CustomEvent(type, {
113
- detail,
114
- bubbles: true,
115
- cancelable: true
116
- })
117
- )
143
+ this.dispatchEvent(this._createEvent(type, detail, {
144
+ bubbles: true,
145
+ cancelable: true
146
+ }))
118
147
  }
119
148
 
120
149
  /**
121
150
  * @param {string} resource
122
151
  * @return {any} */
123
152
  resolve (resource) {
124
- const event = new globalThis.CustomEvent('resolve', {
125
- detail: { resource },
153
+ const event = this._createEvent('resolve', { resource }, {
126
154
  bubbles: true,
127
155
  cancelable: true
128
156
  })
129
157
  this.dispatchEvent(event)
130
158
  return event.detail[resource]
131
159
  }
160
+
161
+ /**
162
+ * @param {any} detail
163
+ * @param {string} phase
164
+ * @return {Error} */
165
+ _enhanceError (detail, phase) {
166
+ if (!detail) return detail
167
+
168
+ const error = detail instanceof Error ? detail : new Error(
169
+ `${detail.message || detail}`
170
+ )
171
+ /** @type {Error & { phase?: string, component?: string }} */
172
+ const enhancedError = error
173
+ enhancedError.phase = phase
174
+ enhancedError.component = this.tagName
175
+ return error
176
+ }
177
+
178
+ /** @returns {void} */
179
+ _cleanup () {
180
+ const callbacks = [...this._cleanupCallbacks]
181
+ this._cleanupCallbacks = []
182
+
183
+ for (const callback of callbacks) {
184
+ try {
185
+ callback()
186
+ } catch (error) {
187
+ this.emit('error', this._enhanceError(error, 'cleanup'))
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Creates an event object for dispatch.
194
+ * @param {string} type
195
+ * @param {any} detail
196
+ * @param {{ bubbles?: boolean, cancelable?: boolean }} [options]
197
+ * @returns {CustomEvent}
198
+ */
199
+ _createEvent (type, detail, options = {}) {
200
+ /** @type {{ [key: string]: any }} */
201
+ const contextGlobal = this.global || {}
202
+ const contextWindow = contextGlobal.document?.defaultView || contextGlobal.window || contextGlobal
203
+ const { bubbles = true, cancelable = true } = options
204
+
205
+ if (typeof contextWindow.CustomEvent === 'function') {
206
+ return new contextWindow.CustomEvent(type, { detail, bubbles, cancelable })
207
+ }
208
+
209
+ if (typeof contextWindow.Event === 'function') {
210
+ const event = new contextWindow.Event(type, { bubbles, cancelable })
211
+ ;(/** @type {any} */ (event)).detail = detail
212
+ return /** @type {CustomEvent} */ (event)
213
+ }
214
+
215
+ if (typeof globalThis.CustomEvent === 'function') {
216
+ return new globalThis.CustomEvent(type, { detail, bubbles, cancelable })
217
+ }
218
+
219
+ if (typeof globalThis.Event === 'function') {
220
+ const event = new globalThis.Event(type, { bubbles, cancelable })
221
+ ;(/** @type {any} */ (event)).detail = detail
222
+ return /** @type {CustomEvent} */ (event)
223
+ }
224
+
225
+ return /** @type {CustomEvent} */ (/** @type {unknown} */ ({
226
+ type,
227
+ detail,
228
+ bubbles,
229
+ cancelable,
230
+ currentTarget: this,
231
+ defaultPrevented: false,
232
+ cancelBubble: false,
233
+ target: this,
234
+ stopPropagation () {},
235
+ preventDefault () {}
236
+ }))
237
+ }
132
238
  }
133
239
  Component.define(tag, Component, styles)
@@ -64,6 +64,44 @@ class PlainLoadComponent extends Component {
64
64
  }
65
65
  Component.define('plain-load-component', PlainLoadComponent)
66
66
 
67
+ class StringLoadComponent extends Component {
68
+ load () {
69
+ throw 'String Load Error!'
70
+ }
71
+ }
72
+ Component.define('string-load-component', StringLoadComponent)
73
+
74
+ class CleanupComponent extends Component {
75
+ init (context = {}) {
76
+ this.cleanupCalls = 0
77
+ return super.init(context)
78
+ }
79
+
80
+ render () {
81
+ return super.render()
82
+ }
83
+ }
84
+ Component.define('cleanup-component', CleanupComponent)
85
+
86
+ class OptimizedListenerComponent extends Component {
87
+ init (context = {}) {
88
+ this.clicks = 0
89
+ return super.init(context)
90
+ }
91
+
92
+ render () {
93
+ if (!this.querySelector('button')) {
94
+ this.content = '<button listen on-click="onClick">Click me</button>'
95
+ }
96
+ return super.render()
97
+ }
98
+
99
+ onClick () {
100
+ this.clicks += 1
101
+ }
102
+ }
103
+ Component.define('optimized-listener-component', OptimizedListenerComponent)
104
+
67
105
  let container = null
68
106
  let component = null
69
107
 
@@ -166,6 +204,10 @@ it('catches and re-raises connectedCallback errors', async () => {
166
204
  const component = /** @type {Component} */ (
167
205
  document.createElement('mock-component'))
168
206
  const consoleErrorMock = mock.method(console, 'error', () => {})
207
+ let errorEvent = null
208
+ component.addEventListener('error', (event) => {
209
+ errorEvent = event
210
+ })
169
211
  component.render = () => {
170
212
  throw new Error('Render Error!')
171
213
  }
@@ -174,14 +216,72 @@ it('catches and re-raises connectedCallback errors', async () => {
174
216
  component.connectedCallback()
175
217
  } catch (error) {
176
218
  assert.deepStrictEqual(error.message, 'Render Error!')
219
+ assert.deepStrictEqual(errorEvent.detail.phase, 'init-render')
220
+ assert.deepStrictEqual(errorEvent.detail.component, 'MOCK-COMPONENT')
177
221
  } finally {
178
222
  consoleErrorMock.mock.restore()
179
223
  }
180
224
  })
181
225
 
182
- it('emits an error event when async load fails', async () => {
183
- setup()
184
- const component = document.createElement('async-load-component')
226
+ it('creates events without CustomEvent and keeps event details', () => {
227
+ setup()
228
+ const component = container.querySelector('mock-component')
229
+ const previousCustomEvent = globalThis.CustomEvent
230
+
231
+ try {
232
+ globalThis.CustomEvent = undefined
233
+ let detail = null
234
+ component.addEventListener('fallback-emit', (event) => {
235
+ detail = event.detail
236
+ })
237
+
238
+ component.emit('fallback-emit', { ready: true })
239
+
240
+ assert.deepStrictEqual(detail, { ready: true })
241
+ } finally {
242
+ globalThis.CustomEvent = previousCustomEvent
243
+ }
244
+ })
245
+
246
+ it('creates events from the component global window Event fallback', () => {
247
+ setup()
248
+ const component = document.createElement('mock-component')
249
+ const previousGlobal = component.global
250
+
251
+ try {
252
+ component.global = {
253
+ document: {
254
+ defaultView: {
255
+ Event: globalThis.Event
256
+ }
257
+ }
258
+ }
259
+
260
+ const event = component._createEvent('context-window-event', { ready: true })
261
+
262
+ assert.deepStrictEqual(event.type, 'context-window-event')
263
+ assert.deepStrictEqual(event.detail.ready, true)
264
+ assert.deepStrictEqual(typeof event.stopPropagation, 'function')
265
+ } finally {
266
+ component.global = previousGlobal
267
+ }
268
+ })
269
+
270
+ it('creates events from globalThis CustomEvent when component global has no window objects', () => {
271
+ setup()
272
+ const component = document.createElement('mock-component')
273
+
274
+ component.global = {}
275
+ const event = component._createEvent('global-custom-event', { ready: true })
276
+
277
+ assert.deepStrictEqual(event.type, 'global-custom-event')
278
+ assert.deepStrictEqual(event.detail.ready, true)
279
+ assert.deepStrictEqual(event instanceof globalThis.CustomEvent, true)
280
+ })
281
+
282
+ it('emits an error event when async load fails', async () => {
283
+ setup()
284
+ const component = document.createElement('async-load-component')
185
285
  let errorEvent = null
186
286
 
187
287
  component.addEventListener('error', (event) => {
@@ -194,6 +294,8 @@ it('emits an error event when async load fails', async () => {
194
294
 
195
295
  assert.ok(errorEvent)
196
296
  assert.deepStrictEqual(errorEvent.detail.message, 'Async Load Error!')
297
+ assert.deepStrictEqual(errorEvent.detail.phase, 'load')
298
+ assert.deepStrictEqual(errorEvent.detail.component, 'ASYNC-LOAD-COMPONENT')
197
299
  })
198
300
 
199
301
  it('emits an error event and throws when sync load fails', () => {
@@ -211,6 +313,176 @@ it('emits an error event and throws when sync load fails', () => {
211
313
 
212
314
  assert.ok(errorEvent)
213
315
  assert.deepStrictEqual(errorEvent.detail.message, 'Sync Load Error!')
316
+ assert.deepStrictEqual(errorEvent.detail.phase, 'load')
317
+ assert.deepStrictEqual(errorEvent.detail.component, 'SYNC-LOAD-COMPONENT')
318
+ })
319
+
320
+ it('coerces non-error load failures into errors with metadata', () => {
321
+ setup()
322
+ const component = document.createElement('string-load-component')
323
+ let errorEvent = null
324
+
325
+ component.addEventListener('error', (event) => {
326
+ errorEvent = event
327
+ })
328
+
329
+ assert.throws(() => {
330
+ component.connectedCallback()
331
+ }, /String Load Error!/)
332
+
333
+ assert.ok(errorEvent)
334
+ assert.deepStrictEqual(errorEvent.detail.message, 'String Load Error!')
335
+ assert.deepStrictEqual(errorEvent.detail.phase, 'load')
336
+ assert.deepStrictEqual(errorEvent.detail.component, 'STRING-LOAD-COMPONENT')
337
+ })
338
+
339
+ it('creates resolve events without CustomEvent and still resolves', () => {
340
+ setup()
341
+ const component = document.createElement('mock-component')
342
+ const previousCustomEvent = globalThis.CustomEvent
343
+
344
+ try {
345
+ globalThis.CustomEvent = undefined
346
+ let resource = null
347
+ component.addEventListener('resolve', (event) => {
348
+ resource = event.detail.resource
349
+ })
350
+
351
+ const result = component.resolve('dependency')
352
+
353
+ assert.deepStrictEqual(resource, 'dependency')
354
+ assert.strictEqual(result, undefined)
355
+ } finally {
356
+ globalThis.CustomEvent = previousCustomEvent
357
+ }
358
+ })
359
+
360
+ it('creates fallback events when neither CustomEvent nor Event are available', () => {
361
+ setup()
362
+ const component = document.createElement('mock-component')
363
+ const previousCustomEvent = globalThis.CustomEvent
364
+ const previousEvent = globalThis.Event
365
+
366
+ try {
367
+ globalThis.CustomEvent = undefined
368
+ globalThis.Event = undefined
369
+
370
+ component.global = {}
371
+ const event = component._createEvent('fallback', { ready: true })
372
+
373
+ assert.deepStrictEqual(event.type, 'fallback')
374
+ assert.deepStrictEqual(event.detail, { ready: true })
375
+ assert.deepStrictEqual(typeof event.stopPropagation, 'function')
376
+ assert.deepStrictEqual(typeof event.preventDefault, 'function')
377
+ assert.doesNotThrow(() => event.stopPropagation())
378
+ assert.doesNotThrow(() => event.preventDefault())
379
+ } finally {
380
+ globalThis.CustomEvent = previousCustomEvent
381
+ globalThis.Event = previousEvent
382
+ }
383
+ })
384
+
385
+ it('falls back to globalThis when component global is missing', () => {
386
+ setup()
387
+ const component = document.createElement('mock-component')
388
+ const previousCustomEvent = globalThis.CustomEvent
389
+
390
+ try {
391
+ globalThis.CustomEvent = undefined
392
+ component.global = null
393
+
394
+ const event = component._createEvent('global-fallback', { ready: true })
395
+
396
+ assert.deepStrictEqual(event.type, 'global-fallback')
397
+ assert.deepStrictEqual(event.detail.ready, true)
398
+ } finally {
399
+ globalThis.CustomEvent = previousCustomEvent
400
+ }
401
+ })
402
+
403
+ it('registers and runs cleanup callbacks on disconnect', () => {
404
+ setup()
405
+ const component = document.createElement('cleanup-component')
406
+ container.appendChild(component)
407
+
408
+ let cleanupCalls = 0
409
+ component.registerCleanup(() => {
410
+ cleanupCalls += 1
411
+ })
412
+ component.registerCleanup(() => {
413
+ cleanupCalls += 1
414
+ })
415
+
416
+ component.disconnectedCallback()
417
+
418
+ assert.deepStrictEqual(cleanupCalls, 2)
419
+ })
420
+
421
+ it('supports unregistering cleanup callbacks', () => {
422
+ setup()
423
+ const component = document.createElement('cleanup-component')
424
+ container.appendChild(component)
425
+
426
+ let cleanupCalls = 0
427
+ const unregister = component.registerCleanup(() => {
428
+ cleanupCalls += 1
429
+ })
430
+ unregister()
431
+ unregister()
432
+
433
+ component.disconnectedCallback()
434
+
435
+ assert.deepStrictEqual(cleanupCalls, 0)
436
+ })
437
+
438
+ it('supports disconnected init fast-path when local state is already set', () => {
439
+ setup()
440
+ const component = document.createElement('mock-component')
441
+ component.local = { preset: true }
442
+ const initSpy = mock.method(component, 'init')
443
+ container.appendChild(component)
444
+
445
+ assert.deepStrictEqual(initSpy.mock.calls.length, 0)
446
+ initSpy.mock.restore()
447
+ })
448
+
449
+ it('returns unchanged values when enhanced error details are absent', () => {
450
+ setup()
451
+ const component = document.createElement('cleanup-component')
452
+
453
+ const result = component._enhanceError(undefined, 'init-render')
454
+
455
+ assert.strictEqual(result, undefined)
456
+ })
457
+
458
+ it('accepts non-function cleanup callbacks as no-op registrations', () => {
459
+ setup()
460
+ const component = document.createElement('cleanup-component')
461
+
462
+ const unregister = component.registerCleanup()
463
+
464
+ assert.doesNotThrow(() => unregister())
465
+ })
466
+
467
+ it('emits an error event when a cleanup callback throws', () => {
468
+ setup()
469
+ const component = document.createElement('cleanup-component')
470
+ container.appendChild(component)
471
+ let errorEvent = null
472
+
473
+ component.addEventListener('error', (event) => {
474
+ errorEvent = event
475
+ })
476
+ component.registerCleanup(() => {
477
+ throw new Error('Cleanup failed')
478
+ })
479
+
480
+ component.disconnectedCallback()
481
+
482
+ assert.ok(errorEvent)
483
+ assert.deepStrictEqual(errorEvent.detail.message, 'Cleanup failed')
484
+ assert.deepStrictEqual(errorEvent.detail.phase, 'cleanup')
485
+ assert.deepStrictEqual(errorEvent.detail.component, 'CLEANUP-COMPONENT')
214
486
  })
215
487
 
216
488
  it('supports load implementations that do not return promises', () => {
@@ -611,3 +883,24 @@ it('provides a styleNames utility function for setting styles', () => {
611
883
 
612
884
  assert.deepStrictEqual(result, 'background-primary shadow-small')
613
885
  })
886
+
887
+ it('reuses listener bindings when content does not change', () => {
888
+ setup()
889
+ container.innerHTML = `
890
+ <optimized-listener-component></optimized-listener-component>
891
+ `
892
+ const component = container.querySelector('optimized-listener-component')
893
+
894
+ component.render()
895
+ component.querySelector('button').click()
896
+
897
+ component.render()
898
+ component.querySelector('button').click()
899
+
900
+ component.content = '<button listen on-click="onClick">Click me</button>'
901
+ component.render()
902
+ component.querySelector('button').click()
903
+
904
+ assert.deepStrictEqual(component.clicks, 3)
905
+ assert.deepStrictEqual(component._needsBinding, false)
906
+ })
@@ -1,3 +1,6 @@
1
1
  export { Component } from './component.js'
2
+
3
+ /** @type {typeof String.raw} */
2
4
  export const css = String.raw
5
+ /** @type {typeof String.raw} */
3
6
  export const html = String.raw