simplyflow 0.2.3 → 0.3.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/dist/simply.flow.js +191 -64
- 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 +244 -73
- package/src/model.mjs +13 -13
- package/src/state.mjs +13 -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,16 @@ 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
|
+
destroy(this.bindings.get(el))
|
|
64
|
+
// doing this here instead of in a mutationobserver
|
|
65
|
+
// allows an element to be temporary removed and then inserted
|
|
66
|
+
// without the binding having to be reset
|
|
67
|
+
return
|
|
68
|
+
}
|
|
38
69
|
const context = {
|
|
39
70
|
templates: el.querySelectorAll(':scope > template'),
|
|
40
71
|
attribute: getBindingAttribute(el)
|
|
@@ -43,7 +74,7 @@ class SimplyBind {
|
|
|
43
74
|
context.value = getValueByPath(this.options.root, context.path)
|
|
44
75
|
context.element = el
|
|
45
76
|
runTransformers(context)
|
|
46
|
-
},
|
|
77
|
+
}, 50))
|
|
47
78
|
}
|
|
48
79
|
|
|
49
80
|
// finds and runs applicable transformers
|
|
@@ -63,14 +94,18 @@ class SimplyBind {
|
|
|
63
94
|
transformers = this.options.defaultTransformers.map || []
|
|
64
95
|
break
|
|
65
96
|
}
|
|
66
|
-
if (context.element.
|
|
67
|
-
context.element.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
97
|
+
if (context.element.hasAttribute(transformAttribute)) {
|
|
98
|
+
context.element.getAttribute(transformAttribute)
|
|
99
|
+
.split(' ').filter(Boolean)
|
|
100
|
+
.forEach(t => {
|
|
101
|
+
if (this.options.transformers[t]) {
|
|
102
|
+
transformers.push(this.options.transformers[t])
|
|
103
|
+
} else {
|
|
104
|
+
console.warn('No transformer with name '+t+' configured', {cause:context.element})
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
} else {
|
|
108
|
+
console.log(context.element.outerHTML)
|
|
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
|
})
|
|
@@ -276,7 +333,8 @@ export function bind(options)
|
|
|
276
333
|
* Returns true if a matches b, either by having the
|
|
277
334
|
* same string value, or matching string :empty against a falsy value
|
|
278
335
|
*/
|
|
279
|
-
export function matchValue(a,b)
|
|
336
|
+
export function matchValue(a,b)
|
|
337
|
+
{
|
|
280
338
|
if (a==':empty' && !b) {
|
|
281
339
|
return true
|
|
282
340
|
}
|
|
@@ -290,10 +348,12 @@ export function matchValue(a,b) {
|
|
|
290
348
|
}
|
|
291
349
|
|
|
292
350
|
/**
|
|
293
|
-
* Returns the value by walking the given path
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
351
|
+
* Returns the value by walking the given path as a json pointer, starting at root
|
|
352
|
+
* if you have a property with a '.' in its name urlencode the '.', e.g: %46
|
|
353
|
+
*
|
|
354
|
+
* @param HTMLElement root
|
|
355
|
+
* @param string path e.g. 'foo.bar'
|
|
356
|
+
* @return mixed the value found by walking the path from the root object or undefined
|
|
297
357
|
*/
|
|
298
358
|
export function getValueByPath(root, path)
|
|
299
359
|
{
|
|
@@ -321,7 +381,8 @@ export function getValueByPath(root, path)
|
|
|
321
381
|
* Default transformer for data binding
|
|
322
382
|
* Will be used unless overriden in the SimplyBind options parameter
|
|
323
383
|
*/
|
|
324
|
-
export function defaultFieldTransformer(context)
|
|
384
|
+
export function defaultFieldTransformer(context)
|
|
385
|
+
{
|
|
325
386
|
const el = context.element
|
|
326
387
|
const templates = context.templates
|
|
327
388
|
const templatesCount = templates.length
|
|
@@ -345,6 +406,15 @@ export function defaultFieldTransformer(context) {
|
|
|
345
406
|
case 'A':
|
|
346
407
|
transformAnchor.call(this, context)
|
|
347
408
|
break
|
|
409
|
+
case 'IMG':
|
|
410
|
+
transformImage.call(this, contet)
|
|
411
|
+
break
|
|
412
|
+
case 'IFRAME':
|
|
413
|
+
transformIframe.call(this, context)
|
|
414
|
+
break
|
|
415
|
+
case 'META':
|
|
416
|
+
transformMeta.call(this, context)
|
|
417
|
+
break
|
|
348
418
|
case 'TEMPLATE': // never touch templates!
|
|
349
419
|
break
|
|
350
420
|
default:
|
|
@@ -355,7 +425,8 @@ export function defaultFieldTransformer(context) {
|
|
|
355
425
|
return context
|
|
356
426
|
}
|
|
357
427
|
|
|
358
|
-
export function defaultListTransformer(context)
|
|
428
|
+
export function defaultListTransformer(context)
|
|
429
|
+
{
|
|
359
430
|
const el = context.element
|
|
360
431
|
const templates = context.templates
|
|
361
432
|
const templatesCount = templates.length
|
|
@@ -373,7 +444,8 @@ export function defaultListTransformer(context) {
|
|
|
373
444
|
return context
|
|
374
445
|
}
|
|
375
446
|
|
|
376
|
-
export function defaultMapTransformer(context)
|
|
447
|
+
export function defaultMapTransformer(context)
|
|
448
|
+
{
|
|
377
449
|
const el = context.element
|
|
378
450
|
const templates = context.templates
|
|
379
451
|
const templatesCount = templates.length
|
|
@@ -399,7 +471,8 @@ export function defaultMapTransformer(context) {
|
|
|
399
471
|
* FIXME: this doesn't handle situations where there is no matching template
|
|
400
472
|
* this messes up self healing. check transformObjectByTemplates for a better implementation
|
|
401
473
|
*/
|
|
402
|
-
export function transformArrayByTemplates(context)
|
|
474
|
+
export function transformArrayByTemplates(context)
|
|
475
|
+
{
|
|
403
476
|
const el = context.element
|
|
404
477
|
const templates = context.templates
|
|
405
478
|
const templatesCount = templates.length
|
|
@@ -476,7 +549,8 @@ export function transformArrayByTemplates(context) {
|
|
|
476
549
|
* Replaces,moves or removes existing DOM children if needed
|
|
477
550
|
* Reuses (doesn't touch) DOM children if template doesn't change
|
|
478
551
|
*/
|
|
479
|
-
export function transformObjectByTemplates(context)
|
|
552
|
+
export function transformObjectByTemplates(context)
|
|
553
|
+
{
|
|
480
554
|
const el = context.element
|
|
481
555
|
const templates = context.templates
|
|
482
556
|
const templatesCount = templates.length
|
|
@@ -521,7 +595,8 @@ export function transformObjectByTemplates(context) {
|
|
|
521
595
|
}
|
|
522
596
|
}
|
|
523
597
|
|
|
524
|
-
function getParentPath(el, attribute)
|
|
598
|
+
function getParentPath(el, attribute)
|
|
599
|
+
{
|
|
525
600
|
const parentEl = el.parentElement?.closest(`[${attribute}-list],[${attribute}-map]`)
|
|
526
601
|
if (!parentEl) {
|
|
527
602
|
return ':root'
|
|
@@ -538,7 +613,8 @@ function getParentPath(el, attribute) {
|
|
|
538
613
|
* data-bind attributes inside the template use the same
|
|
539
614
|
* parent path as this html element uses
|
|
540
615
|
*/
|
|
541
|
-
export function transformLiteralByTemplates(context)
|
|
616
|
+
export function transformLiteralByTemplates(context)
|
|
617
|
+
{
|
|
542
618
|
const el = context.element
|
|
543
619
|
const templates = context.templates
|
|
544
620
|
const value = context.value
|
|
@@ -568,12 +644,16 @@ export function transformLiteralByTemplates(context) {
|
|
|
568
644
|
* for radio/checkbox inputs it only sets the checked attribute to true/false
|
|
569
645
|
* if the value attribute matches the current value
|
|
570
646
|
* for other inputs the value attribute is updated
|
|
571
|
-
* FIXME: handle radio/checkboxes in separate transformer
|
|
572
647
|
*/
|
|
573
|
-
export function transformInput(context)
|
|
574
|
-
|
|
575
|
-
const
|
|
648
|
+
export function transformInput(context)
|
|
649
|
+
{
|
|
650
|
+
const el = context.element
|
|
651
|
+
let value = context.value
|
|
576
652
|
|
|
653
|
+
transformElement(context)
|
|
654
|
+
if (typeof value == 'undefined') {
|
|
655
|
+
value = ''
|
|
656
|
+
}
|
|
577
657
|
if (el.type=='checkbox' || el.type=='radio') {
|
|
578
658
|
if (matchValue(el.value, value)) {
|
|
579
659
|
el.checked = true
|
|
@@ -588,36 +668,80 @@ export function transformInput(context) {
|
|
|
588
668
|
/**
|
|
589
669
|
* Sets the value of the button, doesn't touch the innerHTML
|
|
590
670
|
*/
|
|
591
|
-
export function transformButton(context)
|
|
671
|
+
export function transformButton(context)
|
|
672
|
+
{
|
|
592
673
|
const el = context.element
|
|
593
674
|
const value = context.value
|
|
594
675
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
676
|
+
transformElement(context)
|
|
677
|
+
setProperties(el, value, 'value')
|
|
598
678
|
}
|
|
599
679
|
|
|
600
680
|
/**
|
|
601
681
|
* Sets the selected attribute of select options
|
|
602
682
|
*/
|
|
603
|
-
export function transformSelect(context)
|
|
604
|
-
|
|
605
|
-
const
|
|
683
|
+
export function transformSelect(context)
|
|
684
|
+
{
|
|
685
|
+
const el = context.element
|
|
686
|
+
let value = context.value
|
|
606
687
|
|
|
607
|
-
if (
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
688
|
+
if (value === null) {
|
|
689
|
+
value = ''
|
|
690
|
+
}
|
|
691
|
+
if (typeof value!='object') {
|
|
692
|
+
if (el.multiple) {
|
|
693
|
+
if (Array.isArray(value)) {
|
|
694
|
+
for (let option of el.options) {
|
|
695
|
+
if (value.indexOf(option.value)===false) {
|
|
696
|
+
option.selected = false
|
|
697
|
+
} else {
|
|
698
|
+
option.selected = true
|
|
699
|
+
}
|
|
614
700
|
}
|
|
615
701
|
}
|
|
702
|
+
} else {
|
|
703
|
+
let option = el.options.find(o => matchValue(o.value,value))
|
|
704
|
+
if (option) {
|
|
705
|
+
option.selected = true
|
|
706
|
+
option.setAttribute('selected', true)
|
|
707
|
+
}
|
|
616
708
|
}
|
|
617
|
-
} else {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
709
|
+
} else { // value is a non-null object
|
|
710
|
+
if (value.options) {
|
|
711
|
+
setSelectOptions(el, value.options)
|
|
712
|
+
}
|
|
713
|
+
if (value.selected) {
|
|
714
|
+
transformSelect(Object.asssign({}, context, {value:value.selected}))
|
|
715
|
+
}
|
|
716
|
+
setProperties(el, value, 'name', 'id', 'selectedIndex', 'className') // allow innerHTML? if so call transformElement instead
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export function addOption(select, option)
|
|
721
|
+
{
|
|
722
|
+
if (!option) {
|
|
723
|
+
return
|
|
724
|
+
}
|
|
725
|
+
if (typeof option !== 'object') {
|
|
726
|
+
select.options.add(new Option(''+option))
|
|
727
|
+
} else if (option.text) {
|
|
728
|
+
select.options.add(new Option(option.text, option.value, option.defaultSelected, option.selected))
|
|
729
|
+
} else if (typeof option.value != 'undefined') {
|
|
730
|
+
select.options.add(new Option(''+option.value, option.value, option.defaultSelected, option.selected))
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export function setSelectOptions(select,options)
|
|
735
|
+
{
|
|
736
|
+
//@TODO: only update in case of changes?
|
|
737
|
+
select.innerHTML = ''
|
|
738
|
+
if (Array.isArray(options)) {
|
|
739
|
+
for (const option of options) {
|
|
740
|
+
addOption(select, option)
|
|
741
|
+
}
|
|
742
|
+
} else if (options && typeof options == 'object') {
|
|
743
|
+
for (const option in options) {
|
|
744
|
+
addOption(select, { text: options[option], value: option })
|
|
621
745
|
}
|
|
622
746
|
}
|
|
623
747
|
}
|
|
@@ -626,30 +750,77 @@ export function transformSelect(context) {
|
|
|
626
750
|
* Sets the innerHTML and href attribute of an anchor
|
|
627
751
|
* TODO: support target, title, etc. attributes
|
|
628
752
|
*/
|
|
629
|
-
export function transformAnchor(context)
|
|
753
|
+
export function transformAnchor(context)
|
|
754
|
+
{
|
|
630
755
|
const el = context.element
|
|
631
756
|
const value = context.value
|
|
632
757
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
if (value?.href && !matchValue(el.href,value.href)) {
|
|
637
|
-
el.href = ''+value.href
|
|
638
|
-
}
|
|
758
|
+
transformElement(context)
|
|
759
|
+
setProperties(el, value, 'title', 'target', 'href', 'name', 'newwindow', 'nofollow')
|
|
639
760
|
}
|
|
640
761
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
*/
|
|
644
|
-
export function transformElement(context) {
|
|
762
|
+
export function transformImage(context)
|
|
763
|
+
{
|
|
645
764
|
const el = context.element
|
|
646
765
|
const value = context.value
|
|
647
766
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
767
|
+
transformElement(context)
|
|
768
|
+
setProperties(el, value, 'title', 'alt', 'src')
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
export function transformIframe(context)
|
|
772
|
+
{
|
|
773
|
+
const el = context.element
|
|
774
|
+
const value = context.value
|
|
775
|
+
|
|
776
|
+
transformElement(context)
|
|
777
|
+
setProperties(el, value, 'title', 'src')
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export function transformMeta(context)
|
|
781
|
+
{
|
|
782
|
+
const el = context.element
|
|
783
|
+
const value = context.value
|
|
784
|
+
|
|
785
|
+
transformElement(context)
|
|
786
|
+
setProperties(el, value, 'content')
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* sets the innerHTML and title and id properties of any HTML element
|
|
790
|
+
*/
|
|
791
|
+
export function transformElement(context)
|
|
792
|
+
{
|
|
793
|
+
const el = context.element
|
|
794
|
+
let value = context.value
|
|
795
|
+
|
|
796
|
+
if (typeof value=='undefined' || value==null) {
|
|
797
|
+
value = ''
|
|
798
|
+
}
|
|
799
|
+
if (typeof value == 'string') {
|
|
800
|
+
el.innerHTML = ''+value
|
|
801
|
+
return
|
|
802
|
+
}
|
|
803
|
+
setProperties(el, value, 'innerHTML', 'title', 'id', 'className')
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Sets a list of properties on a dom element, equal to
|
|
808
|
+
* the string value of a data object
|
|
809
|
+
* only updates the dom element if the property doesn't match
|
|
810
|
+
*/
|
|
811
|
+
export function setProperties(el, data, ...properties) {
|
|
812
|
+
if (!data || typeof data!=='object') {
|
|
813
|
+
return
|
|
814
|
+
}
|
|
815
|
+
for (const property of properties) {
|
|
816
|
+
if (typeof data[property] !== 'undefined') {
|
|
817
|
+
if (!matchValue(el[property], data[property])) {
|
|
818
|
+
if (data[property] === null) {
|
|
819
|
+
el[property] = ''
|
|
820
|
+
} else {
|
|
821
|
+
el[property] = ''+data[property]
|
|
822
|
+
}
|
|
823
|
+
}
|
|
653
824
|
}
|
|
654
825
|
}
|
|
655
|
-
}
|
|
826
|
+
}
|
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
|
}
|
package/src/state.mjs
CHANGED
|
@@ -2,12 +2,18 @@ const iterate = Symbol('iterate')
|
|
|
2
2
|
if (!Symbol.xRay) {
|
|
3
3
|
Symbol.xRay = Symbol('xRay')
|
|
4
4
|
}
|
|
5
|
+
if (!Symbol.Signal) {
|
|
6
|
+
Symbol.Signal = Symbol('Signal')
|
|
7
|
+
}
|
|
5
8
|
|
|
6
9
|
const signalHandler = {
|
|
7
10
|
get: (target, property, receiver) => {
|
|
8
11
|
if (property===Symbol.xRay) {
|
|
9
12
|
return target // don't notifyGet here, this is only called by set
|
|
10
13
|
}
|
|
14
|
+
if (property===Symbol.Signal) {
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
11
17
|
const value = target?.[property] // Reflect.get fails on a Set.
|
|
12
18
|
notifyGet(receiver, property)
|
|
13
19
|
if (typeof value === 'function') {
|
|
@@ -116,7 +122,13 @@ const signals = new WeakMap()
|
|
|
116
122
|
* to allow reactive functions to be triggered when signal values change.
|
|
117
123
|
*/
|
|
118
124
|
export function signal(v) {
|
|
119
|
-
if (
|
|
125
|
+
if (v[Symbol.Signal]) { // avoid wrapping a Signal inside a Signal
|
|
126
|
+
let target = v[Symbol.xRay]
|
|
127
|
+
if (!signals.has(target)) {
|
|
128
|
+
signals.set(target, v)
|
|
129
|
+
}
|
|
130
|
+
v = target
|
|
131
|
+
} else if (!signals.has(v)) {
|
|
120
132
|
signals.set(v, new Proxy(v, signalHandler))
|
|
121
133
|
}
|
|
122
134
|
return signals.get(v)
|