sygnal 2.9.3 → 3.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/dist/index.esm.js CHANGED
@@ -3144,10 +3144,11 @@ var _default$2 = debounce$1.default = debounce;
3144
3144
  const ENVIRONMENT = ((typeof window != 'undefined' && window) || (process && process.env)) || {};
3145
3145
 
3146
3146
 
3147
- const REQUEST_SELECTOR_METHOD = 'request';
3148
- const BOOTSTRAP_ACTION = 'BOOTSTRAP';
3149
- const INITIALIZE_ACTION = 'INITIALIZE';
3150
- const HYDRATE_ACTION = 'HYDRATE';
3147
+ const BOOTSTRAP_ACTION = 'BOOTSTRAP';
3148
+ const INITIALIZE_ACTION = 'INITIALIZE';
3149
+ const HYDRATE_ACTION = 'HYDRATE';
3150
+ const PARENT_SINK_NAME = 'PARENT';
3151
+ const CHILD_SOURCE_NAME = 'CHILD';
3151
3152
 
3152
3153
 
3153
3154
  let COMPONENT_COUNT = 0;
@@ -3158,7 +3159,7 @@ const ABORT = '~#~#~ABORT~#~#~';
3158
3159
  function component (opts) {
3159
3160
  const { name, sources, isolateOpts, stateSourceName='STATE' } = opts;
3160
3161
 
3161
- if (sources && typeof sources !== 'object') {
3162
+ if (sources && !isObj(sources)) {
3162
3163
  throw new Error('Sources must be a Cycle.js sources object:', name)
3163
3164
  }
3164
3165
 
@@ -3176,7 +3177,7 @@ function component (opts) {
3176
3177
  const currySources = typeof sources === 'undefined';
3177
3178
  let returnFunction;
3178
3179
 
3179
- if (typeof fixedIsolateOpts == 'object') {
3180
+ if (isObj(fixedIsolateOpts)) {
3180
3181
  const wrapped = (sources) => {
3181
3182
  const fixedOpts = { ...opts, sources };
3182
3183
  return (new Component(fixedOpts)).sinks
@@ -3201,12 +3202,10 @@ class Component {
3201
3202
  // name
3202
3203
  // sources
3203
3204
  // intent
3204
- // request
3205
3205
  // model
3206
3206
  // context
3207
- // response
3208
3207
  // view
3209
- // children
3208
+ // peers
3210
3209
  // components
3211
3210
  // initialState
3212
3211
  // calculated
@@ -3222,15 +3221,16 @@ class Component {
3222
3221
  // action$
3223
3222
  // model$
3224
3223
  // context$
3225
- // response$
3226
- // sendResponse$
3227
- // children$
3224
+ // peers$
3225
+ // childSources
3228
3226
  // vdom$
3229
3227
  // currentState
3228
+ // currentProps
3229
+ // currentChildren
3230
3230
  // currentContext
3231
3231
  // subComponentSink$
3232
- // unmountRequest$
3233
- // unmount()
3232
+ // unmountRequest$ <- TODO
3233
+ // unmount() <- TODO
3234
3234
  // _debug
3235
3235
 
3236
3236
  // [ INSTANTIATED STREAM OPERATOR ]
@@ -3239,18 +3239,17 @@ class Component {
3239
3239
  // [ OUTPUT ]
3240
3240
  // sinks
3241
3241
 
3242
- constructor({ name='NO NAME', sources, intent, request, model, context, response, view, children={}, components={}, initialState, calculated, storeCalculatedInState=true, DOMSourceName='DOM', stateSourceName='STATE', requestSourceName='HTTP', debug=false }) {
3243
- if (!sources || typeof sources != 'object') throw new Error('Missing or invalid sources')
3242
+ constructor({ name='NO NAME', sources, intent, model, context, response, view, peers={}, components={}, initialState, calculated, storeCalculatedInState=true, DOMSourceName='DOM', stateSourceName='STATE', requestSourceName='HTTP', debug=false }) {
3243
+ if (!sources || !isObj(sources)) throw new Error('Missing or invalid sources')
3244
3244
 
3245
3245
  this.name = name;
3246
3246
  this.sources = sources;
3247
3247
  this.intent = intent;
3248
- this.request = request;
3249
3248
  this.model = model;
3250
3249
  this.context = context;
3251
3250
  this.response = response;
3252
3251
  this.view = view;
3253
- this.children = children;
3252
+ this.peers = peers;
3254
3253
  this.components = components;
3255
3254
  this.initialState = initialState;
3256
3255
  this.calculated = calculated;
@@ -3273,6 +3272,23 @@ class Component {
3273
3272
  }));
3274
3273
  }
3275
3274
 
3275
+ const props$ = sources.props$;
3276
+ if (props$) {
3277
+ this.sources.props$ = props$.map(val => {
3278
+ this.currentProps = val;
3279
+ return val
3280
+ });
3281
+ }
3282
+
3283
+ const children$ = sources.children$;
3284
+ if (children$) {
3285
+ this.sources.children$ = children$.map(val => {
3286
+ this.currentChildren = val;
3287
+ return val
3288
+ });
3289
+ }
3290
+
3291
+
3276
3292
  // Ensure that the root component has an intent and model
3277
3293
  // This is necessary to ensure that the component tree's state sink is subscribed to
3278
3294
  if (!this.isSubComponent && typeof this.intent === 'undefined' && typeof this.model === 'undefined') {
@@ -3288,14 +3304,13 @@ class Component {
3288
3304
  this.addCalculated = this.createMemoizedAddCalculated();
3289
3305
  this.log = makeLog(`${componentNumber} | ${name}`);
3290
3306
 
3307
+ this.initChildSources$();
3291
3308
  this.initIntent$();
3292
3309
  this.initAction$();
3293
- this.initResponse$();
3294
3310
  this.initState();
3295
3311
  this.initContext();
3296
3312
  this.initModel$();
3297
- this.initSendResponse$();
3298
- this.initChildren$();
3313
+ this.initPeers$();
3299
3314
  this.initSubComponentSink$();
3300
3315
  this.initSubComponentsRendered$();
3301
3316
  this.initVdom$();
@@ -3307,7 +3322,7 @@ class Component {
3307
3322
  }
3308
3323
 
3309
3324
  get debug() {
3310
- return this._debug || (ENVIRONMENT.DEBUG === 'true' || ENVIRONMENT.DEBUG === true)
3325
+ return this._debug || (ENVIRONMENT.SYGNAL_DEBUG === 'true' || ENVIRONMENT.SYGNAL_DEBUG === true)
3311
3326
  }
3312
3327
 
3313
3328
  initIntent$() {
@@ -3320,7 +3335,7 @@ class Component {
3320
3335
 
3321
3336
  this.intent$ = this.intent(this.sources);
3322
3337
 
3323
- if (!(this.intent$ instanceof Stream$1) && (typeof this.intent$ != 'object')) {
3338
+ if (!(this.intent$ instanceof Stream$1) && (!isObj(this.intent$))) {
3324
3339
  throw new Error('Intent must return either an action$ stream or map of event streams')
3325
3340
  }
3326
3341
  }
@@ -3344,7 +3359,7 @@ class Component {
3344
3359
 
3345
3360
  const action$ = ((runner instanceof Stream$1) ? runner : (runner.apply && runner(this.sources) || xs$1.never()));
3346
3361
  const bootstrap$ = xs$1.of({ type: BOOTSTRAP_ACTION }).compose(_default$4(10));
3347
- const wrapped$ = _default$3(bootstrap$, action$);
3362
+ const wrapped$ = this.model[BOOTSTRAP_ACTION] ? _default$3(bootstrap$, action$) : action$;
3348
3363
 
3349
3364
 
3350
3365
  let initialApiData;
@@ -3361,92 +3376,6 @@ class Component {
3361
3376
  .compose(this.log(({ type }) => `<${ type }> Action triggered`));
3362
3377
  }
3363
3378
 
3364
- initResponse$() {
3365
- if (typeof this.request == 'undefined') {
3366
- return
3367
- } else if (typeof this.request != 'object') {
3368
- throw new Error('The request parameter must be an object')
3369
- }
3370
-
3371
- const router$ = this.sources[this.requestSourceName];
3372
- const methods = Object.entries(this.request);
3373
-
3374
- const wrapped = methods.reduce((acc, [method, routes]) => {
3375
- const _method = method.toLowerCase();
3376
- if (typeof router$[_method] != 'function') {
3377
- throw new Error('Invalid method in request object:', method)
3378
- }
3379
- const entries = Object.entries(routes);
3380
- const mapped = entries.reduce((acc, [route, action]) => {
3381
- const routeString = `[${_method.toUpperCase()}]:${route || 'none'}`;
3382
- const actionType = typeof action;
3383
- if (actionType === 'undefined') {
3384
- throw new Error(`Action for '${ route }' route in request object not specified`)
3385
- } else if (actionType !== 'string' && actionType !== 'function') {
3386
- throw new Error(`Invalid action for '${ route }' route: expecting string or function`)
3387
- }
3388
- const actionString = (actionType === 'function') ? '[ FUNCTION ]' : `< ${ action } >`;
3389
- console.log(`[${ this.name }] Adding ${ this.requestSourceName } route:`, _method.toUpperCase(), `'${ route }' <${ actionString }>`);
3390
- const route$ = router$[_method](route)
3391
- .compose(_default$5((a, b) => a.id == b.id))
3392
- .map(req => {
3393
- if (!req || !req.id) {
3394
- throw new Error(`No id found in request: ${ routeString }`)
3395
- }
3396
- try {
3397
- const _reqId = req.id;
3398
- const params = req.params;
3399
- const body = req.body;
3400
- const cookies = req.cookies;
3401
- const type = (actionType === 'function') ? 'FUNCTION' : action;
3402
- const data = { params, body, cookies, req };
3403
- const obj = { type, data: body, req, _reqId, _action: type };
3404
-
3405
- const timestamp = (new Date()).toISOString();
3406
- const ip = req.get ? req.get('host') : '0.0.0.0';
3407
-
3408
- console.log(`${ timestamp } ${ ip } ${ req.method } ${ req.url }`);
3409
-
3410
- if (this.debug) {
3411
- this.action$.setDebugListener({next: ({ type }) => this.log(`[${ this.name }] Action from ${ this.requestSourceName } request: <${ type }>`, true)});
3412
- }
3413
-
3414
- if (actionType === 'function') {
3415
- const enhancedState = this.addCalculated(this.currentState);
3416
- const result = action(enhancedState, req);
3417
- return xs$1.of({ ...obj, data: result })
3418
- } else {
3419
- this.action$.shamefullySendNext(obj);
3420
-
3421
- const sourceEntries = Object.entries(this.sources);
3422
- const responses = sourceEntries.reduce((acc, [name, source]) => {
3423
- if (!source || typeof source[REQUEST_SELECTOR_METHOD] != 'function') return acc
3424
- const selected$ = source[REQUEST_SELECTOR_METHOD](_reqId);
3425
- return [ ...acc, selected$ ]
3426
- }, []);
3427
- return xs$1.merge(...responses)
3428
- }
3429
- } catch(err) {
3430
- console.error(err);
3431
- }
3432
- }).flatten();
3433
- return [ ...acc, route$ ]
3434
- }, []);
3435
- const mapped$ = xs$1.merge(...mapped);
3436
- return [ ...acc, mapped$ ]
3437
- }, []);
3438
-
3439
- this.response$ = xs$1.merge(...wrapped)
3440
- .compose(this.log(res => {
3441
- if (res._action) return `[${ this.requestSourceName }] response data received for Action: <${ res._action }>`
3442
- return `[${ this.requestSourceName }] response data received from FUNCTION`
3443
- }));
3444
-
3445
- if (typeof this.response != 'undefined' && typeof this.response$ == 'undefined') {
3446
- throw new Error('Cannot have a response parameter without a request parameter')
3447
- }
3448
- }
3449
-
3450
3379
  initState() {
3451
3380
  if (this.model != undefined) {
3452
3381
  if (this.model[INITIALIZE_ACTION] === undefined) {
@@ -3469,29 +3398,16 @@ class Component {
3469
3398
  this.context$ = xs$1.of({});
3470
3399
  return
3471
3400
  }
3472
- const repeatChecker = (a, b) => {
3473
- if (a === b) return true
3474
- if (typeof a !== 'object' || typeof b !== 'object') {
3475
- return a === b
3476
- }
3477
- const entriesA = Object.entries(a);
3478
- const entriesB = Object.entries(b);
3479
- if (entriesA.length === 0 && entriesB.length === 0) return true
3480
- if (entriesA.length !== entriesB.length) return false
3481
- return entriesA.every(([name, value]) => {
3482
- return b[name] === value
3483
- })
3484
- };
3485
3401
 
3486
- const state$ = this.sources[this.stateSourceName]?.stream.startWith({}).compose(_default$5(repeatChecker)) || xs$1.never();
3487
- const parentContext$ = this.sources.__parentContext$.startWith({}).compose(_default$5(repeatChecker)) || xs$1.of({});
3488
- if (this.context && typeof this.context !== 'object') {
3402
+ const state$ = this.sources[this.stateSourceName]?.stream.startWith({}).compose(_default$5(objIsEqual)) || xs$1.never();
3403
+ const parentContext$ = this.sources.__parentContext$.startWith({}).compose(_default$5(objIsEqual)) || xs$1.of({});
3404
+ if (this.context && !isObj(this.context)) {
3489
3405
  console.error(`[${this.name}] Context must be an object mapping names to values of functions: ignoring provided ${ typeof this.context }`);
3490
3406
  }
3491
3407
  this.context$ = xs$1.combine(state$, parentContext$)
3492
3408
  .map(([_, parent]) => {
3493
- const _parent = typeof parent === 'object' ? parent : {};
3494
- const context = typeof this.context === 'object' ? this.context : {};
3409
+ const _parent = isObj(parent) ? parent : {};
3410
+ const context = isObj(this.context) ? this.context : {};
3495
3411
  const state = this.currentState;
3496
3412
  const values = Object.entries(context).reduce((acc, current) => {
3497
3413
  const [name, value] = current;
@@ -3514,7 +3430,7 @@ class Component {
3514
3430
  this.currentContext = newContext;
3515
3431
  return newContext
3516
3432
  })
3517
- .compose(_default$5(repeatChecker))
3433
+ .compose(_default$5(objIsEqual))
3518
3434
  .startWith({});
3519
3435
  this.context$.subscribe({ next: _ => _ });
3520
3436
  }
@@ -3548,7 +3464,7 @@ class Component {
3548
3464
  sinks = { [this.stateSourceName]: sinks };
3549
3465
  }
3550
3466
 
3551
- if (typeof sinks !== 'object') {
3467
+ if (!isObj(sinks)) {
3552
3468
  throw new Error(`Entry for each action must be an object: ${ this.name } ${ action }`)
3553
3469
  }
3554
3470
 
@@ -3557,15 +3473,18 @@ class Component {
3557
3473
  sinkEntries.forEach((entry) => {
3558
3474
  const [sink, reducer] = entry;
3559
3475
 
3560
- const isStateSink = (sink == this.stateSourceName);
3476
+ const isStateSink = (sink === this.stateSourceName);
3477
+ const isParentSink = (sink === PARENT_SINK_NAME);
3561
3478
 
3562
3479
  const on = isStateSink ? onState() : onNormal();
3563
- const on$ = on(action, reducer);
3480
+ const on$ = isParentSink ? on(action, reducer).map(value => ({ name: this.name, value })) : on(action, reducer);
3564
3481
 
3565
3482
  const wrapped$ = on$
3566
3483
  .compose(this.log(data => {
3567
3484
  if (isStateSink) {
3568
3485
  return `<${ action }> State reducer added`
3486
+ } else if (isParentSink) {
3487
+ return `<${ action }> Data sent to parent component: ${ JSON.stringify(data.value).replaceAll('"', '') }`
3569
3488
  } else {
3570
3489
  const extra = data && (data.type || data.command || data.name || data.key || (Array.isArray(data) && 'Array') || data);
3571
3490
  return `<${ action }> Data sent to [${ sink }]: ${ JSON.stringify(extra).replaceAll('"', '') }`
@@ -3589,51 +3508,7 @@ class Component {
3589
3508
  this.model$ = model$;
3590
3509
  }
3591
3510
 
3592
- initSendResponse$() {
3593
- const responseType = typeof this.response;
3594
- if (responseType != 'function' && responseType != 'undefined') {
3595
- throw new Error('The response parameter must be a function')
3596
- }
3597
-
3598
- if (responseType == 'undefined') {
3599
- if (this.response$) {
3600
- this.response$.subscribe({
3601
- next: this.log(({ _reqId, _action }) => `Unhandled response for request: ${ _action } ${ _reqId }`)
3602
- });
3603
- }
3604
- this.sendResponse$ = xs$1.never();
3605
- return
3606
- }
3607
-
3608
- const selectable = {
3609
- select: (actions) => {
3610
- if (typeof actions == 'undefined') return this.response$
3611
- if (!Array.isArray(actions)) actions = [actions];
3612
- return this.response$.filter(({_action}) => (actions.length > 0) ? (_action === 'FUNCTION' || actions.includes(_action)) : true)
3613
- }
3614
- };
3615
-
3616
- const out = this.response(selectable);
3617
- if (typeof out != 'object') {
3618
- throw new Error('The response function must return an object')
3619
- }
3620
-
3621
- const entries = Object.entries(out);
3622
- const out$ = entries.reduce((acc, [command, response$]) => {
3623
- const mapped$ = response$.map(({ _reqId, _action, data }) => {
3624
- if (!_reqId) {
3625
- throw new Error(`No request id found for response for: ${ command }`)
3626
- }
3627
- return { _reqId, _action, command, data }
3628
- });
3629
- return [ ...acc, mapped$ ]
3630
- }, []);
3631
-
3632
- this.sendResponse$ = xs$1.merge(...out$)
3633
- .compose(this.log(({ _reqId, _action }) => `[${ this.requestSourceName }] response sent for: <${ _action }>`));
3634
- }
3635
-
3636
- initChildren$() {
3511
+ initPeers$() {
3637
3512
  const initial = this.sourceNames.reduce((acc, name) => {
3638
3513
  if (name == this.DOMSourceName) {
3639
3514
  acc[name] = {};
@@ -3643,19 +3518,46 @@ class Component {
3643
3518
  return acc
3644
3519
  }, {});
3645
3520
 
3646
- this.children$ = Object.entries(this.children).reduce((acc, [childName, childFactory]) => {
3647
- const child$ = childFactory(this.sources);
3521
+ this.peers$ = Object.entries(this.peers).reduce((acc, [peerName, peerFactory]) => {
3522
+ const peer$ = peerFactory(this.sources);
3648
3523
  this.sourceNames.forEach(source => {
3649
3524
  if (source == this.DOMSourceName) {
3650
- acc[source][childName] = child$[source];
3525
+ acc[source][peerName] = peer$[source];
3651
3526
  } else {
3652
- acc[source].push(child$[source]);
3527
+ acc[source].push(peer$[source]);
3653
3528
  }
3654
3529
  });
3655
3530
  return acc
3656
3531
  }, initial);
3657
3532
  }
3658
3533
 
3534
+ initChildSources$() {
3535
+ let newSourcesNext;
3536
+ const childSources$ = xs$1.create({
3537
+ start: listener => {
3538
+ newSourcesNext = listener.next.bind(listener);
3539
+ },
3540
+ stop: _ => {
3541
+
3542
+ }
3543
+ }).map(sources => xs$1.merge(...sources)).flatten();
3544
+
3545
+ // childSources$.subscribe({ next: _ => _})
3546
+
3547
+ this.sources[CHILD_SOURCE_NAME] = {
3548
+ select: (name) => {
3549
+ const all$ = childSources$;
3550
+ const filtered$ = name ? all$.filter(entry => entry.name === name) : all$;
3551
+ const unwrapped$ = filtered$.map(entry => entry.value);
3552
+ return unwrapped$
3553
+ }
3554
+ };
3555
+
3556
+ this.newChildSources = (sources) => {
3557
+ if (typeof newSourcesNext === 'function') newSourcesNext(sources);
3558
+ };
3559
+ }
3560
+
3659
3561
  initSubComponentSink$() {
3660
3562
  const subComponentSink$ = xs$1.create({
3661
3563
  start: listener => {
@@ -3702,17 +3604,17 @@ class Component {
3702
3604
  initSinks() {
3703
3605
  this.sinks = this.sourceNames.reduce((acc, name) => {
3704
3606
  if (name == this.DOMSourceName) return acc
3705
- const subComponentSink$ = this.subComponentSink$ ? this.subComponentSink$.map(sinks => sinks[name]).filter(sink => !!sink).flatten() : xs$1.never();
3607
+ const subComponentSink$ = (this.subComponentSink$ && name !== PARENT_SINK_NAME) ? this.subComponentSink$.map(sinks => sinks[name]).filter(sink => !!sink).flatten() : xs$1.never();
3706
3608
  if (name === this.stateSourceName) {
3707
- acc[name] = xs$1.merge((this.model$[name] || xs$1.never()), subComponentSink$, this.sources[this.stateSourceName].stream.filter(_ => false), ...this.children$[name]);
3609
+ acc[name] = xs$1.merge((this.model$[name] || xs$1.never()), subComponentSink$, this.sources[this.stateSourceName].stream.filter(_ => false), ...(this.peers$[name] || []));
3708
3610
  } else {
3709
- acc[name] = xs$1.merge((this.model$[name] || xs$1.never()), subComponentSink$, ...this.children$[name]);
3611
+ acc[name] = xs$1.merge((this.model$[name] || xs$1.never()), subComponentSink$, ...(this.peers$[name] || []));
3710
3612
  }
3711
3613
  return acc
3712
3614
  }, {});
3713
3615
 
3714
- this.sinks[this.DOMSourceName] = this.vdom$;
3715
- this.sinks[this.requestSourceName] = xs$1.merge(this.sendResponse$ ,this.sinks[this.requestSourceName]);
3616
+ this.sinks[this.DOMSourceName] = this.vdom$;
3617
+ this.sinks[PARENT_SINK_NAME] = this.model$[PARENT_SINK_NAME] || xs$1.never();
3716
3618
  }
3717
3619
 
3718
3620
  makeOnAction(action$, isStateSink=true, rootAction$) {
@@ -3725,33 +3627,30 @@ class Component {
3725
3627
  returnStream$ = filtered$.map(action => {
3726
3628
  const next = (type, data, delay=10) => {
3727
3629
  if (typeof delay !== 'number') throw new Error(`[${ this.name } ] Invalid delay value provided to next() function in model action '${ name }'. Must be a number in ms.`)
3728
- const _reqId = action._reqId || (action.req && action.req.id);
3729
- const _data = _reqId ? (typeof data == 'object' ? { ...data, _reqId, _action: name } : { data, _reqId, _action: name }) : data;
3730
3630
  // put the "next" action request at the end of the event loop so the "current" action completes first
3731
3631
  setTimeout(() => {
3732
3632
  // push the "next" action request into the action$ stream
3733
- rootAction$.shamefullySendNext({ type, data: _data });
3633
+ rootAction$.shamefullySendNext({ type, data });
3734
3634
  }, delay);
3735
3635
  this.log(`<${ name }> Triggered a next() action: <${ type }> ${ delay }ms delay`, true);
3736
3636
  };
3737
3637
 
3638
+ const extra = { props: this.currentProps, children: this.currentChildren, context: this.currentContext };
3639
+
3738
3640
  let data = action.data;
3739
- if (data && data.data && data._reqId) data = data.data;
3740
3641
  if (isStateSink) {
3741
3642
  return (state) => {
3742
3643
  const _state = this.isSubComponent ? this.currentState : state;
3743
3644
  const enhancedState = this.addCalculated(_state);
3744
- const newState = reducer(enhancedState, data, next, action.req);
3645
+ const newState = reducer(enhancedState, data, next, extra);
3745
3646
  if (newState == ABORT) return _state
3746
3647
  return this.cleanupCalculated(newState)
3747
3648
  }
3748
3649
  } else {
3749
3650
  const enhancedState = this.addCalculated(this.currentState);
3750
- const reduced = reducer(enhancedState, data, next, action.req);
3651
+ const reduced = reducer(enhancedState, data, next, extra);
3751
3652
  const type = typeof reduced;
3752
- const _reqId = action._reqId || (action.req && action.req.id);
3753
- if (['string', 'number', 'boolean', 'function'].includes(type)) return reduced
3754
- if (type == 'object') return { ...reduced, _reqId, _action: name }
3653
+ if (isObj(reduced) || ['string', 'number', 'boolean', 'function'].includes(type)) return reduced
3755
3654
  if (type == 'undefined') {
3756
3655
  console.warn(`'undefined' value sent to ${ name }`);
3757
3656
  return reduced
@@ -3775,28 +3674,20 @@ class Component {
3775
3674
  let lastResult;
3776
3675
 
3777
3676
  return function(state) {
3778
- if (!this.calculated || typeof state !== 'object' || state instanceof Array) return state
3677
+ if (!this.calculated || !isObj(state) || Array.isArray(state)) return state
3779
3678
  if (state === lastState) {
3780
3679
  return lastResult
3781
3680
  }
3782
- if (typeof this.calculated !== 'object') throw new Error(`'calculated' parameter must be an object mapping calculated state field named to functions`)
3783
- const entries = Object.entries(this.calculated);
3784
- if (entries.length === 0) {
3681
+ if (!isObj(this.calculated)) throw new Error(`'calculated' parameter must be an object mapping calculated state field named to functions`)
3682
+
3683
+ const calculated = this.getCalculatedValues(state);
3684
+ if (!calculated) {
3785
3685
  lastState = state;
3786
3686
  lastResult = state;
3787
3687
  return state
3788
3688
  }
3789
- const calculated = entries.reduce((acc, [field, fn]) => {
3790
- if (typeof fn !== 'function') throw new Error(`Missing or invalid calculator function for calculated field '${ field }`)
3791
- try {
3792
- acc[field] = fn(state);
3793
- } catch(e) {
3794
- console.warn(`Calculated field '${ field }' threw an error during calculation: ${ e.message }`);
3795
- }
3796
- return acc
3797
- }, {});
3798
3689
 
3799
- const newState = { ...state, ...calculated, __context: this.currentContext };
3690
+ const newState = { ...state, ...calculated };
3800
3691
 
3801
3692
  lastState = state;
3802
3693
  lastResult = newState;
@@ -3805,12 +3696,27 @@ class Component {
3805
3696
  }
3806
3697
  }
3807
3698
 
3699
+ getCalculatedValues(state) {
3700
+ const entries = Object.entries(this.calculated || {});
3701
+ if (entries.length === 0) {
3702
+ return
3703
+ }
3704
+ return entries.reduce((acc, [field, fn]) => {
3705
+ if (typeof fn !== 'function') throw new Error(`Missing or invalid calculator function for calculated field '${ field }`)
3706
+ try {
3707
+ acc[field] = fn(state);
3708
+ } catch(e) {
3709
+ console.warn(`Calculated field '${ field }' threw an error during calculation: ${ e.message }`);
3710
+ }
3711
+ return acc
3712
+ }, {})
3713
+ }
3714
+
3808
3715
  cleanupCalculated(incomingState) {
3809
- if (!incomingState || typeof incomingState !== 'object' || incomingState instanceof Array) return incomingState
3716
+ if (!incomingState || !isObj(incomingState) || Array.isArray(incomingState)) return incomingState
3810
3717
  const state = this.storeCalculatedInState ? this.addCalculated(incomingState) : incomingState;
3811
- const { __props, __children, __context, ...sanitized } = state;
3812
- const copy = { ...sanitized };
3813
- if (!this.calculated) return copy
3718
+ const copy = { ...state };
3719
+ if (!this.calculated || this.storeCalculatedInState) return copy
3814
3720
  const keys = Object.keys(this.calculated);
3815
3721
  keys.forEach(key => {
3816
3722
  if (this.initialState && typeof this.initialState[key] !== 'undefined') {
@@ -3824,7 +3730,7 @@ class Component {
3824
3730
 
3825
3731
  collectRenderParameters() {
3826
3732
  const state = this.sources[this.stateSourceName];
3827
- const renderParams = { ...this.children$[this.DOMSourceName] };
3733
+ const renderParams = { ...this.peers$[this.DOMSourceName] };
3828
3734
 
3829
3735
  const enhancedState = state && state.isolateSource(state, { get: state => this.addCalculated(state) });
3830
3736
  const stateStream = (enhancedState && enhancedState.stream) || xs$1.never();
@@ -3833,15 +3739,15 @@ class Component {
3833
3739
  renderParams.state = stateStream.compose(_default$5(objIsEqual));
3834
3740
 
3835
3741
  if (this.sources.props$) {
3836
- renderParams.__props = this.sources.props$.compose(_default$5(objIsEqual));
3742
+ renderParams.props = this.sources.props$.compose(_default$5(propsIsEqual));
3837
3743
  }
3838
3744
 
3839
3745
  if (this.sources.children$) {
3840
- renderParams.__children = this.sources.children$.compose(_default$5(objIsEqual));
3746
+ renderParams.children = this.sources.children$.compose(_default$5(objIsEqual));
3841
3747
  }
3842
3748
 
3843
3749
  if (this.context$) {
3844
- renderParams.__context = this.context$.compose(_default$5(objIsEqual));
3750
+ renderParams.context = this.context$.compose(_default$5(objIsEqual));
3845
3751
  }
3846
3752
 
3847
3753
  const names = [];
@@ -3858,10 +3764,10 @@ class Component {
3858
3764
  .map(arr => {
3859
3765
  const params = names.reduce((acc, name, index) => {
3860
3766
  acc[name] = arr[index];
3861
- if (name === 'state') acc[this.stateSourceName] = arr[index];
3862
- if (name === '__props') acc.props = arr[index];
3863
- if (name === '__children') acc.children = arr[index];
3864
- if (name === '__context') acc.context = arr[index];
3767
+ if (name === 'state') {
3768
+ acc[this.stateSourceName] = arr[index];
3769
+ acc.calculated = (arr[index] && this.getCalculatedValues(arr[index])) || {};
3770
+ }
3865
3771
  return acc
3866
3772
  }, {});
3867
3773
  return params
@@ -3883,6 +3789,7 @@ class Component {
3883
3789
  }
3884
3790
 
3885
3791
  const sinkArrsByType = {};
3792
+ const childSources = [];
3886
3793
  let newInstanceCount = 0;
3887
3794
 
3888
3795
  const newComponents = entries.reduce((acc, [id, el]) => {
@@ -3894,9 +3801,13 @@ class Component {
3894
3801
  const isSwitchable = data.isSwitchable || false;
3895
3802
 
3896
3803
  const addSinks = (sinks) => {
3897
- Object.entries(sinks).map(([name, stream]) => {
3804
+ Object.entries(sinks).forEach(([name, stream]) => {
3898
3805
  sinkArrsByType[name] ||= [];
3899
- if (name !== this.DOMSourceName) sinkArrsByType[name].push(stream);
3806
+ if (name === PARENT_SINK_NAME) {
3807
+ childSources.push(stream);
3808
+ } else if (name !== this.DOMSourceName) {
3809
+ sinkArrsByType[name].push(stream);
3810
+ }
3900
3811
  });
3901
3812
  };
3902
3813
 
@@ -3943,6 +3854,8 @@ class Component {
3943
3854
  }, {});
3944
3855
 
3945
3856
  this.newSubComponentSinks(mergedSinksByType);
3857
+ this.newChildSources(childSources);
3858
+
3946
3859
 
3947
3860
  if (newInstanceCount > 0) this.log(`New sub components instantiated: ${ newInstanceCount }`, true);
3948
3861
 
@@ -3966,21 +3879,34 @@ class Component {
3966
3879
  instantiateCollection(el, props$, children$) {
3967
3880
  const data = el.data;
3968
3881
  const props = data.props || {};
3969
- el.children || [];
3882
+ let filter = typeof props.filter === 'function' ? props.filter : undefined;
3883
+ let sort = sortFunctionFromProp(props.sort);
3970
3884
 
3971
- const combined$ = xs$1.combine(this.sources[this.stateSourceName].stream.startWith(this.currentState), props$, children$)
3972
- .map(([state, __props, __children]) => {
3973
- return typeof state === 'object' ? { ...this.addCalculated(state), __props, __children, __context: this.currentContext } : { value: state, __props, __children, __context: this.currentContext }
3885
+ const arrayOperators = {
3886
+ filter,
3887
+ sort
3888
+ };
3889
+
3890
+ const state$ = xs$1.combine(this.sources[this.stateSourceName].stream.startWith(this.currentState), props$.startWith(props))
3891
+ // this debounce is important. it forces state and prop updates to happen at the same time
3892
+ // without this, changes to sort or filter won't happen properly
3893
+ .compose(_default$2(1))
3894
+ .map(([state, props]) => {
3895
+ if (props.filter !== arrayOperators.filter) {
3896
+ arrayOperators.filter = typeof props.filter === 'function' ? props.filter : undefined;
3897
+ }
3898
+ if (props.sort !== arrayOperators.sort) {
3899
+ arrayOperators.sort = sortFunctionFromProp(props.sort);
3900
+ }
3901
+
3902
+ return isObj(state) ? this.addCalculated(state) : state
3974
3903
  });
3975
3904
 
3976
- const stateSource = new StateSource(combined$);
3977
- const stateField = props.from;
3978
-
3979
- if (typeof props.sygnalFactory !== 'function' && typeof props.sygnalOptions === 'object') {
3980
- props.sygnalFactory = component(props.sygnalOptions);
3981
- }
3982
-
3905
+ const stateSource = new StateSource(state$);
3906
+ const stateField = props.from;
3983
3907
  const collectionOf = props.of;
3908
+ const idField = props.idfield || 'id';
3909
+
3984
3910
  let lense;
3985
3911
  let factory;
3986
3912
 
@@ -3988,10 +3914,10 @@ class Component {
3988
3914
  if (collectionOf.isSygnalComponent) {
3989
3915
  factory = collectionOf;
3990
3916
  } else {
3991
- const name = (typeof collectionOf.name === 'string') ? collectionOf.name : 'FUNCTION_COMPONENT';
3917
+ const name = collectionOf.componentName || collectionOf.label || collectionOf.name || 'FUNCTION_COMPONENT';
3992
3918
  const view = collectionOf;
3993
- const { model, intent, context, children, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug } = collectionOf;
3994
- const options = { name, view, model, intent, context, children, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug };
3919
+ const { model, intent, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug } = collectionOf;
3920
+ const options = { name, view, model, intent, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug };
3995
3921
  factory = component(options);
3996
3922
  }
3997
3923
  } else if (this.components[collectionOf]) {
@@ -4000,29 +3926,33 @@ class Component {
4000
3926
  throw new Error(`[${this.name}] Invalid 'of' propery in collection: ${ collectionOf }`)
4001
3927
  }
4002
3928
 
4003
- const sanitizeItems = item => {
4004
- if (typeof item === 'object') {
4005
- const { __props, __children, __context, ...sanitized } = item;
4006
- return sanitized
4007
- } else {
4008
- return item
4009
- }
4010
- };
4011
-
4012
3929
  const fieldLense = {
4013
3930
  get: state => {
4014
- const { __props, __children } = state;
4015
3931
  if (!Array.isArray(state[stateField])) return []
4016
- return state[stateField].map(item => {
4017
- return typeof item === 'object' ? { ...item, __props, __children, __context: this.currentContext } : { value: item, __props, __children, __context: this.currentContext }
4018
- })
3932
+ const items = state[stateField];
3933
+ const filtered = typeof arrayOperators.filter === 'function' ? items.filter(arrayOperators.filter) : items;
3934
+ const sorted = typeof arrayOperators.sort ? filtered.sort(arrayOperators.sort) : filtered;
3935
+ const mapped = sorted.map((item, index) => {
3936
+ return (isObj(item)) ? { ...item, [idField]: item[idField] || index } : { value: item, [idField]: index }
3937
+ });
3938
+
3939
+ return mapped
4019
3940
  },
4020
3941
  set: (oldState, newState) => {
4021
3942
  if (this.calculated && stateField in this.calculated) {
4022
3943
  console.warn(`Collection sub-component of ${ this.name } attempted to update state on a calculated field '${ stateField }': Update ignored`);
4023
3944
  return oldState
4024
3945
  }
4025
- return { ...oldState, [stateField]: newState.map(sanitizeItems) }
3946
+ const updated = [];
3947
+ for (const oldItem of oldState[stateField].map((item, index) => (isObj(item) ? { ...item, [idField]: item[idField] || index } : { __primitive: true, value: item, [idField]: index }))) {
3948
+ if (typeof arrayOperators.filter === 'function' && !arrayOperators.filter(oldItem)) {
3949
+ updated.push(oldItem.__primitive ? oldItem.value : oldItem);
3950
+ } else {
3951
+ const newItem = newState.find(item => item[idField] === oldItem[idField]);
3952
+ if (typeof newItem !== 'undefined') updated.push(oldItem.__primitive ? newItem.value : newItem);
3953
+ }
3954
+ }
3955
+ return { ...oldState, [stateField]: updated }
4026
3956
  }
4027
3957
  };
4028
3958
 
@@ -4037,7 +3967,7 @@ class Component {
4037
3967
  }
4038
3968
  };
4039
3969
  } else if (typeof stateField === 'string') {
4040
- if (typeof this.currentState === 'object') {
3970
+ if (isObj(this.currentState)) {
4041
3971
  if(!(this.currentState && stateField in this.currentState) && !(this.calculated && stateField in this.calculated)) {
4042
3972
  console.error(`Collection component in ${ this.name } is attempting to use non-existent state property '${ stateField }': To fix this error, specify a valid array property on the state. Attempting to use parent component state.`);
4043
3973
  lense = undefined;
@@ -4055,7 +3985,7 @@ class Component {
4055
3985
  lense = fieldLense;
4056
3986
  }
4057
3987
  }
4058
- } else if (typeof stateField === 'object') {
3988
+ } else if (isObj(stateField)) {
4059
3989
  if (typeof stateField.get !== 'function') {
4060
3990
  console.error(`Collection component in ${ this.name } has an invalid 'from' field: Expecting 'undefined', a string indicating an array property in the state, or an object with 'get' and 'set' functions for retrieving and setting child state from the current state. Attempting to use parent component state.`);
4061
3991
  lense = undefined;
@@ -4077,9 +4007,9 @@ class Component {
4077
4007
  lense = undefined;
4078
4008
  }
4079
4009
 
4080
- const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$ };
4010
+ const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, PARENT: null };
4081
4011
  const sink$ = collection(factory, lense, { container: null })(sources);
4082
- if (typeof sink$ !== 'object') {
4012
+ if (!isObj(sink$)) {
4083
4013
  throw new Error('Invalid sinks returned from component factory of collection element')
4084
4014
  }
4085
4015
  return sink$
@@ -4088,47 +4018,38 @@ class Component {
4088
4018
  instantiateSwitchable(el, props$, children$) {
4089
4019
  const data = el.data;
4090
4020
  const props = data.props || {};
4091
- el.children || [];
4092
4021
 
4093
- const combined$ = xs$1.combine(this.sources[this.stateSourceName].stream.startWith(this.currentState), props$, children$)
4094
- .map(([state, __props, __children]) => {
4095
- return typeof state === 'object' ? { ...this.addCalculated(state), __props, __children, __context: this.currentContext } : { value: state, __props, __children, __context: this.currentContext }
4022
+ const state$ = this.sources[this.stateSourceName].stream.startWith(this.currentState)
4023
+ .map((state) => {
4024
+ return isObj(state) ? this.addCalculated(state) : state
4096
4025
  });
4097
4026
 
4098
- const stateSource = new StateSource(combined$);
4099
- const stateField = props.state;
4027
+ const stateSource = new StateSource(state$);
4028
+ const stateField = props.state;
4100
4029
  let lense;
4101
4030
 
4102
4031
  const fieldLense = {
4103
- get: state => {
4104
- const { __props, __children } = state;
4105
- return (typeof state[stateField] === 'object' && !(state[stateField] instanceof Array)) ? { ...state[stateField], __props, __children, __context: this.currentContext } : { value: state[stateField], __props, __children, __context: this.currentContext }
4106
- },
4032
+ get: state => state[stateField],
4107
4033
  set: (oldState, newState) => {
4108
4034
  if (this.calculated && stateField in this.calculated) {
4109
4035
  console.warn(`Switchable sub-component of ${ this.name } attempted to update state on a calculated field '${ stateField }': Update ignored`);
4110
4036
  return oldState
4111
4037
  }
4112
- if (typeof newState !== 'object' || newState instanceof Array) return { ...oldState, [stateField]: newState }
4113
- const { __props, __children, __context, ...sanitized } = newState;
4114
- return { ...oldState, [stateField]: sanitized }
4038
+ if (!isObj(newState) || Array.isArray(newState)) return { ...oldState, [stateField]: newState }
4039
+ return { ...oldState, [stateField]: newState }
4115
4040
  }
4116
4041
  };
4117
4042
 
4118
4043
  const baseLense = {
4119
4044
  get: state => state,
4120
- set: (oldState, newState) => {
4121
- if (typeof newState !== 'object' || newState instanceof Array) return newState
4122
- const { __props, __children, __context, ...sanitized } = newState;
4123
- return sanitized
4124
- }
4045
+ set: (oldState, newState) => newState
4125
4046
  };
4126
4047
 
4127
4048
  if (typeof stateField === 'undefined') {
4128
4049
  lense = baseLense;
4129
4050
  } else if (typeof stateField === 'string') {
4130
4051
  lense = fieldLense;
4131
- } else if (typeof stateField === 'object') {
4052
+ } else if (isObj(stateField)) {
4132
4053
  if (typeof stateField.get !== 'function') {
4133
4054
  console.error(`Switchable component in ${ this.name } has an invalid 'state' field: Expecting 'undefined', a string indicating an array property in the state, or an object with 'get' and 'set' functions for retrieving and setting sub-component state from the current state. Attempting to use parent component state.`);
4134
4055
  lense = baseLense;
@@ -4145,10 +4066,10 @@ class Component {
4145
4066
  keys.forEach(key => {
4146
4067
  const current = switchableComponents[key];
4147
4068
  if (!current.isSygnalComponent) {
4148
- const name = (typeof current.name === 'string') ? current.name : 'FUNCTION_COMPONENT';
4069
+ const name = current.componentName || current.label || current.name || 'FUNCTION_COMPONENT';
4149
4070
  const view = current;
4150
- const { model, intent, context, children, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug } = current;
4151
- const options = { name, view, model, intent, context, children, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug };
4071
+ const { model, intent, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug } = current;
4072
+ const options = { name, view, model, intent, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug };
4152
4073
  switchableComponents[key] = component(options);
4153
4074
  }
4154
4075
  });
@@ -4156,7 +4077,7 @@ class Component {
4156
4077
 
4157
4078
  const sink$ = isolate(switchable(switchableComponents, props$.map(props => props.current)), { [this.stateSourceName]: lense })(sources);
4158
4079
 
4159
- if (typeof sink$ !== 'object') {
4080
+ if (!isObj(sink$)) {
4160
4081
  throw new Error('Invalid sinks returned from component factory of switchable element')
4161
4082
  }
4162
4083
 
@@ -4167,17 +4088,16 @@ class Component {
4167
4088
  const componentName = el.sel;
4168
4089
  const data = el.data;
4169
4090
  const props = data.props || {};
4170
- el.children || [];
4171
4091
 
4172
- const combined$ = xs$1.combine(this.sources[this.stateSourceName].stream.startWith(this.currentState), props$, children$)
4173
- .map(([state, __props, __children]) => {
4174
- return typeof state === 'object' ? { ...this.addCalculated(state), __props, __children, __context: this.currentContext } : { value: state, __props, __children, __context: this.currentContext }
4092
+ const state$ = this.sources[this.stateSourceName].stream.startWith(this.currentState)
4093
+ .map((state) => {
4094
+ return isObj(state) ? this.addCalculated(state) : state
4175
4095
  });
4176
4096
 
4177
- const stateSource = new StateSource(combined$);
4097
+ const stateSource = new StateSource(state$);
4178
4098
  const stateField = props.state;
4179
4099
 
4180
- if (typeof props.sygnalFactory !== 'function' && typeof props.sygnalOptions === 'object') {
4100
+ if (typeof props.sygnalFactory !== 'function' && isObj(props.sygnalOptions)) {
4181
4101
  props.sygnalFactory = component(props.sygnalOptions);
4182
4102
  }
4183
4103
 
@@ -4190,10 +4110,7 @@ class Component {
4190
4110
  let lense;
4191
4111
 
4192
4112
  const fieldLense = {
4193
- get: state => {
4194
- const { __props, __children } = state;
4195
- return typeof state[stateField] === 'object' ? { ...state[stateField], __props, __children, __context: this.currentContext } : { value: state[stateField], __props, __children, __context: this.currentContext }
4196
- },
4113
+ get: state => state[stateField],
4197
4114
  set: (oldState, newState) => {
4198
4115
  if (this.calculated && stateField in this.calculated) {
4199
4116
  console.warn(`Sub-component of ${ this.name } attempted to update state on a calculated field '${ stateField }': Update ignored`);
@@ -4205,18 +4122,14 @@ class Component {
4205
4122
 
4206
4123
  const baseLense = {
4207
4124
  get: state => state,
4208
- set: (oldState, newState) => {
4209
- if (typeof newState !== 'object' || newState instanceof Array) return newState
4210
- const { __props, __children, __context, ...sanitized } = newState;
4211
- return sanitized
4212
- }
4125
+ set: (oldState, newState) => newState
4213
4126
  };
4214
4127
 
4215
4128
  if (typeof stateField === 'undefined') {
4216
4129
  lense = baseLense;
4217
4130
  } else if (typeof stateField === 'string') {
4218
4131
  lense = fieldLense;
4219
- } else if (typeof stateField === 'object') {
4132
+ } else if (isObj(stateField)) {
4220
4133
  if (typeof stateField.get !== 'function') {
4221
4134
  console.error(`Sub-component in ${ this.name } has an invalid 'state' field: Expecting 'undefined', a string indicating an array property in the state, or an object with 'get' and 'set' functions for retrieving and setting sub-component state from the current state. Attempting to use parent component state.`);
4222
4135
  lense = baseLense;
@@ -4231,7 +4144,7 @@ class Component {
4231
4144
  const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$ };
4232
4145
  const sink$ = isolate(factory, { [this.stateSourceName]: lense })(sources);
4233
4146
 
4234
- if (typeof sink$ !== 'object') {
4147
+ if (!isObj(sink$)) {
4235
4148
  const name = componentName === 'sygnal-factory' ? 'custom element' : componentName;
4236
4149
  throw new Error('Invalid sinks returned from component factory:', name)
4237
4150
  }
@@ -4329,7 +4242,7 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
4329
4242
  const sel = currentElement.sel;
4330
4243
  const isCollection = sel && sel.toLowerCase() === 'collection';
4331
4244
  const isSwitchable = sel && sel.toLowerCase() === 'switchable';
4332
- const isComponent = sel && (['collection', 'switchable', 'sygnal-factory', ...componentNames].includes(sel)) || typeof currentElement.data?.props?.sygnalFactory === 'function' || typeof currentElement.data?.props?.sygnalOptions === 'object';
4245
+ const isComponent = sel && (['collection', 'switchable', 'sygnal-factory', ...componentNames].includes(sel)) || typeof currentElement.data?.props?.sygnalFactory === 'function' || isObj(currentElement.data?.props?.sygnalOptions);
4333
4246
  const props = (currentElement.data && currentElement.data.props) || {};
4334
4247
  (currentElement.data && currentElement.data.attrs) || {};
4335
4248
  const children = currentElement.children || [];
@@ -4340,15 +4253,15 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
4340
4253
  if (isComponent) {
4341
4254
  id = getComponentIdFromElement(currentElement, depth, index, parentId);
4342
4255
  if (isCollection) {
4343
- if (!props.of) throw new Error(`Collection element missing required 'component' property`)
4256
+ if (!props.of) throw new Error(`Collection element missing required 'component' property`)
4344
4257
  if (typeof props.of !== 'string' && typeof props.of !== 'function') throw new Error(`Invalid 'component' property of collection element: found ${ typeof props.of } requires string or component factory function`)
4345
4258
  if (typeof props.of !== 'function' && !componentNames.includes(props.of)) throw new Error(`Specified component for collection not found: ${ props.of }`)
4346
4259
  if (typeof props.from !== 'undefined' && !(typeof props.from === 'string' || Array.isArray(props.from) || typeof props.from.get === 'function')) console.warn(`No valid array found for collection ${ typeof props.of === 'string' ? props.of : 'function component' }: no collection components will be created`, props.from);
4347
4260
  currentElement.data.isCollection = true;
4348
4261
  currentElement.data.props ||= {};
4349
4262
  } else if (isSwitchable) {
4350
- if (!props.of) throw new Error(`Switchable element missing required 'of' property`)
4351
- if (typeof props.of !== 'object') throw new Error(`Invalid 'of' property of switchable element: found ${ typeof props.of } requires object mapping names to component factories`)
4263
+ if (!props.of) throw new Error(`Switchable element missing required 'of' property`)
4264
+ if (!isObj(props.of)) throw new Error(`Invalid 'of' property of switchable element: found ${ typeof props.of } requires object mapping names to component factories`)
4352
4265
  const switchableComponents = Object.values(props.of);
4353
4266
  if (!switchableComponents.every(comp => typeof comp === 'function')) throw new Error(`One or more components provided to switchable element is not a valid component factory`)
4354
4267
  if (!props.current || (typeof props.current !== 'string' && typeof props.current !== 'function')) throw new Error(`Missing or invalid 'current' property for switchable element: found '${ typeof props.current }' requires string or function`)
@@ -4377,7 +4290,7 @@ function injectComponents(currentElement, components, componentNames, depth=0, i
4377
4290
 
4378
4291
 
4379
4292
  const sel = currentElement.sel || 'NO SELECTOR';
4380
- const isComponent = ['collection', 'switchable', 'sygnal-factory', ...componentNames].includes(sel) || typeof currentElement.data?.props?.sygnalFactory === 'function' || typeof currentElement.data?.props?.sygnalOptions === 'object';
4293
+ const isComponent = ['collection', 'switchable', 'sygnal-factory', ...componentNames].includes(sel) || typeof currentElement.data?.props?.sygnalFactory === 'function' || isObj(currentElement.data?.props?.sygnalOptions);
4381
4294
  const isCollection = currentElement?.data?.isCollection;
4382
4295
  const isSwitchable = currentElement?.data?.isSwitchable;
4383
4296
  (currentElement.data && currentElement.data.props) || {};
@@ -4421,9 +4334,11 @@ function deepCopyVdom(obj) {
4421
4334
  return { ...obj, children: Array.isArray(obj.children) ? obj.children.map(deepCopyVdom) : undefined, data: obj.data && { ...obj.data, componentsInjected: false } }
4422
4335
  }
4423
4336
 
4424
- function objIsEqual(objA, objB, maxDepth = 5, depth = 0) {
4425
- const obj1 = sanitizeObject(objA);
4426
- const obj2 = sanitizeObject(objB);
4337
+ function propsIsEqual(obj1, obj2) {
4338
+ return objIsEqual(sanitizeObject(obj1))
4339
+ }
4340
+
4341
+ function objIsEqual(obj1, obj2, maxDepth = 5, depth = 0) {
4427
4342
  // Base case: if the current depth exceeds maxDepth, return true
4428
4343
  if (depth > maxDepth) {
4429
4344
  return false;
@@ -4475,11 +4390,89 @@ function objIsEqual(objA, objB, maxDepth = 5, depth = 0) {
4475
4390
  }
4476
4391
 
4477
4392
  function sanitizeObject(obj) {
4478
- if (typeof obj !== 'object' || obj === null) return obj
4479
- const {state, of, from, _reqId, _action, __props, __children, __context, ...sanitized} = obj;
4393
+ if (!isObj(obj)) return obj
4394
+ const {state, of, from, filter, ...sanitized} = obj;
4480
4395
  return sanitized
4481
4396
  }
4482
4397
 
4398
+ function isObj(obj) {
4399
+ return typeof obj === 'object' && obj !== null && !Array.isArray(obj)
4400
+ }
4401
+
4402
+ function __baseSort(a, b, ascending=true) {
4403
+ const direction = ascending ? 1 : -1;
4404
+ switch(true) {
4405
+ case a > b: return 1 * direction
4406
+ case a < b: return -1 * direction
4407
+ default: return 0
4408
+ }
4409
+ }
4410
+
4411
+ function __sortFunctionFromObj(item) {
4412
+ const entries = Object.entries(item);
4413
+ if (entries.length > 1) {
4414
+ console.error('Sort objects can only have one key:', item);
4415
+ return undefined
4416
+ }
4417
+ const entry = entries[0];
4418
+ const [field, directionRaw] = entry;
4419
+ if (!['string', 'number'].includes(typeof directionRaw)) {
4420
+ console.error('Sort object properties must be a string or number:', item);
4421
+ return undefined
4422
+ }
4423
+ let ascending = true;
4424
+ if (typeof directionRaw === 'string') {
4425
+ if (!['asc', 'dec'].includes(directionRaw.toLowerCase())) {
4426
+ console.error('Sort object string values must be asc or dec:', item);
4427
+ return undefined
4428
+ }
4429
+ ascending = directionRaw.toLowerCase() === 'asc';
4430
+ }
4431
+ if (typeof directionRaw === 'number') {
4432
+ if (directionRaw !== 1 && directionRaw !== -1) {
4433
+ console.error('Sort object number values must be 1 or -1:', item);
4434
+ return undefined
4435
+ }
4436
+ ascending = directionRaw === 1;
4437
+ }
4438
+ return (a, b) => __baseSort(a[field], b[field], ascending)
4439
+ }
4440
+
4441
+ function sortFunctionFromProp(sortProp) {
4442
+ if (!sortProp) return undefined
4443
+ const propType = typeof sortProp;
4444
+ // if function do nothing
4445
+ if (propType === 'function') return sortProp
4446
+ if (propType === 'string') {
4447
+ // if passed either 'asc' or 'dec' sort on the entire item
4448
+ if (sortProp.toLowerCase() === 'asc' || sortProp.toLowerCase() === 'dec') {
4449
+ const ascending = sortProp.toLowerCase() === 'asc';
4450
+ return (a, b) => __baseSort(a, b, ascending)
4451
+ }
4452
+ // assume it's a field/property name, and sort it ascending
4453
+ const field = sortProp;
4454
+ return (a, b) => __baseSort(a[field], b[field], true)
4455
+ } else if (Array.isArray(sortProp)) {
4456
+ const sorters = sortProp.map(item => {
4457
+ if (typeof item === 'function') return item
4458
+ if (typeof item === 'string' && !['asc', 'dec'].includes(item.toLowerCase())) return (a, b) => __baseSort(a[item], b[item], true)
4459
+ if (isObj(item)) {
4460
+ return __sortFunctionFromObj(item)
4461
+ }
4462
+ });
4463
+
4464
+ return (a, b) => sorters.filter(sorter => typeof sorter === 'function').reduce((comparisonSoFar, currentSorter) => {
4465
+ if (comparisonSoFar !== 0) return comparisonSoFar
4466
+ return currentSorter(a, b)
4467
+ }, 0)
4468
+ } else if (isObj(sortProp)) {
4469
+ return __sortFunctionFromObj(sortProp)
4470
+ } else {
4471
+ console.error('Invalid sort option (ignoring):', item);
4472
+ return undefined
4473
+ }
4474
+ }
4475
+
4483
4476
  function driverFromAsync(promiseReturningFunction, opts = {}) {
4484
4477
  const {
4485
4478
  selector: selectorProperty = 'category',
@@ -4693,10 +4686,10 @@ function logDriver(out$) {
4693
4686
  function run(app, drivers={}, options={}) {
4694
4687
  const { mountPoint='#root', fragments=true } = options;
4695
4688
  if (!app.isSygnalComponent) {
4696
- const name = app.name || "FUNCTIONAL_COMPONENT";
4689
+ const name = app.name || app.componentName || app.label || "FUNCTIONAL_COMPONENT";
4697
4690
  const view = app;
4698
- const { model, intent, context, children, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug } = app;
4699
- const options = { name, view, model, intent, context, children, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug };
4691
+ const { model, intent, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug } = app;
4692
+ const options = { name, view, model, intent, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug };
4700
4693
 
4701
4694
  app = component(options);
4702
4695
  }