simplyview 3.0.6 → 3.1.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/src/model.mjs DELETED
@@ -1,151 +0,0 @@
1
- import {signal, effect, batch} from './state.mjs'
2
-
3
- /**
4
- * This class implements a pluggable data model, where you can
5
- * add effects that are run only when either an option for that
6
- * effect changes, or when an effect earlier in the chain of
7
- * effects changes.
8
- */
9
- class SimplyModel {
10
-
11
- /**
12
- * Creates a new datamodel, with a state property that contains
13
- * all the data passed to this constructor
14
- * @param state Object with all the data for this model
15
- */
16
- constructor(state) {
17
- this.state = signal(state)
18
- if (!this.state.options) {
19
- this.state.options = {}
20
- }
21
- this.effects = [{current:state.data}]
22
- this.view = signal(state.data)
23
- }
24
-
25
- /**
26
- * Adds an effect to run whenever a signal it depends on
27
- * changes. this.state is the usual signal.
28
- * The `fn` function param is not itself an effect, but must return
29
- * and effect function. `fn` takes one param, which is the data signal.
30
- * This signal will always have at least a `current` property.
31
- * The result of the effect function is pushed on to the this.effects
32
- * list. And the last effect added is set as this.view
33
- */
34
- addEffect(fn) {
35
- const dataSignal = this.effects[this.effects.length-1]
36
- this.view = fn.call(this, dataSignal)
37
- this.effects.push(this.view)
38
- }
39
- }
40
-
41
- export function model(options) {
42
- return new SimplyModel(options)
43
- }
44
-
45
- export function sort(options={}) {
46
- return function(data) {
47
- // initialize the sort options, only gets called once
48
- this.state.options.sort = Object.assign({
49
- direction: 'asc',
50
- sortBy: null,
51
- sortFn: ((a,b) => {
52
- const sort = this.state.options.sort
53
- const sortBy = sort.sortBy
54
- if (!sort.sortBy) {
55
- return 0
56
- }
57
- const larger = sort.direction == 'asc' ? 1 : -1
58
- const smaller = sort.direction == 'asc' ? -1 : 1
59
- if (typeof a?.[sortBy] === 'undefined') {
60
- if (typeof b?.[sortBy] === 'undefined') {
61
- return 0
62
- }
63
- return larger
64
- }
65
- if (typeof b?.[sortBy] === 'undefined') {
66
- return smaller
67
- }
68
- if (a[sortBy]<b[sortBy]) {
69
- return smaller
70
- } else if (a[sortBy]>b[sortBy]) {
71
- return larger
72
- } else {
73
- return 0
74
- }
75
- })
76
- }, options);
77
- // then return the effect, which is called when
78
- // either the data or the sort options change
79
- return effect(() => {
80
- const sort = this.state.options.sort
81
- if (sort?.sortBy && sort?.direction) {
82
- return data.current.toSorted(sort?.sortFn)
83
- }
84
- return data.current
85
- })
86
- }
87
- }
88
-
89
- export function paging(options={}) {
90
- return function(data) {
91
- // initialize the paging options
92
- this.state.options.paging = Object.assign({
93
- page: 1,
94
- pageSize: 20,
95
- max: 1
96
- }, options)
97
- return effect(() => {
98
- return batch(() => {
99
- const paging = this.state.options.paging
100
- if (!paging.pageSize) {
101
- paging.pageSize = 20
102
- }
103
- paging.max = Math.ceil(this.state.data.length / paging.pageSize)
104
- paging.page = Math.max(1, Math.min(paging.max, paging.page))
105
-
106
- const start = (paging.page-1) * paging.pageSize
107
- const end = start + paging.pageSize
108
- return data.current.slice(start, end)
109
- })
110
- })
111
- }
112
- }
113
-
114
- export function filter(options) {
115
- if (!options?.name || typeof options.name!=='string') {
116
- throw new Error('filter requires options.name to be a string')
117
- }
118
- if (!options.matches || typeof options.matches!=='function') {
119
- throw new Error('filter requires options.matches to be a function')
120
- }
121
- return function(data) {
122
- this.state.options[options.name] = options
123
- return effect(() => {
124
- if (this.state.options[options.name].enabled) {
125
- return data.filter(this.state.options.matches)
126
- }
127
- })
128
- }
129
- }
130
-
131
- export function columns(options={}) {
132
- if (!options
133
- || typeof options!=='object'
134
- || Object.keys(options).length===0) {
135
- throw new Error('columns requires options to be an object with at least one property')
136
- }
137
- return function(data) {
138
- this.state.options.columns = options
139
- return effect(() => {
140
- return data.current.map(input => {
141
- let result = {}
142
- for (let key of Object.keys(this.state.options.columns)) {
143
- if (!this.state.options.columns[key].hidden) {
144
- result[key] = input[key]
145
- }
146
- }
147
- return result
148
- })
149
- })
150
- }
151
- }
package/src/state.mjs DELETED
@@ -1,536 +0,0 @@
1
- const iterate = Symbol('iterate')
2
- if (!Symbol.xRay) {
3
- Symbol.xRay = Symbol('xRay')
4
- }
5
-
6
- const signalHandler = {
7
- get: (target, property, receiver) => {
8
- if (property===Symbol.xRay) {
9
- return target // don't notifyGet here, this is only called by set
10
- }
11
- const value = target?.[property] // Reflect.get fails on a Set.
12
- notifyGet(receiver, property)
13
- if (typeof value === 'function') {
14
- if (Array.isArray(target)) {
15
- return (...args) => {
16
- let l = target.length
17
- // by binding the function to the receiver
18
- // all accesses in the function will be trapped
19
- // by the Proxy, so get/set/delete is all handled
20
- let result = value.apply(receiver, args)
21
- if (l != target.length) {
22
- notifySet(receiver, makeContext('length', { was: l, now: target.length }) )
23
- }
24
- return result
25
- }
26
- } else if (target instanceof Set || target instanceof Map) {
27
- return (...args) => {
28
- // node doesn't allow you to call set/map functions
29
- // bound to the receiver.. so using target instead
30
- // there are no properties to update anyway, except for size
31
- let s = target.size
32
- let result = value.apply(target, args)
33
- if (s != target.size) {
34
- notifySet(receiver, makeContext( 'size', { was: s, now: target.size }) )
35
- }
36
- // there is no efficient way to see if the function called
37
- // has actually changed the Set/Map, but by assuming the
38
- // 'setter' functions will change the results of the
39
- // 'getter' functions, effects should update correctly
40
- if (['set','add','clear','delete'].includes(property)) {
41
- notifySet(receiver, makeContext( { entries: {}, forEach: {}, has: {}, keys: {}, values: {}, [Symbol.iterator]: {} } ) )
42
- }
43
- return result
44
- }
45
- } else if (
46
- target instanceof HTMLElement
47
- || target instanceof Number
48
- || target instanceof String
49
- || target instanceof Boolean
50
- ) {
51
- return value.bind(target)
52
- } else {
53
- // support custom classes, hopefully
54
- return value.bind(receiver)
55
- }
56
- }
57
- if (value && typeof value == 'object') {
58
- //NOTE: get now returns a signal, set doesn't 'unsignal' the value set
59
- return signal(value)
60
- }
61
- return value
62
- },
63
- set: (target, property, value, receiver) => {
64
- value = value?.[Symbol.xRay] || value // unwraps signal
65
- let current = target[property]
66
- if (current!==value) {
67
- target[property] = value
68
- notifySet(receiver, makeContext(property, { was: current, now: value } ) )
69
- }
70
- if (typeof current === 'undefined') {
71
- notifySet(receiver, makeContext(iterate, {}))
72
- }
73
- return true
74
- },
75
- has: (target, property) => { // receiver is not part of the has() call
76
- let receiver = signals.get(target) // so retrieve it here
77
- if (receiver) {
78
- notifyGet(receiver, property)
79
- }
80
- return Object.hasOwn(target, property)
81
- },
82
- deleteProperty: (target, property) => {
83
- if (typeof target[property] !== 'undefined') {
84
- let current = target[property]
85
- delete target[property]
86
- let receiver = signals.get(target) // receiver is not part of the trap arguments, so retrieve it here
87
- notifySet(receiver, makeContext(property,{ delete: true, was: current }))
88
- }
89
- return true
90
- },
91
- defineProperty: (target, property, descriptor) => {
92
- if (typeof target[property] === 'undefined') {
93
- let receiver = signals.get(target) // receiver is not part of the trap arguments, so retrieve it here
94
- notifySet(receiver, makeContext(iterate, {}))
95
- }
96
- return Object.defineProperty(target, property, descriptor)
97
- },
98
- ownKeys: (target) => {
99
- let receiver = signals.get(target) // receiver is not part of the trap arguments, so retrieve it here
100
- notifyGet(receiver, iterate)
101
- return Reflect.ownKeys(target)
102
- }
103
-
104
- }
105
-
106
- /**
107
- * Keeps track of the return signal for an update function, as well
108
- * as signals connected to other objects.
109
- * Makes sure that a given object or function always uses the same
110
- * signal
111
- */
112
- const signals = new WeakMap()
113
-
114
- /**
115
- * Creates a new signal proxy of the given object, that intercepts get/has and set/delete
116
- * to allow reactive functions to be triggered when signal values change.
117
- */
118
- export function signal(v) {
119
- if (!signals.has(v)) {
120
- signals.set(v, new Proxy(v, signalHandler))
121
- }
122
- return signals.get(v)
123
- }
124
-
125
- let batchedListeners = new Set()
126
- let batchMode = 0
127
- /**
128
- * Called when a signal changes a property (set/delete)
129
- * Triggers any reactor function that depends on this signal
130
- * to re-compute its values
131
- */
132
- function notifySet(self, context={}) {
133
- let listeners = []
134
- context.forEach((change, property) => {
135
- let propListeners = getListeners(self, property)
136
- if (propListeners?.length) {
137
- for (let listener of propListeners) {
138
- addContext(listener, makeContext(property,change))
139
- }
140
- listeners = listeners.concat(propListeners)
141
- }
142
- })
143
- listeners = new Set(listeners.filter(Boolean))
144
- if (listeners) {
145
- if (batchMode) {
146
- batchedListeners = batchedListeners.union(listeners)
147
- } else {
148
- const currentEffect = computeStack[computeStack.length-1]
149
- for (let listener of Array.from(listeners)) {
150
- if (listener!=currentEffect && listener?.needsUpdate) {
151
- listener()
152
- }
153
- clearContext(listener)
154
- }
155
- }
156
- }
157
- }
158
-
159
- function makeContext(property, change) {
160
- let context = new Map()
161
- if (typeof property === 'object') {
162
- for (let prop in property) {
163
- context.set(prop, property[prop])
164
- }
165
- } else {
166
- context.set(property, change)
167
- }
168
- return context
169
- }
170
-
171
- function addContext(listener, context) {
172
- if (!listener.context) {
173
- listener.context = context
174
- } else {
175
- context.forEach((change,property)=> {
176
- listener.context.set(property, change) // TODO: merge change if needed
177
- })
178
- }
179
- listener.needsUpdate = true
180
- }
181
-
182
- function clearContext(listener) {
183
- delete listener.context
184
- delete listener.needsUpdate
185
- }
186
-
187
- /**
188
- * Called when a signal property is accessed. If this happens
189
- * inside a reactor function--computeStack is not empty--
190
- * then it adds the current reactor (top of this stack) to its
191
- * listeners. These are later called if this property changes
192
- */
193
- function notifyGet(self, property) {
194
- let currentCompute = computeStack[computeStack.length-1]
195
- if (currentCompute) {
196
- // get was part of a react() function, so add it
197
- setListeners(self, property, currentCompute)
198
- }
199
- }
200
-
201
- /**
202
- * Keeps track of which update() functions are dependent on which
203
- * signal objects and which properties. Maps signals to update fns
204
- */
205
- const listenersMap = new WeakMap()
206
-
207
- /**
208
- * Keeps track of which signals and properties are linked to which
209
- * update functions. Maps update functions and properties to signals
210
- */
211
- const computeMap = new WeakMap()
212
-
213
- /**
214
- * Returns the update functions for a given signal and property
215
- */
216
- function getListeners(self, property) {
217
- let listeners = listenersMap.get(self)
218
- return listeners ? Array.from(listeners.get(property) || []) : []
219
- }
220
-
221
- /**
222
- * Adds an update function (compute) to the list of listeners on
223
- * the given signal (self) and property
224
- */
225
- function setListeners(self, property, compute) {
226
- if (!listenersMap.has(self)) {
227
- listenersMap.set(self, new Map())
228
- }
229
- let listeners = listenersMap.get(self)
230
- if (!listeners.has(property)) {
231
- listeners.set(property, new Set())
232
- }
233
- listeners.get(property).add(compute)
234
-
235
- if (!computeMap.has(compute)) {
236
- computeMap.set(compute, new Map())
237
- }
238
- let connectedSignals = computeMap.get(compute)
239
- if (!connectedSignals.has(property)) {
240
- connectedSignals.set(property, new Set)
241
- }
242
- connectedSignals.get(property).add(self)
243
- }
244
-
245
- /**
246
- * Removes alle listeners that trigger the given reactor function (compute)
247
- * This happens when a reactor is called, so that it can set new listeners
248
- * based on the current call (code path)
249
- */
250
- function clearListeners(compute) {
251
- let connectedSignals = computeMap.get(compute)
252
- if (connectedSignals) {
253
- connectedSignals.forEach(property => {
254
- property.forEach(s => {
255
- let listeners = listenersMap.get(s)
256
- if (listeners.has(property)) {
257
- listeners.get(property).delete(compute)
258
- }
259
- })
260
- })
261
- }
262
- }
263
-
264
- /**
265
- * The top most entry is the currently running update function, used
266
- * to automatically record signals used in an update function.
267
- */
268
- let computeStack = []
269
-
270
- /**
271
- * Used for cycle detection: effectStack contains all running effect
272
- * functions. If the same function appears twice in this stack, there
273
- * is a recursive update call, which would cause an infinite loop.
274
- */
275
- const effectStack = []
276
-
277
- const effectMap = new WeakMap()
278
- /**
279
- * Used for cycle detection: signalStack contains all used signals.
280
- * If the same signal appears more than once, there is a cyclical
281
- * dependency between signals, which would cause an infinite loop.
282
- */
283
- const signalStack = []
284
-
285
- /**
286
- * Runs the given function at once, and then whenever a signal changes that
287
- * is used by the given function (or at least signals used in the previous run).
288
- */
289
- export function effect(fn) {
290
- if (effectStack.findIndex(f => fn==f)!==-1) {
291
- throw new Error('Recursive update() call', {cause:fn})
292
- }
293
- effectStack.push(fn)
294
-
295
- let connectedSignal = signals.get(fn)
296
- if (!connectedSignal) {
297
- connectedSignal = signal({
298
- current: null
299
- })
300
- signals.set(fn, connectedSignal)
301
- }
302
-
303
- // this is the function that is called automatically
304
- // whenever a signal dependency changes
305
- const computeEffect = function computeEffect() {
306
- if (signalStack.findIndex(s => s==connectedSignal)!==-1) {
307
- throw new Error('Cyclical dependency in update() call', { cause: fn})
308
- }
309
- // remove all dependencies (signals) from previous runs
310
- clearListeners(computeEffect)
311
- // record new dependencies on this run
312
- computeStack.push(computeEffect)
313
- // prevent recursion
314
- signalStack.push(connectedSignal)
315
- // call the actual update function
316
- let result
317
- try {
318
- result = fn(computeEffect, computeStack, signalStack)
319
- } finally {
320
- // stop recording dependencies
321
- computeStack.pop()
322
- // stop the recursion prevention
323
- signalStack.pop()
324
- if (result instanceof Promise) {
325
- result.then((result) => {
326
- connectedSignal.current = result
327
- })
328
- } else {
329
- connectedSignal.current = result
330
- }
331
- }
332
- }
333
- computeEffect.fn = fn
334
- effectMap.set(connectedSignal, computeEffect)
335
-
336
- // run the computEffect immediately upon creation
337
- computeEffect()
338
- return connectedSignal
339
- }
340
-
341
-
342
- export function destroy(connectedSignal) {
343
- // find the computeEffect associated with this signal
344
- const computeEffect = effectMap.get(connectedSignal)?.deref()
345
- if (!computeEffect) {
346
- return
347
- }
348
-
349
- // remove all listeners for this effect
350
- clearListeners(computeEffect)
351
-
352
- // remove all references to connectedSignal
353
- let fn = computeEffect.fn
354
- signals.remove(fn)
355
-
356
- effectMap.delete(connectedSignal)
357
-
358
- // if no other references to connectedSignal exist, it will be garbage collected
359
- }
360
-
361
- /**
362
- * Inside a batch() call, any changes to signals do not trigger effects
363
- * immediately. Instead, immediately after finishing the batch() call,
364
- * these effects will be called. Effects that are triggered by multiple
365
- * signals are called only once.
366
- * @param Function fn batch() calls this function immediately
367
- * @result mixed the result of the fn() function call
368
- */
369
- export function batch(fn) {
370
- batchMode++
371
- let result
372
- try {
373
- result = fn()
374
- } finally {
375
- if (result instanceof Promise) {
376
- result.then(() => {
377
- batchMode--
378
- if (!batchMode) {
379
- runBatchedListeners()
380
- }
381
- })
382
- } else {
383
- batchMode--
384
- if (!batchMode) {
385
- runBatchedListeners()
386
- }
387
- }
388
- }
389
- return result
390
- }
391
-
392
- function runBatchedListeners() {
393
- let copyBatchedListeners = Array.from(batchedListeners)
394
- batchedListeners = new Set()
395
- const currentEffect = computeStack[computeStack.length-1]
396
- for (let listener of copyBatchedListeners) {
397
- if (listener!=currentEffect && listener?.needsUpdate) {
398
- listener()
399
- }
400
- clearContext(listener)
401
- }
402
- }
403
-
404
- /**
405
- * A throttledEffect is run immediately once. And then only once
406
- * per throttleTime (in ms).
407
- * @param Function fn the effect function to run whenever a signal changes
408
- * @param int throttleTime in ms
409
- * @returns signal with the result of the effect function fn
410
- */
411
- export function throttledEffect(fn, throttleTime) {
412
- if (effectStack.findIndex(f => fn==f)!==-1) {
413
- throw new Error('Recursive update() call', {cause:fn})
414
- }
415
- effectStack.push(fn)
416
-
417
- let connectedSignal = signals.get(fn)
418
- if (!connectedSignal) {
419
- connectedSignal = signal({
420
- current: null
421
- })
422
- signals.set(fn, connectedSignal)
423
- }
424
-
425
- let throttled = false
426
- let hasChange = true
427
- // this is the function that is called automatically
428
- // whenever a signal dependency changes
429
- const computeEffect = function computeEffect() {
430
- if (signalStack.findIndex(s => s==connectedSignal)!==-1) {
431
- throw new Error('Cyclical dependency in update() call', { cause: fn})
432
- }
433
- if (throttled && throttled>Date.now()) {
434
- hasChange = true
435
- return
436
- }
437
- // remove all dependencies (signals) from previous runs
438
- clearListeners(computeEffect)
439
- // record new dependencies on this run
440
- computeStack.push(computeEffect)
441
- // prevent recursion
442
- signalStack.push(connectedSignal)
443
- // call the actual update function
444
- let result
445
- try {
446
- result = fn(computeEffect, computeStack, signalStack)
447
- } finally {
448
- hasChange = false
449
- // stop recording dependencies
450
- computeStack.pop()
451
- // stop the recursion prevention
452
- signalStack.pop()
453
- if (result instanceof Promise) {
454
- result.then((result) => {
455
- connectedSignal.current = result
456
- })
457
- } else {
458
- connectedSignal.current = result
459
- }
460
- }
461
- throttled = Date.now()+throttleTime
462
- globalThis.setTimeout(() => {
463
- if (hasChange) {
464
- computeEffect()
465
- }
466
- }, throttleTime)
467
- }
468
- // run the computEffect immediately upon creation
469
- computeEffect()
470
- return connectedSignal
471
- }
472
-
473
- // refactor: Class clock() with an effect() method
474
- // keep track of effects per clock, and add clock property to the effect function
475
- // on notifySet add clock.effects to clock.needsUpdate list
476
- // on clock.tick() (or clock.time++) run only the clock.needsUpdate effects
477
- // (first create a copy and reset clock.needsUpdate, then run effects)
478
- export function clockEffect(fn, clock) {
479
- let connectedSignal = signals.get(fn)
480
- if (!connectedSignal) {
481
- connectedSignal = signal({
482
- current: null
483
- })
484
- signals.set(fn, connectedSignal)
485
- }
486
-
487
- let lastTick = -1 // clock.time should start at 0 or larger
488
- let hasChanged = true // make sure the first run goes through
489
- // this is the function that is called automatically
490
- // whenever a signal dependency changes
491
- const computeEffect = function computeEffect() {
492
- if (lastTick < clock.time) {
493
- if (hasChanged) {
494
- // remove all dependencies (signals) from previous runs
495
- clearListeners(computeEffect)
496
- // record new dependencies on this run
497
- computeStack.push(computeEffect)
498
- // make sure the clock.time signal is a dependency
499
- lastTick = clock.time
500
- // call the actual update function
501
- let result
502
- try {
503
- result = fn(computeEffect, computeStack)
504
- } finally {
505
- // stop recording dependencies
506
- computeStack.pop()
507
- if (result instanceof Promise) {
508
- result.then((result) => {
509
- connectedSignal.current = result
510
- })
511
- } else {
512
- connectedSignal.current = result
513
- }
514
- hasChanged = false
515
- }
516
- } else {
517
- lastTick = clock.time
518
- }
519
- } else {
520
- hasChanged = true
521
- }
522
- }
523
- // run the computEffect immediately upon creation
524
- computeEffect()
525
- return connectedSignal
526
- }
527
-
528
- export function untracked(fn) {
529
- const remember = computeStack.slice()
530
- computeStack = []
531
- try {
532
- return fn()
533
- } finally {
534
- computeStack = remember
535
- }
536
- }