sygnal 5.2.0 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/astro/client.cjs.js +29 -2
- package/dist/astro/client.mjs +29 -2
- package/dist/astro/server.cjs.js +18 -0
- package/dist/astro/server.mjs +18 -0
- package/dist/index.cjs.js +107 -2
- package/dist/index.d.ts +34 -0
- package/dist/index.esm.js +105 -3
- package/dist/sygnal.min.js +1 -1
- package/dist/vike/+config.cjs.js +5 -1
- package/dist/vike/+config.js +5 -1
- package/dist/vike/ClientOnly.cjs.js +34 -0
- package/dist/vike/ClientOnly.mjs +32 -0
- package/dist/vike/onRenderClient.cjs.js +292 -35
- package/dist/vike/onRenderClient.mjs +292 -35
- package/dist/vike/onRenderHtml.cjs.js +71 -34
- package/dist/vike/onRenderHtml.mjs +71 -34
- package/dist/vite/plugin.cjs.js +6 -4
- package/dist/vite/plugin.mjs +6 -4
- package/package.json +5 -1
- package/src/component.ts +29 -2
- package/src/extra/reducers.ts +64 -0
- package/src/extra/ssr.ts +19 -0
- package/src/index.d.ts +34 -0
- package/src/index.ts +1 -0
- package/src/vike/+config.ts +5 -1
- package/src/vike/ClientOnly.ts +10 -0
- package/src/vike/onRenderClient.ts +319 -36
- package/src/vike/onRenderHtml.ts +77 -33
- package/src/vike/types.ts +2 -0
- package/src/vite/plugin.ts +6 -4
package/README.md
CHANGED
|
@@ -404,7 +404,7 @@ import vikeSygnal from 'sygnal/config'
|
|
|
404
404
|
export default { extends: [vikeSygnal] }
|
|
405
405
|
```
|
|
406
406
|
|
|
407
|
-
Pages are standard Sygnal components in `pages/*/+Page.jsx`. Supports layouts, data fetching, and
|
|
407
|
+
Pages are standard Sygnal components in `pages/*/+Page.jsx`. Supports layouts, data fetching, SPA mode, and `ClientOnly` for browser-only components.
|
|
408
408
|
|
|
409
409
|
### TypeScript
|
|
410
410
|
|
package/dist/astro/client.cjs.js
CHANGED
|
@@ -5532,7 +5532,7 @@ function component(opts) {
|
|
|
5532
5532
|
return returnFunction;
|
|
5533
5533
|
}
|
|
5534
5534
|
class Component {
|
|
5535
|
-
constructor({ name = 'NO NAME', sources, intent, model, hmrActions, context, response, view, peers = {}, components = {}, initialState, calculated, storeCalculatedInState = true, DOMSourceName = 'DOM', stateSourceName = 'STATE', requestSourceName = 'HTTP', onError, debug = false }) {
|
|
5535
|
+
constructor({ name = 'NO NAME', sources, intent, model, hmrActions, context, response, view, peers = {}, components = {}, initialState, calculated, storeCalculatedInState = true, DOMSourceName = 'DOM', stateSourceName = 'STATE', requestSourceName = 'HTTP', isolatedState = false, onError, debug = false }) {
|
|
5536
5536
|
if (!sources || !isObj(sources))
|
|
5537
5537
|
throw new Error(`[${name}] Missing or invalid sources`);
|
|
5538
5538
|
this._componentNumber = COMPONENT_COUNT++;
|
|
@@ -5554,6 +5554,7 @@ class Component {
|
|
|
5554
5554
|
this.requestSourceName = requestSourceName;
|
|
5555
5555
|
this.sourceNames = Object.keys(sources);
|
|
5556
5556
|
this.onError = onError;
|
|
5557
|
+
this.isolatedState = isolatedState;
|
|
5557
5558
|
this._debug = debug;
|
|
5558
5559
|
// Warn if calculated fields shadow base state keys
|
|
5559
5560
|
if (this.calculated && this.initialState
|
|
@@ -5953,7 +5954,7 @@ class Component {
|
|
|
5953
5954
|
const hmrState = ENVIRONMENT?.__SYGNAL_HMR_STATE;
|
|
5954
5955
|
const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState;
|
|
5955
5956
|
const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState };
|
|
5956
|
-
if (this.isSubComponent && this.initialState) {
|
|
5957
|
+
if (this.isSubComponent && this.initialState && !this.isolatedState) {
|
|
5957
5958
|
console.warn(`[${this.name}] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`);
|
|
5958
5959
|
}
|
|
5959
5960
|
const hasInitialState = (typeof effectiveInitialState !== 'undefined');
|
|
@@ -6134,6 +6135,7 @@ class Component {
|
|
|
6134
6135
|
.map((vdom) => processLazy(vdom, this))
|
|
6135
6136
|
.map(processPortals)
|
|
6136
6137
|
.map(processTransitions)
|
|
6138
|
+
.map(processClientOnly)
|
|
6137
6139
|
.compose(this.instantiateSubComponents.bind(this))
|
|
6138
6140
|
.filter((val) => val !== undefined)
|
|
6139
6141
|
.compose(this.renderVdom.bind(this));
|
|
@@ -7111,6 +7113,31 @@ function processTransitions(vnode) {
|
|
|
7111
7113
|
}
|
|
7112
7114
|
return vnode;
|
|
7113
7115
|
}
|
|
7116
|
+
function processClientOnly(vnode) {
|
|
7117
|
+
if (!vnode || !vnode.sel)
|
|
7118
|
+
return vnode;
|
|
7119
|
+
if (vnode.sel === 'clientonly') {
|
|
7120
|
+
// On the client, unwrap to children (render them normally)
|
|
7121
|
+
const children = vnode.children || [];
|
|
7122
|
+
if (children.length === 0)
|
|
7123
|
+
return { sel: 'div', data: {}, children: [] };
|
|
7124
|
+
if (children.length === 1)
|
|
7125
|
+
return processClientOnly(children[0]);
|
|
7126
|
+
// Multiple children: wrap in a div
|
|
7127
|
+
return {
|
|
7128
|
+
sel: 'div',
|
|
7129
|
+
data: {},
|
|
7130
|
+
children: children.map(processClientOnly),
|
|
7131
|
+
text: undefined,
|
|
7132
|
+
elm: undefined,
|
|
7133
|
+
key: undefined,
|
|
7134
|
+
};
|
|
7135
|
+
}
|
|
7136
|
+
if (vnode.children && vnode.children.length > 0) {
|
|
7137
|
+
vnode.children = vnode.children.map(processClientOnly);
|
|
7138
|
+
}
|
|
7139
|
+
return vnode;
|
|
7140
|
+
}
|
|
7114
7141
|
function applyTransitionHooks(vnode, name, duration) {
|
|
7115
7142
|
const existingInsert = vnode.data?.hook?.insert;
|
|
7116
7143
|
const existingRemove = vnode.data?.hook?.remove;
|
package/dist/astro/client.mjs
CHANGED
|
@@ -5530,7 +5530,7 @@ function component(opts) {
|
|
|
5530
5530
|
return returnFunction;
|
|
5531
5531
|
}
|
|
5532
5532
|
class Component {
|
|
5533
|
-
constructor({ name = 'NO NAME', sources, intent, model, hmrActions, context, response, view, peers = {}, components = {}, initialState, calculated, storeCalculatedInState = true, DOMSourceName = 'DOM', stateSourceName = 'STATE', requestSourceName = 'HTTP', onError, debug = false }) {
|
|
5533
|
+
constructor({ name = 'NO NAME', sources, intent, model, hmrActions, context, response, view, peers = {}, components = {}, initialState, calculated, storeCalculatedInState = true, DOMSourceName = 'DOM', stateSourceName = 'STATE', requestSourceName = 'HTTP', isolatedState = false, onError, debug = false }) {
|
|
5534
5534
|
if (!sources || !isObj(sources))
|
|
5535
5535
|
throw new Error(`[${name}] Missing or invalid sources`);
|
|
5536
5536
|
this._componentNumber = COMPONENT_COUNT++;
|
|
@@ -5552,6 +5552,7 @@ class Component {
|
|
|
5552
5552
|
this.requestSourceName = requestSourceName;
|
|
5553
5553
|
this.sourceNames = Object.keys(sources);
|
|
5554
5554
|
this.onError = onError;
|
|
5555
|
+
this.isolatedState = isolatedState;
|
|
5555
5556
|
this._debug = debug;
|
|
5556
5557
|
// Warn if calculated fields shadow base state keys
|
|
5557
5558
|
if (this.calculated && this.initialState
|
|
@@ -5951,7 +5952,7 @@ class Component {
|
|
|
5951
5952
|
const hmrState = ENVIRONMENT?.__SYGNAL_HMR_STATE;
|
|
5952
5953
|
const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState;
|
|
5953
5954
|
const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState };
|
|
5954
|
-
if (this.isSubComponent && this.initialState) {
|
|
5955
|
+
if (this.isSubComponent && this.initialState && !this.isolatedState) {
|
|
5955
5956
|
console.warn(`[${this.name}] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`);
|
|
5956
5957
|
}
|
|
5957
5958
|
const hasInitialState = (typeof effectiveInitialState !== 'undefined');
|
|
@@ -6132,6 +6133,7 @@ class Component {
|
|
|
6132
6133
|
.map((vdom) => processLazy(vdom, this))
|
|
6133
6134
|
.map(processPortals)
|
|
6134
6135
|
.map(processTransitions)
|
|
6136
|
+
.map(processClientOnly)
|
|
6135
6137
|
.compose(this.instantiateSubComponents.bind(this))
|
|
6136
6138
|
.filter((val) => val !== undefined)
|
|
6137
6139
|
.compose(this.renderVdom.bind(this));
|
|
@@ -7109,6 +7111,31 @@ function processTransitions(vnode) {
|
|
|
7109
7111
|
}
|
|
7110
7112
|
return vnode;
|
|
7111
7113
|
}
|
|
7114
|
+
function processClientOnly(vnode) {
|
|
7115
|
+
if (!vnode || !vnode.sel)
|
|
7116
|
+
return vnode;
|
|
7117
|
+
if (vnode.sel === 'clientonly') {
|
|
7118
|
+
// On the client, unwrap to children (render them normally)
|
|
7119
|
+
const children = vnode.children || [];
|
|
7120
|
+
if (children.length === 0)
|
|
7121
|
+
return { sel: 'div', data: {}, children: [] };
|
|
7122
|
+
if (children.length === 1)
|
|
7123
|
+
return processClientOnly(children[0]);
|
|
7124
|
+
// Multiple children: wrap in a div
|
|
7125
|
+
return {
|
|
7126
|
+
sel: 'div',
|
|
7127
|
+
data: {},
|
|
7128
|
+
children: children.map(processClientOnly),
|
|
7129
|
+
text: undefined,
|
|
7130
|
+
elm: undefined,
|
|
7131
|
+
key: undefined,
|
|
7132
|
+
};
|
|
7133
|
+
}
|
|
7134
|
+
if (vnode.children && vnode.children.length > 0) {
|
|
7135
|
+
vnode.children = vnode.children.map(processClientOnly);
|
|
7136
|
+
}
|
|
7137
|
+
return vnode;
|
|
7138
|
+
}
|
|
7112
7139
|
function applyTransitionHooks(vnode, name, duration) {
|
|
7113
7140
|
const existingInsert = vnode.data?.hook?.insert;
|
|
7114
7141
|
const existingRemove = vnode.data?.hook?.remove;
|
package/dist/astro/server.cjs.js
CHANGED
|
@@ -172,6 +172,24 @@ function processSSRTree(vnode, context, parentState) {
|
|
|
172
172
|
key: undefined,
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
|
+
// ClientOnly: render fallback during SSR, skip children (they need a browser)
|
|
176
|
+
if (sel === 'clientonly') {
|
|
177
|
+
const props = vnode.data?.props || {};
|
|
178
|
+
const fallback = props.fallback;
|
|
179
|
+
if (fallback) {
|
|
180
|
+
// fallback can be a VNode or a string
|
|
181
|
+
return processSSRTree(fallback, context, parentState);
|
|
182
|
+
}
|
|
183
|
+
// No fallback — render an empty placeholder div
|
|
184
|
+
return {
|
|
185
|
+
sel: 'div',
|
|
186
|
+
data: { attrs: { 'data-sygnal-clientonly': '' } },
|
|
187
|
+
children: [],
|
|
188
|
+
text: undefined,
|
|
189
|
+
elm: undefined,
|
|
190
|
+
key: undefined,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
175
193
|
// Slot: unwrap to children
|
|
176
194
|
if (sel === 'slot') {
|
|
177
195
|
const children = vnode.children || [];
|
package/dist/astro/server.mjs
CHANGED
|
@@ -168,6 +168,24 @@ function processSSRTree(vnode, context, parentState) {
|
|
|
168
168
|
key: undefined,
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
|
+
// ClientOnly: render fallback during SSR, skip children (they need a browser)
|
|
172
|
+
if (sel === 'clientonly') {
|
|
173
|
+
const props = vnode.data?.props || {};
|
|
174
|
+
const fallback = props.fallback;
|
|
175
|
+
if (fallback) {
|
|
176
|
+
// fallback can be a VNode or a string
|
|
177
|
+
return processSSRTree(fallback, context, parentState);
|
|
178
|
+
}
|
|
179
|
+
// No fallback — render an empty placeholder div
|
|
180
|
+
return {
|
|
181
|
+
sel: 'div',
|
|
182
|
+
data: { attrs: { 'data-sygnal-clientonly': '' } },
|
|
183
|
+
children: [],
|
|
184
|
+
text: undefined,
|
|
185
|
+
elm: undefined,
|
|
186
|
+
key: undefined,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
171
189
|
// Slot: unwrap to children
|
|
172
190
|
if (sel === 'slot') {
|
|
173
191
|
const children = vnode.children || [];
|
package/dist/index.cjs.js
CHANGED
|
@@ -2270,7 +2270,7 @@ function component(opts) {
|
|
|
2270
2270
|
return returnFunction;
|
|
2271
2271
|
}
|
|
2272
2272
|
class Component {
|
|
2273
|
-
constructor({ name = 'NO NAME', sources, intent, model, hmrActions, context, response, view, peers = {}, components = {}, initialState, calculated, storeCalculatedInState = true, DOMSourceName = 'DOM', stateSourceName = 'STATE', requestSourceName = 'HTTP', onError, debug = false }) {
|
|
2273
|
+
constructor({ name = 'NO NAME', sources, intent, model, hmrActions, context, response, view, peers = {}, components = {}, initialState, calculated, storeCalculatedInState = true, DOMSourceName = 'DOM', stateSourceName = 'STATE', requestSourceName = 'HTTP', isolatedState = false, onError, debug = false }) {
|
|
2274
2274
|
if (!sources || !isObj(sources))
|
|
2275
2275
|
throw new Error(`[${name}] Missing or invalid sources`);
|
|
2276
2276
|
this._componentNumber = COMPONENT_COUNT++;
|
|
@@ -2292,6 +2292,7 @@ class Component {
|
|
|
2292
2292
|
this.requestSourceName = requestSourceName;
|
|
2293
2293
|
this.sourceNames = Object.keys(sources);
|
|
2294
2294
|
this.onError = onError;
|
|
2295
|
+
this.isolatedState = isolatedState;
|
|
2295
2296
|
this._debug = debug;
|
|
2296
2297
|
// Warn if calculated fields shadow base state keys
|
|
2297
2298
|
if (this.calculated && this.initialState
|
|
@@ -2691,7 +2692,7 @@ class Component {
|
|
|
2691
2692
|
const hmrState = ENVIRONMENT?.__SYGNAL_HMR_STATE;
|
|
2692
2693
|
const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState;
|
|
2693
2694
|
const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState };
|
|
2694
|
-
if (this.isSubComponent && this.initialState) {
|
|
2695
|
+
if (this.isSubComponent && this.initialState && !this.isolatedState) {
|
|
2695
2696
|
console.warn(`[${this.name}] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`);
|
|
2696
2697
|
}
|
|
2697
2698
|
const hasInitialState = (typeof effectiveInitialState !== 'undefined');
|
|
@@ -2872,6 +2873,7 @@ class Component {
|
|
|
2872
2873
|
.map((vdom) => processLazy(vdom, this))
|
|
2873
2874
|
.map(processPortals)
|
|
2874
2875
|
.map(processTransitions)
|
|
2876
|
+
.map(processClientOnly)
|
|
2875
2877
|
.compose(this.instantiateSubComponents.bind(this))
|
|
2876
2878
|
.filter((val) => val !== undefined)
|
|
2877
2879
|
.compose(this.renderVdom.bind(this));
|
|
@@ -3849,6 +3851,31 @@ function processTransitions(vnode) {
|
|
|
3849
3851
|
}
|
|
3850
3852
|
return vnode;
|
|
3851
3853
|
}
|
|
3854
|
+
function processClientOnly(vnode) {
|
|
3855
|
+
if (!vnode || !vnode.sel)
|
|
3856
|
+
return vnode;
|
|
3857
|
+
if (vnode.sel === 'clientonly') {
|
|
3858
|
+
// On the client, unwrap to children (render them normally)
|
|
3859
|
+
const children = vnode.children || [];
|
|
3860
|
+
if (children.length === 0)
|
|
3861
|
+
return { sel: 'div', data: {}, children: [] };
|
|
3862
|
+
if (children.length === 1)
|
|
3863
|
+
return processClientOnly(children[0]);
|
|
3864
|
+
// Multiple children: wrap in a div
|
|
3865
|
+
return {
|
|
3866
|
+
sel: 'div',
|
|
3867
|
+
data: {},
|
|
3868
|
+
children: children.map(processClientOnly),
|
|
3869
|
+
text: undefined,
|
|
3870
|
+
elm: undefined,
|
|
3871
|
+
key: undefined,
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3874
|
+
if (vnode.children && vnode.children.length > 0) {
|
|
3875
|
+
vnode.children = vnode.children.map(processClientOnly);
|
|
3876
|
+
}
|
|
3877
|
+
return vnode;
|
|
3878
|
+
}
|
|
3852
3879
|
function applyTransitionHooks(vnode, name, duration) {
|
|
3853
3880
|
const existingInsert = vnode.data?.hook?.insert;
|
|
3854
3881
|
const existingRemove = vnode.data?.hook?.remove;
|
|
@@ -5842,6 +5869,63 @@ function renderComponent(componentDef, options = {}) {
|
|
|
5842
5869
|
};
|
|
5843
5870
|
}
|
|
5844
5871
|
|
|
5872
|
+
/**
|
|
5873
|
+
* Reducer helpers for common state update patterns.
|
|
5874
|
+
*
|
|
5875
|
+
* These reduce boilerplate in model definitions by providing
|
|
5876
|
+
* shorthand factories for the most frequent reducer shapes.
|
|
5877
|
+
*/
|
|
5878
|
+
// ── set() ──────────────────────────────────────────────────────────
|
|
5879
|
+
/**
|
|
5880
|
+
* Create a reducer that merges a partial update into state.
|
|
5881
|
+
*
|
|
5882
|
+
* Static form — merge a fixed object:
|
|
5883
|
+
* set({ isEditing: true })
|
|
5884
|
+
*
|
|
5885
|
+
* Dynamic form — function receives (state, data, next, props) and
|
|
5886
|
+
* returns the partial update to merge:
|
|
5887
|
+
* set((state, title) => ({ title }))
|
|
5888
|
+
*/
|
|
5889
|
+
function set(partial) {
|
|
5890
|
+
if (typeof partial === 'function') {
|
|
5891
|
+
return (state, data, next, props) => ({
|
|
5892
|
+
...state,
|
|
5893
|
+
...partial(state, data, next, props),
|
|
5894
|
+
});
|
|
5895
|
+
}
|
|
5896
|
+
return (state) => ({ ...state, ...partial });
|
|
5897
|
+
}
|
|
5898
|
+
// ── toggle() ───────────────────────────────────────────────────────
|
|
5899
|
+
/**
|
|
5900
|
+
* Create a reducer that toggles a boolean field on state.
|
|
5901
|
+
*
|
|
5902
|
+
* toggle('showModal')
|
|
5903
|
+
* // equivalent to: (state) => ({ ...state, showModal: !state.showModal })
|
|
5904
|
+
*/
|
|
5905
|
+
function toggle(field) {
|
|
5906
|
+
return (state) => ({ ...state, [field]: !state[field] });
|
|
5907
|
+
}
|
|
5908
|
+
// ── emit() ─────────────────────────────────────────────────────────
|
|
5909
|
+
/**
|
|
5910
|
+
* Create a model entry that emits an EVENTS bus event.
|
|
5911
|
+
*
|
|
5912
|
+
* With static data:
|
|
5913
|
+
* emit('DELETE_LANE', { laneId: 42 })
|
|
5914
|
+
*
|
|
5915
|
+
* With dynamic data derived from state:
|
|
5916
|
+
* emit('DELETE_LANE', (state) => ({ laneId: state.id }))
|
|
5917
|
+
*
|
|
5918
|
+
* Fire-and-forget (no data):
|
|
5919
|
+
* emit('REFRESH')
|
|
5920
|
+
*/
|
|
5921
|
+
function emit(type, data) {
|
|
5922
|
+
return {
|
|
5923
|
+
EVENTS: typeof data === 'function'
|
|
5924
|
+
? (state, actionData, next, props) => ({ type, data: data(state, actionData, next, props) })
|
|
5925
|
+
: () => ({ type, data }),
|
|
5926
|
+
};
|
|
5927
|
+
}
|
|
5928
|
+
|
|
5845
5929
|
/**
|
|
5846
5930
|
* Server-Side Rendering utilities for Sygnal components.
|
|
5847
5931
|
*
|
|
@@ -6012,6 +6096,24 @@ function processSSRTree(vnode, context, parentState) {
|
|
|
6012
6096
|
key: undefined,
|
|
6013
6097
|
};
|
|
6014
6098
|
}
|
|
6099
|
+
// ClientOnly: render fallback during SSR, skip children (they need a browser)
|
|
6100
|
+
if (sel === 'clientonly') {
|
|
6101
|
+
const props = vnode.data?.props || {};
|
|
6102
|
+
const fallback = props.fallback;
|
|
6103
|
+
if (fallback) {
|
|
6104
|
+
// fallback can be a VNode or a string
|
|
6105
|
+
return processSSRTree(fallback, context, parentState);
|
|
6106
|
+
}
|
|
6107
|
+
// No fallback — render an empty placeholder div
|
|
6108
|
+
return {
|
|
6109
|
+
sel: 'div',
|
|
6110
|
+
data: { attrs: { 'data-sygnal-clientonly': '' } },
|
|
6111
|
+
children: [],
|
|
6112
|
+
text: undefined,
|
|
6113
|
+
elm: undefined,
|
|
6114
|
+
key: undefined,
|
|
6115
|
+
};
|
|
6116
|
+
}
|
|
6015
6117
|
// Slot: unwrap to children
|
|
6016
6118
|
if (sel === 'slot') {
|
|
6017
6119
|
const children = vnode.children || [];
|
|
@@ -6517,6 +6619,7 @@ exports.createElement = createElement;
|
|
|
6517
6619
|
exports.createRef = createRef;
|
|
6518
6620
|
exports.createRef$ = createRef$;
|
|
6519
6621
|
exports.driverFromAsync = driverFromAsync;
|
|
6622
|
+
exports.emit = emit;
|
|
6520
6623
|
exports.enableHMR = enableHMR;
|
|
6521
6624
|
exports.exactState = exactState;
|
|
6522
6625
|
exports.getDevTools = getDevTools;
|
|
@@ -6530,6 +6633,8 @@ exports.processForm = processForm;
|
|
|
6530
6633
|
exports.renderComponent = renderComponent;
|
|
6531
6634
|
exports.renderToString = renderToString;
|
|
6532
6635
|
exports.run = run;
|
|
6636
|
+
exports.set = set;
|
|
6533
6637
|
exports.switchable = switchable;
|
|
6534
6638
|
exports.thunk = thunk;
|
|
6639
|
+
exports.toggle = toggle;
|
|
6535
6640
|
exports.xs = xs;
|
package/dist/index.d.ts
CHANGED
|
@@ -378,6 +378,40 @@ export function enableHMR<STATE = any, DRIVERS = {}>(
|
|
|
378
378
|
export function classes(...classes: ClassesType): string
|
|
379
379
|
export function exactState<STATE>(): <ACTUAL extends STATE>(state: ExactShape<STATE, ACTUAL>) => STATE
|
|
380
380
|
|
|
381
|
+
// ── Reducer helpers ────────────────────────────────────────────────
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Create a reducer that merges a partial update into state.
|
|
385
|
+
*
|
|
386
|
+
* Static form — merge a fixed object:
|
|
387
|
+
* `set({ isEditing: true })`
|
|
388
|
+
*
|
|
389
|
+
* Dynamic form — function receives (state, data, next, props) and
|
|
390
|
+
* returns the partial update to merge:
|
|
391
|
+
* `set((state, title) => ({ title }))`
|
|
392
|
+
*/
|
|
393
|
+
export function set<S = any>(
|
|
394
|
+
partial: Partial<S> | ((state: S, data: any, next: Function, props: any) => Partial<S>)
|
|
395
|
+
): (state: S, data: any, next: Function, props: any) => S
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Create a reducer that toggles a boolean field on state.
|
|
399
|
+
*
|
|
400
|
+
* `toggle('showModal')`
|
|
401
|
+
*/
|
|
402
|
+
export function toggle<S = any>(field: keyof S & string): (state: S) => S
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Create a model entry that emits an EVENTS bus event.
|
|
406
|
+
*
|
|
407
|
+
* `emit('DELETE_LANE', (state) => ({ laneId: state.id }))`
|
|
408
|
+
* `emit('REFRESH')`
|
|
409
|
+
*/
|
|
410
|
+
export function emit(
|
|
411
|
+
type: string,
|
|
412
|
+
data?: any | ((state: any, actionData: any, next: Function, props: any) => any)
|
|
413
|
+
): { EVENTS: (state: any, actionData: any, next: Function, props: any) => { type: string; data: any } }
|
|
414
|
+
|
|
381
415
|
/**
|
|
382
416
|
* Any object with an events() method (e.g., DOM.select('form')).
|
|
383
417
|
* Uses permissive signature to be compatible with MainDOMSource's overloaded events().
|
package/dist/index.esm.js
CHANGED
|
@@ -2253,7 +2253,7 @@ function component(opts) {
|
|
|
2253
2253
|
return returnFunction;
|
|
2254
2254
|
}
|
|
2255
2255
|
class Component {
|
|
2256
|
-
constructor({ name = 'NO NAME', sources, intent, model, hmrActions, context, response, view, peers = {}, components = {}, initialState, calculated, storeCalculatedInState = true, DOMSourceName = 'DOM', stateSourceName = 'STATE', requestSourceName = 'HTTP', onError, debug = false }) {
|
|
2256
|
+
constructor({ name = 'NO NAME', sources, intent, model, hmrActions, context, response, view, peers = {}, components = {}, initialState, calculated, storeCalculatedInState = true, DOMSourceName = 'DOM', stateSourceName = 'STATE', requestSourceName = 'HTTP', isolatedState = false, onError, debug = false }) {
|
|
2257
2257
|
if (!sources || !isObj(sources))
|
|
2258
2258
|
throw new Error(`[${name}] Missing or invalid sources`);
|
|
2259
2259
|
this._componentNumber = COMPONENT_COUNT++;
|
|
@@ -2275,6 +2275,7 @@ class Component {
|
|
|
2275
2275
|
this.requestSourceName = requestSourceName;
|
|
2276
2276
|
this.sourceNames = Object.keys(sources);
|
|
2277
2277
|
this.onError = onError;
|
|
2278
|
+
this.isolatedState = isolatedState;
|
|
2278
2279
|
this._debug = debug;
|
|
2279
2280
|
// Warn if calculated fields shadow base state keys
|
|
2280
2281
|
if (this.calculated && this.initialState
|
|
@@ -2674,7 +2675,7 @@ class Component {
|
|
|
2674
2675
|
const hmrState = ENVIRONMENT?.__SYGNAL_HMR_STATE;
|
|
2675
2676
|
const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState;
|
|
2676
2677
|
const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState };
|
|
2677
|
-
if (this.isSubComponent && this.initialState) {
|
|
2678
|
+
if (this.isSubComponent && this.initialState && !this.isolatedState) {
|
|
2678
2679
|
console.warn(`[${this.name}] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`);
|
|
2679
2680
|
}
|
|
2680
2681
|
const hasInitialState = (typeof effectiveInitialState !== 'undefined');
|
|
@@ -2855,6 +2856,7 @@ class Component {
|
|
|
2855
2856
|
.map((vdom) => processLazy(vdom, this))
|
|
2856
2857
|
.map(processPortals)
|
|
2857
2858
|
.map(processTransitions)
|
|
2859
|
+
.map(processClientOnly)
|
|
2858
2860
|
.compose(this.instantiateSubComponents.bind(this))
|
|
2859
2861
|
.filter((val) => val !== undefined)
|
|
2860
2862
|
.compose(this.renderVdom.bind(this));
|
|
@@ -3832,6 +3834,31 @@ function processTransitions(vnode) {
|
|
|
3832
3834
|
}
|
|
3833
3835
|
return vnode;
|
|
3834
3836
|
}
|
|
3837
|
+
function processClientOnly(vnode) {
|
|
3838
|
+
if (!vnode || !vnode.sel)
|
|
3839
|
+
return vnode;
|
|
3840
|
+
if (vnode.sel === 'clientonly') {
|
|
3841
|
+
// On the client, unwrap to children (render them normally)
|
|
3842
|
+
const children = vnode.children || [];
|
|
3843
|
+
if (children.length === 0)
|
|
3844
|
+
return { sel: 'div', data: {}, children: [] };
|
|
3845
|
+
if (children.length === 1)
|
|
3846
|
+
return processClientOnly(children[0]);
|
|
3847
|
+
// Multiple children: wrap in a div
|
|
3848
|
+
return {
|
|
3849
|
+
sel: 'div',
|
|
3850
|
+
data: {},
|
|
3851
|
+
children: children.map(processClientOnly),
|
|
3852
|
+
text: undefined,
|
|
3853
|
+
elm: undefined,
|
|
3854
|
+
key: undefined,
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3857
|
+
if (vnode.children && vnode.children.length > 0) {
|
|
3858
|
+
vnode.children = vnode.children.map(processClientOnly);
|
|
3859
|
+
}
|
|
3860
|
+
return vnode;
|
|
3861
|
+
}
|
|
3835
3862
|
function applyTransitionHooks(vnode, name, duration) {
|
|
3836
3863
|
const existingInsert = vnode.data?.hook?.insert;
|
|
3837
3864
|
const existingRemove = vnode.data?.hook?.remove;
|
|
@@ -5825,6 +5852,63 @@ function renderComponent(componentDef, options = {}) {
|
|
|
5825
5852
|
};
|
|
5826
5853
|
}
|
|
5827
5854
|
|
|
5855
|
+
/**
|
|
5856
|
+
* Reducer helpers for common state update patterns.
|
|
5857
|
+
*
|
|
5858
|
+
* These reduce boilerplate in model definitions by providing
|
|
5859
|
+
* shorthand factories for the most frequent reducer shapes.
|
|
5860
|
+
*/
|
|
5861
|
+
// ── set() ──────────────────────────────────────────────────────────
|
|
5862
|
+
/**
|
|
5863
|
+
* Create a reducer that merges a partial update into state.
|
|
5864
|
+
*
|
|
5865
|
+
* Static form — merge a fixed object:
|
|
5866
|
+
* set({ isEditing: true })
|
|
5867
|
+
*
|
|
5868
|
+
* Dynamic form — function receives (state, data, next, props) and
|
|
5869
|
+
* returns the partial update to merge:
|
|
5870
|
+
* set((state, title) => ({ title }))
|
|
5871
|
+
*/
|
|
5872
|
+
function set(partial) {
|
|
5873
|
+
if (typeof partial === 'function') {
|
|
5874
|
+
return (state, data, next, props) => ({
|
|
5875
|
+
...state,
|
|
5876
|
+
...partial(state, data, next, props),
|
|
5877
|
+
});
|
|
5878
|
+
}
|
|
5879
|
+
return (state) => ({ ...state, ...partial });
|
|
5880
|
+
}
|
|
5881
|
+
// ── toggle() ───────────────────────────────────────────────────────
|
|
5882
|
+
/**
|
|
5883
|
+
* Create a reducer that toggles a boolean field on state.
|
|
5884
|
+
*
|
|
5885
|
+
* toggle('showModal')
|
|
5886
|
+
* // equivalent to: (state) => ({ ...state, showModal: !state.showModal })
|
|
5887
|
+
*/
|
|
5888
|
+
function toggle(field) {
|
|
5889
|
+
return (state) => ({ ...state, [field]: !state[field] });
|
|
5890
|
+
}
|
|
5891
|
+
// ── emit() ─────────────────────────────────────────────────────────
|
|
5892
|
+
/**
|
|
5893
|
+
* Create a model entry that emits an EVENTS bus event.
|
|
5894
|
+
*
|
|
5895
|
+
* With static data:
|
|
5896
|
+
* emit('DELETE_LANE', { laneId: 42 })
|
|
5897
|
+
*
|
|
5898
|
+
* With dynamic data derived from state:
|
|
5899
|
+
* emit('DELETE_LANE', (state) => ({ laneId: state.id }))
|
|
5900
|
+
*
|
|
5901
|
+
* Fire-and-forget (no data):
|
|
5902
|
+
* emit('REFRESH')
|
|
5903
|
+
*/
|
|
5904
|
+
function emit(type, data) {
|
|
5905
|
+
return {
|
|
5906
|
+
EVENTS: typeof data === 'function'
|
|
5907
|
+
? (state, actionData, next, props) => ({ type, data: data(state, actionData, next, props) })
|
|
5908
|
+
: () => ({ type, data }),
|
|
5909
|
+
};
|
|
5910
|
+
}
|
|
5911
|
+
|
|
5828
5912
|
/**
|
|
5829
5913
|
* Server-Side Rendering utilities for Sygnal components.
|
|
5830
5914
|
*
|
|
@@ -5995,6 +6079,24 @@ function processSSRTree(vnode, context, parentState) {
|
|
|
5995
6079
|
key: undefined,
|
|
5996
6080
|
};
|
|
5997
6081
|
}
|
|
6082
|
+
// ClientOnly: render fallback during SSR, skip children (they need a browser)
|
|
6083
|
+
if (sel === 'clientonly') {
|
|
6084
|
+
const props = vnode.data?.props || {};
|
|
6085
|
+
const fallback = props.fallback;
|
|
6086
|
+
if (fallback) {
|
|
6087
|
+
// fallback can be a VNode or a string
|
|
6088
|
+
return processSSRTree(fallback, context, parentState);
|
|
6089
|
+
}
|
|
6090
|
+
// No fallback — render an empty placeholder div
|
|
6091
|
+
return {
|
|
6092
|
+
sel: 'div',
|
|
6093
|
+
data: { attrs: { 'data-sygnal-clientonly': '' } },
|
|
6094
|
+
children: [],
|
|
6095
|
+
text: undefined,
|
|
6096
|
+
elm: undefined,
|
|
6097
|
+
key: undefined,
|
|
6098
|
+
};
|
|
6099
|
+
}
|
|
5998
6100
|
// Slot: unwrap to children
|
|
5999
6101
|
if (sel === 'slot') {
|
|
6000
6102
|
const children = vnode.children || [];
|
|
@@ -6474,4 +6576,4 @@ function buildAttributes(data, selectorId, selectorClasses) {
|
|
|
6474
6576
|
return result;
|
|
6475
6577
|
}
|
|
6476
6578
|
|
|
6477
|
-
export { ABORT, Collection, MainDOMSource, MockedDOMSource, Portal, Slot, Suspense, Switchable, Transition, classes, collection, component, createCommand, createElement, createRef, createRef$, driverFromAsync, enableHMR, exactState, getDevTools, lazy, makeDOMDriver, makeDragDriver, mockDOMSource, Portal as portal, processDrag, processForm, renderComponent, renderToString, run, switchable, thunk, xs };
|
|
6579
|
+
export { ABORT, Collection, MainDOMSource, MockedDOMSource, Portal, Slot, Suspense, Switchable, Transition, classes, collection, component, createCommand, createElement, createRef, createRef$, driverFromAsync, emit, enableHMR, exactState, getDevTools, lazy, makeDOMDriver, makeDragDriver, mockDOMSource, Portal as portal, processDrag, processForm, renderComponent, renderToString, run, set, switchable, thunk, toggle, xs };
|