@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.
- package/README.md +57 -45
- package/lib/base/component/component.js +127 -21
- package/lib/base/component/component.test.js +296 -3
- package/lib/base/component/index.js +3 -0
- package/lib/base/styles/index.js +4 -1
- package/lib/base/utils/define.js +2 -1
- package/lib/base/utils/format.js +12 -6
- package/lib/base/utils/helpers.js +31 -5
- package/lib/base/utils/index.js +1 -0
- package/lib/base/utils/slots.js +3 -2
- package/lib/base/utils/uuid.js +1 -1
- package/lib/components/audio/components/audio.js +17 -2
- package/lib/components/audio/index.js +1 -0
- package/lib/components/audio/styles/index.js +5 -1
- package/lib/components/camera/components/camera.js +10 -0
- package/lib/components/camera/index.js +1 -0
- package/lib/components/camera/styles/index.js +5 -1
- package/lib/components/capture/components/capture.js +18 -2
- package/lib/components/capture/index.js +1 -0
- package/lib/components/droparea/components/droparea-preview.js +58 -13
- package/lib/components/droparea/components/droparea-preview.test.js +82 -0
- package/lib/components/droparea/components/droparea.js +41 -2
- package/lib/components/droparea/index.js +1 -0
- package/lib/components/droparea/styles/index.js +5 -1
- package/lib/components/emit/components/emit.js +11 -1
- package/lib/components/emit/index.js +1 -0
- package/lib/components/index.js +2 -1
- package/lib/components/list/components/item.js +6 -0
- package/lib/components/list/components/list.js +18 -4
- package/lib/components/list/index.js +1 -0
- package/lib/components/paginator/components/paginator.js +34 -8
- package/lib/components/paginator/index.js +1 -0
- package/lib/components/paginator/styles/index.js +5 -1
- package/lib/components/spinner/components/spinner.js +10 -0
- package/lib/components/spinner/index.js +1 -0
- package/lib/components/spinner/styles/index.js +5 -1
- package/lib/components/splitview/components/splitview.detail.js +10 -1
- package/lib/components/splitview/components/splitview.js +18 -3
- package/lib/components/splitview/components/splitview.master.js +10 -0
- package/lib/components/splitview/index.js +1 -0
- package/lib/components/translate/components/translate.js +42 -11
- package/lib/components/translate/components/translate.test.js +169 -1
- package/lib/components/translate/index.js +1 -0
- package/lib/index.js +3 -0
- package/package.json +2 -1
- package/tsconfig.json +1 -1
- package/types/base/component/component.d.ts +43 -8
- package/types/base/component/component.d.ts.map +1 -1
- package/types/base/component/index.d.ts +4 -6
- package/types/base/component/index.d.ts.map +1 -1
- package/types/base/styles/index.d.ts +3 -2
- package/types/base/styles/index.d.ts.map +1 -1
- package/types/base/utils/define.d.ts +3 -2
- package/types/base/utils/define.d.ts.map +1 -1
- package/types/base/utils/format.d.ts +12 -6
- package/types/base/utils/format.d.ts.map +1 -1
- package/types/base/utils/helpers.d.ts +27 -7
- package/types/base/utils/helpers.d.ts.map +1 -1
- package/types/base/utils/slots.d.ts +8 -10
- package/types/base/utils/slots.d.ts.map +1 -1
- package/types/base/utils/uuid.d.ts +1 -1
- package/types/base/utils/uuid.d.ts.map +1 -1
- package/types/components/audio/components/audio.d.ts +23 -9
- package/types/components/audio/components/audio.d.ts.map +1 -1
- package/types/components/audio/styles/index.d.ts +3 -2
- package/types/components/audio/styles/index.d.ts.map +1 -1
- package/types/components/camera/components/camera.d.ts +11 -3
- package/types/components/camera/components/camera.d.ts.map +1 -1
- package/types/components/camera/styles/index.d.ts +3 -2
- package/types/components/camera/styles/index.d.ts.map +1 -1
- package/types/components/capture/components/capture.d.ts +23 -3
- package/types/components/capture/components/capture.d.ts.map +1 -1
- package/types/components/droparea/components/droparea-preview.d.ts +64 -11
- package/types/components/droparea/components/droparea-preview.d.ts.map +1 -1
- package/types/components/droparea/components/droparea.d.ts +58 -13
- package/types/components/droparea/components/droparea.d.ts.map +1 -1
- package/types/components/droparea/styles/index.d.ts +3 -2
- package/types/components/droparea/styles/index.d.ts.map +1 -1
- package/types/components/emit/components/emit.d.ts +15 -3
- package/types/components/emit/components/emit.d.ts.map +1 -1
- package/types/components/list/components/item.d.ts +8 -1
- package/types/components/list/components/item.d.ts.map +1 -1
- package/types/components/list/components/list.d.ts +23 -5
- package/types/components/list/components/list.d.ts.map +1 -1
- package/types/components/paginator/components/paginator.d.ts +32 -8
- package/types/components/paginator/components/paginator.d.ts.map +1 -1
- package/types/components/paginator/styles/index.d.ts +3 -2
- package/types/components/paginator/styles/index.d.ts.map +1 -1
- package/types/components/spinner/components/spinner.d.ts +14 -3
- package/types/components/spinner/components/spinner.d.ts.map +1 -1
- package/types/components/spinner/styles/index.d.ts +3 -2
- package/types/components/spinner/styles/index.d.ts.map +1 -1
- package/types/components/splitview/components/splitview.d.ts +22 -4
- package/types/components/splitview/components/splitview.d.ts.map +1 -1
- package/types/components/splitview/components/splitview.detail.d.ts +12 -2
- package/types/components/splitview/components/splitview.detail.d.ts.map +1 -1
- package/types/components/splitview/components/splitview.master.d.ts +12 -1
- package/types/components/splitview/components/splitview.master.d.ts.map +1 -1
- package/types/components/translate/components/translate.d.ts +44 -10
- 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
19
|
-
|
|
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 {
|
|
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 {
|
|
109
|
+
/** @return {this} */
|
|
82
110
|
render () {
|
|
83
111
|
this.classList.add(this.tagName.toLowerCase())
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 =
|
|
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('
|
|
183
|
-
|
|
184
|
-
|
|
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
|
+
})
|