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/README.md +685 -220
- package/dist/index.cjs.js +301 -308
- package/dist/index.esm.js +301 -308
- package/dist/jsx.cjs.js +4 -4
- package/dist/jsx.esm.js +4 -4
- package/dist/sygnal.min.js +1 -1
- package/package.json +1 -1
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
|
|
3148
|
-
const
|
|
3149
|
-
const
|
|
3150
|
-
const
|
|
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 &&
|
|
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 (
|
|
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
|
-
//
|
|
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
|
-
//
|
|
3226
|
-
//
|
|
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,
|
|
3243
|
-
if (!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.
|
|
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.
|
|
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.
|
|
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) && (
|
|
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(
|
|
3487
|
-
const parentContext$ = this.sources.__parentContext$.startWith({}).compose(_default$5(
|
|
3488
|
-
if (this.context &&
|
|
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 =
|
|
3494
|
-
const 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(
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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.
|
|
3647
|
-
const
|
|
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][
|
|
3525
|
+
acc[source][peerName] = peer$[source];
|
|
3651
3526
|
} else {
|
|
3652
|
-
acc[source].push(
|
|
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.
|
|
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.
|
|
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]
|
|
3715
|
-
this.sinks[this.
|
|
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
|
|
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,
|
|
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,
|
|
3651
|
+
const reduced = reducer(enhancedState, data, next, extra);
|
|
3751
3652
|
const type = typeof reduced;
|
|
3752
|
-
|
|
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 ||
|
|
3677
|
+
if (!this.calculated || !isObj(state) || Array.isArray(state)) return state
|
|
3779
3678
|
if (state === lastState) {
|
|
3780
3679
|
return lastResult
|
|
3781
3680
|
}
|
|
3782
|
-
if (
|
|
3783
|
-
|
|
3784
|
-
|
|
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
|
|
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 ||
|
|
3716
|
+
if (!incomingState || !isObj(incomingState) || Array.isArray(incomingState)) return incomingState
|
|
3810
3717
|
const state = this.storeCalculatedInState ? this.addCalculated(incomingState) : incomingState;
|
|
3811
|
-
const {
|
|
3812
|
-
|
|
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.
|
|
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.
|
|
3742
|
+
renderParams.props = this.sources.props$.compose(_default$5(propsIsEqual));
|
|
3837
3743
|
}
|
|
3838
3744
|
|
|
3839
3745
|
if (this.sources.children$) {
|
|
3840
|
-
renderParams.
|
|
3746
|
+
renderParams.children = this.sources.children$.compose(_default$5(objIsEqual));
|
|
3841
3747
|
}
|
|
3842
3748
|
|
|
3843
3749
|
if (this.context$) {
|
|
3844
|
-
renderParams.
|
|
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')
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
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).
|
|
3804
|
+
Object.entries(sinks).forEach(([name, stream]) => {
|
|
3898
3805
|
sinkArrsByType[name] ||= [];
|
|
3899
|
-
if (name
|
|
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
|
-
|
|
3882
|
+
let filter = typeof props.filter === 'function' ? props.filter : undefined;
|
|
3883
|
+
let sort = sortFunctionFromProp(props.sort);
|
|
3970
3884
|
|
|
3971
|
-
const
|
|
3972
|
-
|
|
3973
|
-
|
|
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
|
|
3977
|
-
const stateField
|
|
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 =
|
|
3917
|
+
const name = collectionOf.componentName || collectionOf.label || collectionOf.name || 'FUNCTION_COMPONENT';
|
|
3992
3918
|
const view = collectionOf;
|
|
3993
|
-
const { model, intent, context,
|
|
3994
|
-
const options = { name, view, model, intent, context,
|
|
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
|
-
|
|
4017
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
4094
|
-
.map((
|
|
4095
|
-
return
|
|
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
|
|
4099
|
-
const stateField
|
|
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 (
|
|
4113
|
-
|
|
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 (
|
|
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 =
|
|
4069
|
+
const name = current.componentName || current.label || current.name || 'FUNCTION_COMPONENT';
|
|
4149
4070
|
const view = current;
|
|
4150
|
-
const { model, intent, context,
|
|
4151
|
-
const options = { name, view, model, intent, context,
|
|
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 (
|
|
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
|
|
4173
|
-
.map((
|
|
4174
|
-
return
|
|
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(
|
|
4097
|
+
const stateSource = new StateSource(state$);
|
|
4178
4098
|
const stateField = props.state;
|
|
4179
4099
|
|
|
4180
|
-
if (typeof props.sygnalFactory !== 'function' &&
|
|
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 (
|
|
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 (
|
|
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' ||
|
|
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)
|
|
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)
|
|
4351
|
-
if (
|
|
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' ||
|
|
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
|
|
4425
|
-
|
|
4426
|
-
|
|
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 (
|
|
4479
|
-
const {state, of, from,
|
|
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,
|
|
4699
|
-
const options = { name, view, model, intent, context,
|
|
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
|
}
|