simplyflow 0.2.3 → 0.3.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/dist/simply.flow.js +223 -63
- package/dist/simply.flow.min.js +1 -1
- package/dist/simply.flow.min.js.map +3 -3
- package/package.json +1 -1
- package/src/bind.mjs +267 -72
- package/src/model.mjs +13 -13
- package/src/state.mjs +41 -1
package/src/bind.mjs
CHANGED
|
@@ -1,12 +1,36 @@
|
|
|
1
1
|
import { throttledEffect, destroy } from './state.mjs'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Implements one way databinding, updating dom elements with matching attributes
|
|
5
|
+
* to changes in signals (see state.mjs)
|
|
6
|
+
*
|
|
7
|
+
* @class
|
|
8
|
+
*/
|
|
9
|
+
class SimplyBind
|
|
10
|
+
{
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param Object options - a set of options for this instance, options may include:
|
|
14
|
+
* - root (signal) (required) - the root data object that contains al signals that can be bound
|
|
15
|
+
* - container (HTMLElement) - the dom element to use as the root for all bindings
|
|
16
|
+
* - attribute (string) - the prefix for the field, list and map attributes, e.g. 'data-bind'
|
|
17
|
+
* - transformers (object name:function) - a map of transformer names and functions
|
|
18
|
+
* - defaultTransformers (object with field, list and map properties)
|
|
19
|
+
*/
|
|
20
|
+
constructor(options)
|
|
21
|
+
{
|
|
22
|
+
/**
|
|
23
|
+
* A map of HTMLElements and the data bindings on each, in the form of
|
|
24
|
+
* the connectedSignal returned by the (throttled)Effect.
|
|
25
|
+
* @type {Map}
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
5
28
|
this.bindings = new Map()
|
|
29
|
+
|
|
6
30
|
const defaultOptions = {
|
|
7
31
|
container: document.body,
|
|
8
32
|
attribute: 'data-bind',
|
|
9
|
-
transformers:
|
|
33
|
+
transformers: {},
|
|
10
34
|
defaultTransformers: {
|
|
11
35
|
field: [defaultFieldTransformer],
|
|
12
36
|
list: [defaultListTransformer],
|
|
@@ -17,10 +41,10 @@ class SimplyBind {
|
|
|
17
41
|
throw new Error('bind needs at least options.root set')
|
|
18
42
|
}
|
|
19
43
|
this.options = Object.assign({}, defaultOptions, options)
|
|
20
|
-
|
|
21
44
|
const attribute = this.options.attribute
|
|
22
45
|
const bindAttributes = [attribute+'-field',attribute+'-list',attribute+'-map']
|
|
23
46
|
const bindSelector = `[${attribute}-field],[${attribute}-list],[${attribute}-map]`
|
|
47
|
+
const transformAttribute = attribute+'-transform'
|
|
24
48
|
|
|
25
49
|
const getBindingAttribute = (el) => {
|
|
26
50
|
const foundAttribute = bindAttributes.find(attr => el.hasAttribute(attr))
|
|
@@ -32,9 +56,17 @@ class SimplyBind {
|
|
|
32
56
|
|
|
33
57
|
// sets up the effect that updates the element if its
|
|
34
58
|
// data binding value changes
|
|
35
|
-
|
|
36
59
|
const render = (el) => {
|
|
37
60
|
this.bindings.set(el, throttledEffect(() => {
|
|
61
|
+
if (!el.isConnected) {
|
|
62
|
+
// el is no longer part of this document
|
|
63
|
+
untrack(el, this.getBindingPath(el))
|
|
64
|
+
destroy(this.bindings.get(el))
|
|
65
|
+
// doing this here instead of in a mutationobserver
|
|
66
|
+
// allows an element to be temporary removed and then inserted
|
|
67
|
+
// without the binding having to be reset
|
|
68
|
+
return
|
|
69
|
+
}
|
|
38
70
|
const context = {
|
|
39
71
|
templates: el.querySelectorAll(':scope > template'),
|
|
40
72
|
attribute: getBindingAttribute(el)
|
|
@@ -42,8 +74,9 @@ class SimplyBind {
|
|
|
42
74
|
context.path = this.getBindingPath(el)
|
|
43
75
|
context.value = getValueByPath(this.options.root, context.path)
|
|
44
76
|
context.element = el
|
|
77
|
+
track(el, context)
|
|
45
78
|
runTransformers(context)
|
|
46
|
-
},
|
|
79
|
+
}, 50))
|
|
47
80
|
}
|
|
48
81
|
|
|
49
82
|
// finds and runs applicable transformers
|
|
@@ -63,14 +96,16 @@ class SimplyBind {
|
|
|
63
96
|
transformers = this.options.defaultTransformers.map || []
|
|
64
97
|
break
|
|
65
98
|
}
|
|
66
|
-
if (context.element.
|
|
67
|
-
context.element.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
99
|
+
if (context.element.hasAttribute(transformAttribute)) {
|
|
100
|
+
context.element.getAttribute(transformAttribute)
|
|
101
|
+
.split(' ').filter(Boolean)
|
|
102
|
+
.forEach(t => {
|
|
103
|
+
if (this.options.transformers[t]) {
|
|
104
|
+
transformers.push(this.options.transformers[t])
|
|
105
|
+
} else {
|
|
106
|
+
console.warn('No transformer with name '+t+' configured', {cause:context.element})
|
|
107
|
+
}
|
|
108
|
+
})
|
|
74
109
|
}
|
|
75
110
|
let next
|
|
76
111
|
for (let transformer of transformers) {
|
|
@@ -87,7 +122,9 @@ class SimplyBind {
|
|
|
87
122
|
// this renders each of those elements
|
|
88
123
|
const applyBindings = (bindings) => {
|
|
89
124
|
for (let bindingEl of bindings) {
|
|
90
|
-
|
|
125
|
+
if (!this.bindings.get(bindingEl)) { // bindingEl may have moved from somewhere else in this document
|
|
126
|
+
render(bindingEl)
|
|
127
|
+
}
|
|
91
128
|
}
|
|
92
129
|
}
|
|
93
130
|
|
|
@@ -141,8 +178,11 @@ class SimplyBind {
|
|
|
141
178
|
/**
|
|
142
179
|
* Finds the first matching template and creates a new DocumentFragment
|
|
143
180
|
* with the correct data bind attributes in it (prepends the current path)
|
|
181
|
+
* @param Context context
|
|
182
|
+
* @return DocumentFragment
|
|
144
183
|
*/
|
|
145
|
-
applyTemplate(context)
|
|
184
|
+
applyTemplate(context)
|
|
185
|
+
{
|
|
146
186
|
const path = context.path
|
|
147
187
|
const templates = context.templates
|
|
148
188
|
const list = context.list
|
|
@@ -182,13 +222,28 @@ class SimplyBind {
|
|
|
182
222
|
if (typeof index !== 'undefined') {
|
|
183
223
|
clone.children[0].setAttribute(attribute+'-key',index)
|
|
184
224
|
}
|
|
185
|
-
// keep track of the used template, so if that changes, the
|
|
186
|
-
|
|
187
|
-
|
|
225
|
+
// keep track of the used template, so if that changes, the item can be updated
|
|
226
|
+
Object.defineProperty(
|
|
227
|
+
clone.children[0],
|
|
228
|
+
'$bindTemplate',
|
|
229
|
+
{
|
|
230
|
+
value: template,
|
|
231
|
+
enumerable: false,
|
|
232
|
+
writable: true,
|
|
233
|
+
configurable: true
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
// return clone, not the firstChild, so that all whitespace is cloned as well
|
|
188
237
|
return clone
|
|
189
238
|
}
|
|
190
239
|
|
|
191
|
-
|
|
240
|
+
/**
|
|
241
|
+
* Returns the path referenced in either the field, list or map attribute
|
|
242
|
+
* @param HTMLElement el
|
|
243
|
+
* @return string The path referenced, or void
|
|
244
|
+
*/
|
|
245
|
+
getBindingPath(el)
|
|
246
|
+
{
|
|
192
247
|
const attributes = [
|
|
193
248
|
this.options.attribute+'-field',
|
|
194
249
|
this.options.attribute+'-list',
|
|
@@ -205,7 +260,8 @@ class SimplyBind {
|
|
|
205
260
|
* Finds the first template from an array of templates that
|
|
206
261
|
* matches the given value.
|
|
207
262
|
*/
|
|
208
|
-
findTemplate(templates, value)
|
|
263
|
+
findTemplate(templates, value)
|
|
264
|
+
{
|
|
209
265
|
const templateMatches = t => {
|
|
210
266
|
// find the value to match against (e.g. data-bind="foo")
|
|
211
267
|
let path = this.getBindingPath(t)
|
|
@@ -253,7 +309,8 @@ class SimplyBind {
|
|
|
253
309
|
return template
|
|
254
310
|
}
|
|
255
311
|
|
|
256
|
-
destroy()
|
|
312
|
+
destroy()
|
|
313
|
+
{
|
|
257
314
|
this.bindings.forEach(binding => {
|
|
258
315
|
destroy(binding)
|
|
259
316
|
})
|
|
@@ -272,11 +329,33 @@ export function bind(options)
|
|
|
272
329
|
return new SimplyBind(options)
|
|
273
330
|
}
|
|
274
331
|
|
|
332
|
+
const tracking = new Map()
|
|
333
|
+
|
|
334
|
+
export function trace(path)
|
|
335
|
+
{
|
|
336
|
+
return tracking.get(path)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function track(el, context) {
|
|
340
|
+
if (!tracking.has(context.path)) {
|
|
341
|
+
tracking.set(context.path, [context])
|
|
342
|
+
} else {
|
|
343
|
+
tracking.get(context.path).push(context)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function untrack(el, path) {
|
|
348
|
+
let list = tracking.get(path)
|
|
349
|
+
list = list.filter(context => context.element == el)
|
|
350
|
+
tracking.set(path, list)
|
|
351
|
+
}
|
|
352
|
+
|
|
275
353
|
/**
|
|
276
354
|
* Returns true if a matches b, either by having the
|
|
277
355
|
* same string value, or matching string :empty against a falsy value
|
|
278
356
|
*/
|
|
279
|
-
export function matchValue(a,b)
|
|
357
|
+
export function matchValue(a,b)
|
|
358
|
+
{
|
|
280
359
|
if (a==':empty' && !b) {
|
|
281
360
|
return true
|
|
282
361
|
}
|
|
@@ -290,10 +369,12 @@ export function matchValue(a,b) {
|
|
|
290
369
|
}
|
|
291
370
|
|
|
292
371
|
/**
|
|
293
|
-
* Returns the value by walking the given path
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
372
|
+
* Returns the value by walking the given path as a json pointer, starting at root
|
|
373
|
+
* if you have a property with a '.' in its name urlencode the '.', e.g: %46
|
|
374
|
+
*
|
|
375
|
+
* @param HTMLElement root
|
|
376
|
+
* @param string path e.g. 'foo.bar'
|
|
377
|
+
* @return mixed the value found by walking the path from the root object or undefined
|
|
297
378
|
*/
|
|
298
379
|
export function getValueByPath(root, path)
|
|
299
380
|
{
|
|
@@ -321,7 +402,8 @@ export function getValueByPath(root, path)
|
|
|
321
402
|
* Default transformer for data binding
|
|
322
403
|
* Will be used unless overriden in the SimplyBind options parameter
|
|
323
404
|
*/
|
|
324
|
-
export function defaultFieldTransformer(context)
|
|
405
|
+
export function defaultFieldTransformer(context)
|
|
406
|
+
{
|
|
325
407
|
const el = context.element
|
|
326
408
|
const templates = context.templates
|
|
327
409
|
const templatesCount = templates.length
|
|
@@ -345,6 +427,15 @@ export function defaultFieldTransformer(context) {
|
|
|
345
427
|
case 'A':
|
|
346
428
|
transformAnchor.call(this, context)
|
|
347
429
|
break
|
|
430
|
+
case 'IMG':
|
|
431
|
+
transformImage.call(this, contet)
|
|
432
|
+
break
|
|
433
|
+
case 'IFRAME':
|
|
434
|
+
transformIframe.call(this, context)
|
|
435
|
+
break
|
|
436
|
+
case 'META':
|
|
437
|
+
transformMeta.call(this, context)
|
|
438
|
+
break
|
|
348
439
|
case 'TEMPLATE': // never touch templates!
|
|
349
440
|
break
|
|
350
441
|
default:
|
|
@@ -355,7 +446,8 @@ export function defaultFieldTransformer(context) {
|
|
|
355
446
|
return context
|
|
356
447
|
}
|
|
357
448
|
|
|
358
|
-
export function defaultListTransformer(context)
|
|
449
|
+
export function defaultListTransformer(context)
|
|
450
|
+
{
|
|
359
451
|
const el = context.element
|
|
360
452
|
const templates = context.templates
|
|
361
453
|
const templatesCount = templates.length
|
|
@@ -373,7 +465,8 @@ export function defaultListTransformer(context) {
|
|
|
373
465
|
return context
|
|
374
466
|
}
|
|
375
467
|
|
|
376
|
-
export function defaultMapTransformer(context)
|
|
468
|
+
export function defaultMapTransformer(context)
|
|
469
|
+
{
|
|
377
470
|
const el = context.element
|
|
378
471
|
const templates = context.templates
|
|
379
472
|
const templatesCount = templates.length
|
|
@@ -399,7 +492,8 @@ export function defaultMapTransformer(context) {
|
|
|
399
492
|
* FIXME: this doesn't handle situations where there is no matching template
|
|
400
493
|
* this messes up self healing. check transformObjectByTemplates for a better implementation
|
|
401
494
|
*/
|
|
402
|
-
export function transformArrayByTemplates(context)
|
|
495
|
+
export function transformArrayByTemplates(context)
|
|
496
|
+
{
|
|
403
497
|
const el = context.element
|
|
404
498
|
const templates = context.templates
|
|
405
499
|
const templatesCount = templates.length
|
|
@@ -476,7 +570,8 @@ export function transformArrayByTemplates(context) {
|
|
|
476
570
|
* Replaces,moves or removes existing DOM children if needed
|
|
477
571
|
* Reuses (doesn't touch) DOM children if template doesn't change
|
|
478
572
|
*/
|
|
479
|
-
export function transformObjectByTemplates(context)
|
|
573
|
+
export function transformObjectByTemplates(context)
|
|
574
|
+
{
|
|
480
575
|
const el = context.element
|
|
481
576
|
const templates = context.templates
|
|
482
577
|
const templatesCount = templates.length
|
|
@@ -521,7 +616,8 @@ export function transformObjectByTemplates(context) {
|
|
|
521
616
|
}
|
|
522
617
|
}
|
|
523
618
|
|
|
524
|
-
function getParentPath(el, attribute)
|
|
619
|
+
function getParentPath(el, attribute)
|
|
620
|
+
{
|
|
525
621
|
const parentEl = el.parentElement?.closest(`[${attribute}-list],[${attribute}-map]`)
|
|
526
622
|
if (!parentEl) {
|
|
527
623
|
return ':root'
|
|
@@ -538,7 +634,8 @@ function getParentPath(el, attribute) {
|
|
|
538
634
|
* data-bind attributes inside the template use the same
|
|
539
635
|
* parent path as this html element uses
|
|
540
636
|
*/
|
|
541
|
-
export function transformLiteralByTemplates(context)
|
|
637
|
+
export function transformLiteralByTemplates(context)
|
|
638
|
+
{
|
|
542
639
|
const el = context.element
|
|
543
640
|
const templates = context.templates
|
|
544
641
|
const value = context.value
|
|
@@ -568,12 +665,16 @@ export function transformLiteralByTemplates(context) {
|
|
|
568
665
|
* for radio/checkbox inputs it only sets the checked attribute to true/false
|
|
569
666
|
* if the value attribute matches the current value
|
|
570
667
|
* for other inputs the value attribute is updated
|
|
571
|
-
* FIXME: handle radio/checkboxes in separate transformer
|
|
572
668
|
*/
|
|
573
|
-
export function transformInput(context)
|
|
574
|
-
|
|
575
|
-
const
|
|
669
|
+
export function transformInput(context)
|
|
670
|
+
{
|
|
671
|
+
const el = context.element
|
|
672
|
+
let value = context.value
|
|
576
673
|
|
|
674
|
+
transformElement(context)
|
|
675
|
+
if (typeof value == 'undefined') {
|
|
676
|
+
value = ''
|
|
677
|
+
}
|
|
577
678
|
if (el.type=='checkbox' || el.type=='radio') {
|
|
578
679
|
if (matchValue(el.value, value)) {
|
|
579
680
|
el.checked = true
|
|
@@ -588,36 +689,80 @@ export function transformInput(context) {
|
|
|
588
689
|
/**
|
|
589
690
|
* Sets the value of the button, doesn't touch the innerHTML
|
|
590
691
|
*/
|
|
591
|
-
export function transformButton(context)
|
|
692
|
+
export function transformButton(context)
|
|
693
|
+
{
|
|
592
694
|
const el = context.element
|
|
593
695
|
const value = context.value
|
|
594
696
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
697
|
+
transformElement(context)
|
|
698
|
+
setProperties(el, value, 'value')
|
|
598
699
|
}
|
|
599
700
|
|
|
600
701
|
/**
|
|
601
702
|
* Sets the selected attribute of select options
|
|
602
703
|
*/
|
|
603
|
-
export function transformSelect(context)
|
|
604
|
-
|
|
605
|
-
const
|
|
704
|
+
export function transformSelect(context)
|
|
705
|
+
{
|
|
706
|
+
const el = context.element
|
|
707
|
+
let value = context.value
|
|
606
708
|
|
|
607
|
-
if (
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
709
|
+
if (value === null) {
|
|
710
|
+
value = ''
|
|
711
|
+
}
|
|
712
|
+
if (typeof value!='object') {
|
|
713
|
+
if (el.multiple) {
|
|
714
|
+
if (Array.isArray(value)) { //FIXME: cannot be true, since typeof != 'object'
|
|
715
|
+
for (let option of el.options) {
|
|
716
|
+
if (value.indexOf(option.value)===false) {
|
|
717
|
+
option.selected = false
|
|
718
|
+
} else {
|
|
719
|
+
option.selected = true
|
|
720
|
+
}
|
|
614
721
|
}
|
|
615
722
|
}
|
|
723
|
+
} else {
|
|
724
|
+
let option = el.options.find(o => matchValue(o.value,value))
|
|
725
|
+
if (option) {
|
|
726
|
+
option.selected = true
|
|
727
|
+
option.setAttribute('selected', true)
|
|
728
|
+
}
|
|
616
729
|
}
|
|
617
|
-
} else {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
730
|
+
} else { // value is a non-null object
|
|
731
|
+
if (value.options) {
|
|
732
|
+
setSelectOptions(el, value.options)
|
|
733
|
+
}
|
|
734
|
+
if (value.selected) {
|
|
735
|
+
transformSelect(Object.asssign({}, context, {value:value.selected}))
|
|
736
|
+
}
|
|
737
|
+
setProperties(el, value, 'name', 'id', 'selectedIndex', 'className') // allow innerHTML? if so call transformElement instead
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export function addOption(select, option)
|
|
742
|
+
{
|
|
743
|
+
if (!option) {
|
|
744
|
+
return
|
|
745
|
+
}
|
|
746
|
+
if (typeof option !== 'object') {
|
|
747
|
+
select.options.add(new Option(''+option))
|
|
748
|
+
} else if (option.text) {
|
|
749
|
+
select.options.add(new Option(option.text, option.value, option.defaultSelected, option.selected))
|
|
750
|
+
} else if (typeof option.value != 'undefined') {
|
|
751
|
+
select.options.add(new Option(''+option.value, option.value, option.defaultSelected, option.selected))
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export function setSelectOptions(select,options)
|
|
756
|
+
{
|
|
757
|
+
//@TODO: only update in case of changes?
|
|
758
|
+
select.innerHTML = ''
|
|
759
|
+
if (Array.isArray(options)) {
|
|
760
|
+
for (const option of options) {
|
|
761
|
+
addOption(select, option)
|
|
762
|
+
}
|
|
763
|
+
} else if (options && typeof options == 'object') {
|
|
764
|
+
for (const option in options) {
|
|
765
|
+
addOption(select, { text: options[option], value: option })
|
|
621
766
|
}
|
|
622
767
|
}
|
|
623
768
|
}
|
|
@@ -626,30 +771,80 @@ export function transformSelect(context) {
|
|
|
626
771
|
* Sets the innerHTML and href attribute of an anchor
|
|
627
772
|
* TODO: support target, title, etc. attributes
|
|
628
773
|
*/
|
|
629
|
-
export function transformAnchor(context)
|
|
774
|
+
export function transformAnchor(context)
|
|
775
|
+
{
|
|
630
776
|
const el = context.element
|
|
631
777
|
const value = context.value
|
|
632
778
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
if (value?.href && !matchValue(el.href,value.href)) {
|
|
637
|
-
el.href = ''+value.href
|
|
638
|
-
}
|
|
779
|
+
transformElement(context)
|
|
780
|
+
setProperties(el, value, 'title', 'target', 'href', 'name', 'newwindow', 'nofollow')
|
|
639
781
|
}
|
|
640
782
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
783
|
+
export function transformImage(context)
|
|
784
|
+
{
|
|
785
|
+
const el = context.element
|
|
786
|
+
const value = context.value
|
|
787
|
+
|
|
788
|
+
transformElement(context)
|
|
789
|
+
setProperties(el, value, 'title', 'alt', 'src')
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export function transformIframe(context)
|
|
793
|
+
{
|
|
645
794
|
const el = context.element
|
|
646
795
|
const value = context.value
|
|
647
796
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
797
|
+
transformElement(context)
|
|
798
|
+
setProperties(el, value, 'title', 'src')
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export function transformMeta(context)
|
|
802
|
+
{
|
|
803
|
+
const el = context.element
|
|
804
|
+
const value = context.value
|
|
805
|
+
|
|
806
|
+
transformElement(context)
|
|
807
|
+
setProperties(el, value, 'content')
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* sets the innerHTML and title and id properties of any HTML element
|
|
811
|
+
*/
|
|
812
|
+
export function transformElement(context)
|
|
813
|
+
{
|
|
814
|
+
const el = context.element
|
|
815
|
+
let value = context.value
|
|
816
|
+
|
|
817
|
+
if (typeof value=='undefined' || value==null) {
|
|
818
|
+
value = ''
|
|
819
|
+
}
|
|
820
|
+
let strValue = ''+value
|
|
821
|
+
if (typeof value!='object' || strValue.substring(0,8)!='[object ') {
|
|
822
|
+
el.innerHTML = strValue
|
|
823
|
+
return
|
|
824
|
+
}
|
|
825
|
+
setProperties(el, value, 'innerHTML', 'title', 'id', 'className')
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Sets a list of properties on a dom element, equal to
|
|
830
|
+
* the string value of a data object
|
|
831
|
+
* only updates the dom element if the property doesn't match
|
|
832
|
+
*/
|
|
833
|
+
export function setProperties(el, data, ...properties) {
|
|
834
|
+
if (!data || typeof data!=='object') {
|
|
835
|
+
return
|
|
836
|
+
}
|
|
837
|
+
for (const property of properties) {
|
|
838
|
+
if (typeof data[property] === 'undefined') {
|
|
839
|
+
continue
|
|
840
|
+
}
|
|
841
|
+
if (matchValue(el[property], data[property])) {
|
|
842
|
+
continue
|
|
843
|
+
}
|
|
844
|
+
if (data[property] === null) {
|
|
845
|
+
el[property] = ''
|
|
651
846
|
} else {
|
|
652
|
-
el
|
|
847
|
+
el[property] = ''+data[property]
|
|
653
848
|
}
|
|
654
849
|
}
|
|
655
|
-
}
|
|
850
|
+
}
|
package/src/model.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {signal, effect, batch} from './state.mjs'
|
|
1
|
+
import {signal, effect, throttledEffect, batch} from './state.mjs'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* This class implements a pluggable data model, where you can
|
|
@@ -84,13 +84,13 @@ export function sort(options={}) {
|
|
|
84
84
|
}, options);
|
|
85
85
|
// then return the effect, which is called when
|
|
86
86
|
// either the data or the sort options change
|
|
87
|
-
return
|
|
87
|
+
return throttledEffect(() => {
|
|
88
88
|
const sort = this.state.options.sort
|
|
89
89
|
if (sort?.sortBy && sort?.direction) {
|
|
90
90
|
return data.current.toSorted(sort?.sortFn)
|
|
91
91
|
}
|
|
92
92
|
return data.current
|
|
93
|
-
})
|
|
93
|
+
}, 50)
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -112,7 +112,7 @@ export function paging(options={}) {
|
|
|
112
112
|
pageSize: 20,
|
|
113
113
|
max: 1
|
|
114
114
|
}, options)
|
|
115
|
-
return
|
|
115
|
+
return throttledEffect(() => {
|
|
116
116
|
return batch(() => {
|
|
117
117
|
const paging = this.state.options.paging
|
|
118
118
|
if (!paging.pageSize) {
|
|
@@ -125,7 +125,7 @@ export function paging(options={}) {
|
|
|
125
125
|
const end = start + paging.pageSize
|
|
126
126
|
return data.current.slice(start, end)
|
|
127
127
|
})
|
|
128
|
-
})
|
|
128
|
+
}, 50)
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -149,12 +149,12 @@ export function filter(options) {
|
|
|
149
149
|
throw new Error('a filter with this name already exists on this model')
|
|
150
150
|
}
|
|
151
151
|
this.state.options[options.name] = options
|
|
152
|
-
return
|
|
152
|
+
return throttledEffect(() => {
|
|
153
153
|
if (this.state.options[options.name].enabled) {
|
|
154
154
|
return data.current.filter(this.state.options[options.name].matches.bind(this))
|
|
155
155
|
}
|
|
156
156
|
return data.current
|
|
157
|
-
})
|
|
157
|
+
}, 50)
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
@@ -176,7 +176,7 @@ export function columns(options={}) {
|
|
|
176
176
|
}
|
|
177
177
|
return function(data) {
|
|
178
178
|
this.state.options.columns = options
|
|
179
|
-
return
|
|
179
|
+
return throttledEffect(() => {
|
|
180
180
|
return data.current.map(input => {
|
|
181
181
|
let result = {}
|
|
182
182
|
for (let key of Object.keys(this.state.options.columns)) {
|
|
@@ -186,7 +186,7 @@ export function columns(options={}) {
|
|
|
186
186
|
}
|
|
187
187
|
return result
|
|
188
188
|
})
|
|
189
|
-
})
|
|
189
|
+
}, 50)
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
|
|
@@ -229,13 +229,13 @@ export function scroll(options) {
|
|
|
229
229
|
})
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
|
|
232
|
+
throttledEffect(() => {
|
|
233
233
|
scrollOptions.size = data.current.length * scrollOptions.rowHeight
|
|
234
234
|
scrollbar.style.height = scrollOptions.size + 'px'
|
|
235
|
-
})
|
|
235
|
+
}, 50)
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
return
|
|
238
|
+
return throttledEffect(() => {
|
|
239
239
|
if (scrollOptions.container) {
|
|
240
240
|
//TODO: add a resize listener so that if the size of the container
|
|
241
241
|
// changes, the rowCount is calculated again
|
|
@@ -252,6 +252,6 @@ export function scroll(options) {
|
|
|
252
252
|
start = end - scrollOptions.rowCount
|
|
253
253
|
}
|
|
254
254
|
return data.current.slice(start, end)
|
|
255
|
-
})
|
|
255
|
+
}, 50)
|
|
256
256
|
}
|
|
257
257
|
}
|