@inglorious/web 2.0.0
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/LICENSE +9 -0
- package/README.md +383 -0
- package/package.json +54 -0
- package/src/form.js +371 -0
- package/src/index.js +10 -0
- package/src/list.js +105 -0
- package/src/mount.js +49 -0
- package/src/router.js +268 -0
- package/types/form.d.ts +377 -0
- package/types/index.d.ts +2 -0
- package/types/mount.d.ts +24 -0
- package/types/router.d.ts +111 -0
package/src/form.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { clone, get, set } from "@inglorious/utils/data-structures/object.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('../types/form.js').FormEntity} FormEntity
|
|
5
|
+
* @typedef {import('../types/form.js').FormFieldChangePayload} FormFieldChangePayload
|
|
6
|
+
* @typedef {import('../types/form.js').FormFieldBlurPayload} FormFieldBlurPayload
|
|
7
|
+
*
|
|
8
|
+
* @typedef {object} FormValidatePayload
|
|
9
|
+
* @property {string|number} entityId - The ID of the target form entity.
|
|
10
|
+
* @property {(values: object) => import('../types/form.js').FormErrors<object>} validate - A function that validates the entire form's values and returns a complete error object.
|
|
11
|
+
*
|
|
12
|
+
* @typedef {object} FormValidateAsyncPayload
|
|
13
|
+
* @property {string|number} entityId - The ID of the target form entity.
|
|
14
|
+
* @property {(values: object) => Promise<import('../types/form.js').FormErrors<object>>} validate - An async function that validates the entire form's values.
|
|
15
|
+
*
|
|
16
|
+
* @typedef {object} FormValidationCompletePayload
|
|
17
|
+
* @property {string|number} entityId - The ID of the target form entity.
|
|
18
|
+
* @property {import('../types/form.js').FormErrors<object>} errors - The validation errors.
|
|
19
|
+
* @property {boolean} isValid - Whether the form is valid.
|
|
20
|
+
*
|
|
21
|
+
* @typedef {object} FormValidationErrorPayload
|
|
22
|
+
* @property {string|number} entityId - The ID of the target form entity.
|
|
23
|
+
* @property {string} error - The error message.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const NO_ITEMS_REMOVED = 0
|
|
27
|
+
const ONE_ITEM_REMOVED = 1
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A type definition for managing form state within an entity-based system.
|
|
31
|
+
* It handles initialization, field changes, validation, and submission.
|
|
32
|
+
*
|
|
33
|
+
* @type {import('@inglorious/engine/types/type.js').Type}
|
|
34
|
+
*/
|
|
35
|
+
export const form = {
|
|
36
|
+
/**
|
|
37
|
+
* Initializes the form entity by resetting it to its initial state.
|
|
38
|
+
* @param {FormEntity} entity - The form entity.
|
|
39
|
+
*/
|
|
40
|
+
init(entity) {
|
|
41
|
+
resetForm(entity)
|
|
42
|
+
|
|
43
|
+
document.addEventListener("click", (e) => {
|
|
44
|
+
const button = e.target.closest("button")
|
|
45
|
+
|
|
46
|
+
if (!button || button.getAttribute("type") === "submit") return
|
|
47
|
+
|
|
48
|
+
e.preventDefault()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
document.addEventListener("submit", (e) => {
|
|
52
|
+
const form = e.target.closest("form")
|
|
53
|
+
|
|
54
|
+
if (!form) return
|
|
55
|
+
|
|
56
|
+
e.preventDefault()
|
|
57
|
+
})
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resets the form entity when a 'create' event matches its ID.
|
|
62
|
+
* @param {FormEntity} entity - The form entity.
|
|
63
|
+
* @param {string|number} entityId - The ID from the create event.
|
|
64
|
+
*/
|
|
65
|
+
create(entity) {
|
|
66
|
+
resetForm(entity)
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Appends an item to a field array.
|
|
71
|
+
* @param {FormEntity} entity - The form entity.
|
|
72
|
+
* @param {Object} payload
|
|
73
|
+
* @param {string|number} payload.entityId - The form entity ID.
|
|
74
|
+
* @param {string} payload.path - Path to the array field (e.g., 'addresses').
|
|
75
|
+
* @param {*} payload.value - The value to append.
|
|
76
|
+
*/
|
|
77
|
+
fieldArrayAppend(entity, { path, value }) {
|
|
78
|
+
const array = get(entity.values, path)
|
|
79
|
+
if (!Array.isArray(array)) {
|
|
80
|
+
console.warn(`Field at path '${path}' is not an array`)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
array.push(value)
|
|
85
|
+
|
|
86
|
+
const errorsArray = get(entity.errors, path, [])
|
|
87
|
+
errorsArray.push(initMetadata(value))
|
|
88
|
+
|
|
89
|
+
const touchedArray = get(entity.touched, path, [])
|
|
90
|
+
touchedArray.push(initMetadata(value))
|
|
91
|
+
|
|
92
|
+
entity.isPristine = false
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Removes an item from a field array by index.
|
|
97
|
+
* @param {FormEntity} entity - The form entity.
|
|
98
|
+
* @param {Object} payload
|
|
99
|
+
* @param {string|number} payload.entityId - The form entity ID.
|
|
100
|
+
* @param {string} payload.path - Path to the array field.
|
|
101
|
+
* @param {number} payload.index - The index to remove.
|
|
102
|
+
*/
|
|
103
|
+
fieldArrayRemove(entity, { path, index }) {
|
|
104
|
+
const array = get(entity.values, path)
|
|
105
|
+
if (!Array.isArray(array)) {
|
|
106
|
+
console.warn(`Field at path '${path}' is not an array`)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
array.splice(index, ONE_ITEM_REMOVED)
|
|
111
|
+
|
|
112
|
+
const errorsArray = get(entity.errors, path)
|
|
113
|
+
errorsArray.splice(index, ONE_ITEM_REMOVED)
|
|
114
|
+
|
|
115
|
+
const touchedArray = get(entity.touched, path)
|
|
116
|
+
touchedArray.splice(index, ONE_ITEM_REMOVED)
|
|
117
|
+
|
|
118
|
+
entity.isPristine = false
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Inserts an item into a field array at a specific index.
|
|
123
|
+
* @param {FormEntity} entity - The form entity.
|
|
124
|
+
* @param {Object} payload
|
|
125
|
+
* @param {string|number} payload.entityId - The form entity ID.
|
|
126
|
+
* @param {string} payload.path - Path to the array field.
|
|
127
|
+
* @param {number} payload.index - The index to insert at.
|
|
128
|
+
* @param {*} payload.value - The value to insert.
|
|
129
|
+
*/
|
|
130
|
+
fieldArrayInsert(entity, { path, index, value }) {
|
|
131
|
+
const array = get(entity.values, path)
|
|
132
|
+
if (!Array.isArray(array)) {
|
|
133
|
+
console.warn(`Field at path '${path}' is not an array`)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
array.splice(index, NO_ITEMS_REMOVED, value)
|
|
138
|
+
|
|
139
|
+
const errorsArray = get(entity.errors, path)
|
|
140
|
+
errorsArray.splice(index, NO_ITEMS_REMOVED, initMetadata(value))
|
|
141
|
+
|
|
142
|
+
const touchedArray = get(entity.touched, path)
|
|
143
|
+
touchedArray.splice(index, NO_ITEMS_REMOVED, initMetadata(value))
|
|
144
|
+
|
|
145
|
+
entity.isPristine = false
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Moves an item in a field array from one index to another.
|
|
150
|
+
* @param {FormEntity} entity - The form entity.
|
|
151
|
+
* @param {Object} payload
|
|
152
|
+
* @param {string|number} payload.entityId - The form entity ID.
|
|
153
|
+
* @param {string} payload.path - Path to the array field.
|
|
154
|
+
* @param {number} payload.fromIndex - The source index.
|
|
155
|
+
* @param {number} payload.toIndex - The destination index.
|
|
156
|
+
*/
|
|
157
|
+
fieldArrayMove(entity, { path, fromIndex, toIndex }) {
|
|
158
|
+
const array = get(entity.values, path)
|
|
159
|
+
if (!Array.isArray(array)) {
|
|
160
|
+
console.warn(`Field at path '${path}' is not an array`)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const [item] = array.splice(fromIndex, ONE_ITEM_REMOVED)
|
|
165
|
+
array.splice(toIndex, NO_ITEMS_REMOVED, item)
|
|
166
|
+
|
|
167
|
+
const errorsArray = get(entity.errors, path)
|
|
168
|
+
const [errorItem] = errorsArray.splice(fromIndex, ONE_ITEM_REMOVED)
|
|
169
|
+
errorsArray.splice(toIndex, NO_ITEMS_REMOVED, errorItem)
|
|
170
|
+
|
|
171
|
+
const touchedArray = get(entity.touched, path)
|
|
172
|
+
const [touchedItem] = touchedArray.splice(fromIndex, ONE_ITEM_REMOVED)
|
|
173
|
+
touchedArray.splice(toIndex, NO_ITEMS_REMOVED, touchedItem)
|
|
174
|
+
|
|
175
|
+
entity.isPristine = false
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Handles the blur event for a form field, marking it as touched and optionally validating it.
|
|
180
|
+
* @param {FormEntity} entity - The form entity.
|
|
181
|
+
* @param {FormFieldBlurPayload} payload - The event payload.
|
|
182
|
+
*/
|
|
183
|
+
fieldBlur(entity, { path, validate }) {
|
|
184
|
+
set(entity.touched, path, true)
|
|
185
|
+
|
|
186
|
+
if (!validate) return
|
|
187
|
+
|
|
188
|
+
const error = validate(get(entity.values, path))
|
|
189
|
+
setFieldError(entity, path, error)
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Handles a change in a form field's value and optionally validates it.
|
|
194
|
+
* @param {FormEntity} entity - The form entity.
|
|
195
|
+
* @param {FormFieldChangePayload} payload - The event payload.
|
|
196
|
+
*/
|
|
197
|
+
fieldChange(entity, { path, value, validate }) {
|
|
198
|
+
setFieldValue(entity, path, value)
|
|
199
|
+
|
|
200
|
+
if (!validate) return
|
|
201
|
+
|
|
202
|
+
const error = validate(get(entity.values, path))
|
|
203
|
+
setFieldError(entity, path, error)
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Resets the form to its initial state.
|
|
208
|
+
* @param {FormEntity} entity - The form entity.
|
|
209
|
+
* @param {string|number} entityId - The ID of the target form entity.
|
|
210
|
+
*/
|
|
211
|
+
reset(entity) {
|
|
212
|
+
resetForm(entity)
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Synchronously validates the entire form.
|
|
217
|
+
* @param {FormEntity} entity - The form entity.
|
|
218
|
+
* @param {FormValidatePayload} payload - The event payload.
|
|
219
|
+
*/
|
|
220
|
+
validate(entity, { validate }) {
|
|
221
|
+
entity.errors = validate(entity.values)
|
|
222
|
+
entity.isValid = !hasErrors(entity.errors)
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Asynchronously validates the entire form.
|
|
227
|
+
* Dispatches 'formValidationComplete' on success or 'formValidationError' on failure.
|
|
228
|
+
* @param {FormEntity} entity - The form entity.
|
|
229
|
+
* @param {FormValidateAsyncPayload} payload - The event payload.
|
|
230
|
+
* @param {Object} api - The API object for notifying events.
|
|
231
|
+
*/
|
|
232
|
+
async validateAsync(entity, { validate }, api) {
|
|
233
|
+
try {
|
|
234
|
+
entity.isValidating = true
|
|
235
|
+
const errors = await validate(entity.values)
|
|
236
|
+
api.notify(`#${entity.id}:validationComplete`, {
|
|
237
|
+
errors,
|
|
238
|
+
isValid: !hasErrors(errors),
|
|
239
|
+
})
|
|
240
|
+
} catch (error) {
|
|
241
|
+
api.notify(`#${entity.id}:validationError`, {
|
|
242
|
+
error: error.message,
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Handles the completion of async validation.
|
|
249
|
+
* @param {FormEntity} entity - The form entity.
|
|
250
|
+
* @param {FormValidationCompletePayload} payload - The event payload.
|
|
251
|
+
*/
|
|
252
|
+
validationComplete(entity, { errors, isValid }) {
|
|
253
|
+
entity.isValidating = false
|
|
254
|
+
entity.errors = errors
|
|
255
|
+
entity.isValid = isValid
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Handles validation errors from async validation.
|
|
260
|
+
* @param {FormEntity} entity - The form entity.
|
|
261
|
+
* @param {FormValidationErrorPayload} payload - The event payload.
|
|
262
|
+
*/
|
|
263
|
+
validationError(entity, { error }) {
|
|
264
|
+
entity.isValidating = false
|
|
265
|
+
entity.submitError = error
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Retrieves the validation error for a specific form field.
|
|
271
|
+
* @param {FormEntity} form - The form entity object.
|
|
272
|
+
* @param {string} path - The dot-separated path to the field (e.g., 'user.name').
|
|
273
|
+
* @returns {string|null|undefined} The error message for the field, or null/undefined if there is no error.
|
|
274
|
+
*/
|
|
275
|
+
export function getFieldError(form, path) {
|
|
276
|
+
return get(form.errors, path)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Retrieves the value of a specific form field.
|
|
281
|
+
* @param {FormEntity} form - The form entity object.
|
|
282
|
+
* @param {string} path - The dot-separated path to the field (e.g., 'user.name').
|
|
283
|
+
* @param {*} [defaultValue] - An optional default value to return if the path does not exist.
|
|
284
|
+
* @returns {*} The value of the field, or the default value if not found.
|
|
285
|
+
*/
|
|
286
|
+
export function getFieldValue(form, path, defaultValue) {
|
|
287
|
+
return get(form.values, path, defaultValue)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Checks if a specific form field has been touched (i.e., has received a blur event).
|
|
292
|
+
* @param {FormEntity} form - The form entity object.
|
|
293
|
+
* @param {string} path - The dot-separated path to the field (e.g., 'user.name').
|
|
294
|
+
* @returns {boolean|undefined} `true` if the field has been touched, otherwise `false` or `undefined`.
|
|
295
|
+
*/
|
|
296
|
+
export function isFieldTouched(form, path) {
|
|
297
|
+
return get(form.touched, path)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Private helper functions
|
|
301
|
+
|
|
302
|
+
function hasErrors(errors) {
|
|
303
|
+
if (errors === null || errors === undefined) {
|
|
304
|
+
return false
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (typeof errors !== "object") {
|
|
308
|
+
// Leaf value - check if it's truthy (error string)
|
|
309
|
+
return Boolean(errors)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (Array.isArray(errors)) {
|
|
313
|
+
return errors.some((item) => hasErrors(item))
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Object - check all properties
|
|
317
|
+
return Object.values(errors).some((value) => hasErrors(value))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function initMetadata(value) {
|
|
321
|
+
if (Array.isArray(value)) {
|
|
322
|
+
return value.map((item) => initMetadata(item))
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (typeof value === "object" && value !== null) {
|
|
326
|
+
return Object.fromEntries(
|
|
327
|
+
Object.entries(value).map(([key, val]) => [key, initMetadata(val)]),
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return null
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function resetForm(form) {
|
|
335
|
+
form.values = clone(form.initialValues)
|
|
336
|
+
form.errors = initMetadata(form.initialValues)
|
|
337
|
+
form.touched = initMetadata(form.initialValues)
|
|
338
|
+
|
|
339
|
+
form.isValid = true
|
|
340
|
+
form.isPristine = true
|
|
341
|
+
|
|
342
|
+
form.isValidating = false
|
|
343
|
+
form.isSubmitting = false
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function setFieldValue(form, path, value) {
|
|
347
|
+
set(form.values, path, value)
|
|
348
|
+
set(form.touched, path, true)
|
|
349
|
+
|
|
350
|
+
form.isPristine = false
|
|
351
|
+
|
|
352
|
+
// By clearing the error for this path, the form might become valid.
|
|
353
|
+
// We delegate to setFieldError(..., null) to correctly run the check.
|
|
354
|
+
setFieldError(form, path, null)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function setFieldError(form, path, error) {
|
|
358
|
+
// Ensure we set null if error is falsy, but not an empty string
|
|
359
|
+
const newError = error || null
|
|
360
|
+
set(form.errors, path, newError)
|
|
361
|
+
|
|
362
|
+
if (newError) {
|
|
363
|
+
// If we are adding an error, the form is definitely invalid.
|
|
364
|
+
// No need for an expensive check.
|
|
365
|
+
form.isValid = false
|
|
366
|
+
} else {
|
|
367
|
+
// If we are removing an error, the form MIGHT become valid.
|
|
368
|
+
// This is when we must perform the check.
|
|
369
|
+
form.isValid = !hasErrors(form.errors)
|
|
370
|
+
}
|
|
371
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { form, getFieldError, getFieldValue, isFieldTouched } from "./form.js"
|
|
2
|
+
export { list } from "./list.js"
|
|
3
|
+
export { mount } from "./mount.js"
|
|
4
|
+
export { router } from "./router.js"
|
|
5
|
+
export { createStore } from "@inglorious/store"
|
|
6
|
+
export { createDevtools } from "@inglorious/store/client/devtools.js"
|
|
7
|
+
export { createSelector } from "@inglorious/store/select.js"
|
|
8
|
+
export { html, render, svg } from "lit-html"
|
|
9
|
+
export { classMap } from "lit-html/directives/class-map.js"
|
|
10
|
+
export { repeat } from "lit-html/directives/repeat.js"
|
package/src/list.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { html } from "lit-html"
|
|
2
|
+
import { ref } from "lit-html/directives/ref.js"
|
|
3
|
+
|
|
4
|
+
const LIST_START = 0
|
|
5
|
+
|
|
6
|
+
export const list = {
|
|
7
|
+
init(entity) {
|
|
8
|
+
resetList(entity)
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
create(entity) {
|
|
12
|
+
resetList(entity)
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
render(entity, api) {
|
|
16
|
+
const { items, visibleRange, viewportHeight, itemHeight, estimatedHeight } =
|
|
17
|
+
entity
|
|
18
|
+
const types = api.getTypes()
|
|
19
|
+
const type = types[entity.type]
|
|
20
|
+
|
|
21
|
+
if (!items) {
|
|
22
|
+
console.warn(`list entity ${entity.id} needs 'items'`)
|
|
23
|
+
return html``
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!type.renderItem) {
|
|
27
|
+
console.warn(`type ${entity.type} needs 'renderItem' method`)
|
|
28
|
+
return html``
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const visibleItems = items.slice(visibleRange.start, visibleRange.end)
|
|
32
|
+
const height = itemHeight || estimatedHeight
|
|
33
|
+
const totalHeight = items.length * height
|
|
34
|
+
|
|
35
|
+
return html`
|
|
36
|
+
<div
|
|
37
|
+
style="height: ${viewportHeight}px; overflow: auto"
|
|
38
|
+
@scroll=${(e) => api.notify(`#${entity.id}:scroll`, e.target)}
|
|
39
|
+
${ref((el) => {
|
|
40
|
+
if (el && !itemHeight) {
|
|
41
|
+
queueMicrotask(() => {
|
|
42
|
+
api.notify(`#${entity.id}:measureHeight`, el)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
})}
|
|
46
|
+
>
|
|
47
|
+
<div style="height: ${totalHeight}px; position: relative">
|
|
48
|
+
${visibleItems.map((item, idx) => {
|
|
49
|
+
const absoluteIndex = visibleRange.start + idx
|
|
50
|
+
const top = absoluteIndex * height
|
|
51
|
+
|
|
52
|
+
return html`
|
|
53
|
+
<div
|
|
54
|
+
style="position: absolute; top: ${top}px; width: 100%"
|
|
55
|
+
data-index=${absoluteIndex}
|
|
56
|
+
>
|
|
57
|
+
${type.renderItem(item, absoluteIndex, api)}
|
|
58
|
+
</div>
|
|
59
|
+
`
|
|
60
|
+
})}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
`
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
scroll(entity, containerEl) {
|
|
67
|
+
const scrollTop = containerEl.scrollTop
|
|
68
|
+
const { items, bufferSize, itemHeight, estimatedHeight, viewportHeight } =
|
|
69
|
+
entity
|
|
70
|
+
const height = itemHeight || estimatedHeight
|
|
71
|
+
|
|
72
|
+
const start = Math.max(
|
|
73
|
+
LIST_START,
|
|
74
|
+
Math.floor(scrollTop / height) - bufferSize,
|
|
75
|
+
)
|
|
76
|
+
const visibleCount = Math.ceil(viewportHeight / height)
|
|
77
|
+
const end = Math.min(start + visibleCount + bufferSize, items.length)
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
entity.visibleRange.start === start &&
|
|
81
|
+
entity.visibleRange.end === end
|
|
82
|
+
) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
entity.scrollTop = scrollTop
|
|
87
|
+
entity.visibleRange = { start, end }
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
measureHeight(entity, containerEl) {
|
|
91
|
+
const firstItem = containerEl.querySelector("[data-index]")
|
|
92
|
+
if (!firstItem) return
|
|
93
|
+
|
|
94
|
+
entity.itemHeight = firstItem.offsetHeight
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resetList(entity) {
|
|
99
|
+
entity.scrollTop = 0
|
|
100
|
+
entity.visibleRange ??= { start: 0, end: 20 }
|
|
101
|
+
entity.viewportHeight ??= 600
|
|
102
|
+
entity.bufferSize ??= 5
|
|
103
|
+
entity.itemHeight ??= null
|
|
104
|
+
entity.estimatedHeight ??= 50
|
|
105
|
+
}
|
package/src/mount.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { html, render } from "lit-html"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mounts a lit-html template to the DOM and subscribes to a store for re-rendering.
|
|
5
|
+
* @param {import('@inglorious/store').Store} store - The application state store.
|
|
6
|
+
* @param {(api: import('../types/mount').Api) => import('lit-html').TemplateResult | null} renderFn - The root render function.
|
|
7
|
+
* @param {HTMLElement | DocumentFragment} element - The DOM element to mount the template to.
|
|
8
|
+
* @returns {() => void} An unsubscribe function
|
|
9
|
+
*/
|
|
10
|
+
export function mount(store, renderFn, element) {
|
|
11
|
+
const api = {
|
|
12
|
+
...store._api,
|
|
13
|
+
|
|
14
|
+
/** @param {string} id */
|
|
15
|
+
render(id, options = {}) {
|
|
16
|
+
const entity = api.getEntity(id)
|
|
17
|
+
const types = api.getTypes()
|
|
18
|
+
|
|
19
|
+
if (!entity) {
|
|
20
|
+
const { allowType } = options
|
|
21
|
+
if (!allowType) {
|
|
22
|
+
return ""
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// No entity with this ID, try static type
|
|
26
|
+
const type = types[id]
|
|
27
|
+
if (!type?.render) {
|
|
28
|
+
console.warn(`No entity or type found: ${id}`)
|
|
29
|
+
return html`<div>Not found: ${id}</div>`
|
|
30
|
+
}
|
|
31
|
+
return type.render(api)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Entity exists, render it
|
|
35
|
+
const type = types[entity.type]
|
|
36
|
+
if (!type?.render) {
|
|
37
|
+
console.warn(`No render function for type: ${entity.type}`)
|
|
38
|
+
return html`<div>No renderer for ${entity.type}</div>`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return type.render(entity, api)
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const unsubscribe = store.subscribe(() => render(renderFn(api), element))
|
|
46
|
+
store.notify("init")
|
|
47
|
+
|
|
48
|
+
return unsubscribe
|
|
49
|
+
}
|