cx 26.7.0 → 26.7.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/dist/ui.js CHANGED
@@ -2094,6 +2094,30 @@ class LinkedListsNode {
2094
2094
  }
2095
2095
  }
2096
2096
 
2097
+ // Optional guard (opt in per-instance via the `limitSyncBursts` prop) against React's "Maximum update depth
2098
+ // exceeded" during very large synchronous render bursts. On the initial render of a large document the store
2099
+ // can be mutated thousands of times within a single React commit: every Instance.setState wraps store.notify()
2100
+ // in batchUpdates(), so isBatchingUpdates() stays true and a subscribed Cx takes its *synchronous* setState
2101
+ // branch on each notification. React counts those as nested render-phase updates and aborts past ~50
2102
+ // (surfacing as the recoverable "error during concurrent rendering"). When a Cx opts in, once back-to-back
2103
+ // updates exceed the limit it falls back to the coalesced setTimeout branch, which renders the latest store
2104
+ // data on the next tick. The counter is global (it mirrors React's global nested-update counter) and resets
2105
+ // after any idle gap. It is advanced on every update() of an opted-in Cx -- counting only the synchronous
2106
+ // branch would let the window reset whenever a deferred update lands between two synchronous ones, so the
2107
+ // counter would never reach the limit. performance.now() gives sub-ms resolution (Date.now() is the fallback;
2108
+ // Timing.now() can't be used here because it returns 0 in production).
2109
+ const SYNC_UPDATE_BURST_LIMIT = 30;
2110
+ const SYNC_UPDATE_BURST_RESET_MS = 10;
2111
+ const syncUpdateBurstNow =
2112
+ typeof performance !== "undefined" && performance.now ? () => performance.now() : () => Date.now();
2113
+ let syncUpdateBurstCount = 0;
2114
+ let syncUpdateBurstLastAt = 0;
2115
+ function trackSyncUpdateBurst() {
2116
+ let t = syncUpdateBurstNow();
2117
+ if (t - syncUpdateBurstLastAt > SYNC_UPDATE_BURST_RESET_MS) syncUpdateBurstCount = 0;
2118
+ syncUpdateBurstLastAt = t;
2119
+ return ++syncUpdateBurstCount > SYNC_UPDATE_BURST_LIMIT;
2120
+ }
2097
2121
  class Cx extends VDOM.Component {
2098
2122
  widget;
2099
2123
  store;
@@ -2192,7 +2216,11 @@ class Cx extends VDOM.Component {
2192
2216
  let data = this.store.getData();
2193
2217
  debug(appDataFlag, data);
2194
2218
  if (this.flags.preparing) this.flags.dirty = true;
2195
- else if (isBatchingUpdates() || this.props.immediate) {
2219
+ // `immediate` always renders synchronously (its contract). Otherwise, while batching, a Cx that opts in
2220
+ // via `limitSyncBursts` defers once a synchronous burst gets too deep (avoids React's max-update-depth).
2221
+ // Without the flag the condition is unchanged -- trackSyncUpdateBurst() is never even called, so the
2222
+ // guard has zero effect on instances that don't opt in.
2223
+ else if (this.props.immediate || (isBatchingUpdates() && !(this.props.limitSyncBursts && trackSyncUpdateBurst()))) {
2196
2224
  notifyBatchedUpdateStarting();
2197
2225
  this.setState(
2198
2226
  {
@@ -2201,14 +2229,17 @@ class Cx extends VDOM.Component {
2201
2229
  notifyBatchedUpdateCompleted,
2202
2230
  );
2203
2231
  } else {
2204
- //in standard mode sequential store commands are batched
2232
+ //in standard mode sequential store commands are batched -- as is any update that exceeds the
2233
+ //synchronous burst limit above, which falls through here to avoid React's max-update-depth abort
2205
2234
  if (!this.pendingUpdateTimer) {
2206
2235
  notifyBatchedUpdateStarting();
2207
2236
  this.pendingUpdateTimer = setTimeout(() => {
2208
2237
  delete this.pendingUpdateTimer;
2238
+ //for opted-in instances read fresh data at fire time so a deferred (coalesced) update isn't
2239
+ //left stale after a burst; otherwise keep the original captured-data behavior unchanged
2209
2240
  this.setState(
2210
2241
  {
2211
- data: data,
2242
+ data: this.props.limitSyncBursts ? this.store.getData() : data,
2212
2243
  },
2213
2244
  notifyBatchedUpdateCompleted,
2214
2245
  );
@@ -2662,6 +2693,7 @@ class Restate extends PureContainerBase {
2662
2693
  deferredUntilIdle: instance.data.deferredUntilIdle,
2663
2694
  idleTimeout: instance.data.idleTimeout,
2664
2695
  immediate: this.immediate,
2696
+ limitSyncBursts: this.limitSyncBursts,
2665
2697
  cultureInfo: instance.cultureInfo,
2666
2698
  },
2667
2699
  key,
@@ -2671,6 +2703,7 @@ class Restate extends PureContainerBase {
2671
2703
  Restate.prototype.detached = false;
2672
2704
  Restate.prototype.waitForIdle = false;
2673
2705
  Restate.prototype.immediate = false;
2706
+ Restate.prototype.limitSyncBursts = false;
2674
2707
  Restate.prototype.culture = null;
2675
2708
  const PrivateStore = Restate;
2676
2709
  class RestateStore extends Store {
package/dist/widgets.js CHANGED
@@ -1124,6 +1124,7 @@ class DocumentTitle extends Widget {
1124
1124
  super.explore(context, instance);
1125
1125
  }
1126
1126
  prepare(context, instance) {
1127
+ if (typeof document == "undefined") return;
1127
1128
  if (context.documentTitle.activeInstance == instance) document.title = context.documentTitle.title;
1128
1129
  }
1129
1130
  render() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cx",
3
- "version": "26.7.0",
3
+ "version": "26.7.1",
4
4
  "description": "Advanced JavaScript UI framework for admin and dashboard applications with ready to use grid, form and chart components.",
5
5
  "exports": {
6
6
  "./data": {
package/src/ui/Cx.tsx CHANGED
@@ -21,6 +21,11 @@ export interface CxProps {
21
21
  parentInstance?: Instance;
22
22
  subscribe?: boolean;
23
23
  immediate?: boolean;
24
+ /** Cap re-entrant synchronous updates: while batching, once a burst of back-to-back updates gets too
25
+ * deep, defer the excess to the coalesced (setTimeout) branch instead of updating synchronously. Avoids
26
+ * React's "Maximum update depth exceeded" on very large synchronous render bursts (e.g. the initial
27
+ * render of a huge document). Off by default; has no effect on instances that don't set it. */
28
+ limitSyncBursts?: boolean;
24
29
  deferredUntilIdle?: boolean;
25
30
  idleTimeout?: number;
26
31
  options?: any;
@@ -35,6 +40,32 @@ export interface CxState {
35
40
  data?: any;
36
41
  }
37
42
 
43
+ // Optional guard (opt in per-instance via the `limitSyncBursts` prop) against React's "Maximum update depth
44
+ // exceeded" during very large synchronous render bursts. On the initial render of a large document the store
45
+ // can be mutated thousands of times within a single React commit: every Instance.setState wraps store.notify()
46
+ // in batchUpdates(), so isBatchingUpdates() stays true and a subscribed Cx takes its *synchronous* setState
47
+ // branch on each notification. React counts those as nested render-phase updates and aborts past ~50
48
+ // (surfacing as the recoverable "error during concurrent rendering"). When a Cx opts in, once back-to-back
49
+ // updates exceed the limit it falls back to the coalesced setTimeout branch, which renders the latest store
50
+ // data on the next tick. The counter is global (it mirrors React's global nested-update counter) and resets
51
+ // after any idle gap. It is advanced on every update() of an opted-in Cx -- counting only the synchronous
52
+ // branch would let the window reset whenever a deferred update lands between two synchronous ones, so the
53
+ // counter would never reach the limit. performance.now() gives sub-ms resolution (Date.now() is the fallback;
54
+ // Timing.now() can't be used here because it returns 0 in production).
55
+ const SYNC_UPDATE_BURST_LIMIT = 30;
56
+ const SYNC_UPDATE_BURST_RESET_MS = 10;
57
+ const syncUpdateBurstNow: () => number =
58
+ typeof performance !== "undefined" && performance.now ? () => performance.now() : () => Date.now();
59
+ let syncUpdateBurstCount = 0;
60
+ let syncUpdateBurstLastAt = 0;
61
+
62
+ function trackSyncUpdateBurst(): boolean {
63
+ let t = syncUpdateBurstNow();
64
+ if (t - syncUpdateBurstLastAt > SYNC_UPDATE_BURST_RESET_MS) syncUpdateBurstCount = 0;
65
+ syncUpdateBurstLastAt = t;
66
+ return ++syncUpdateBurstCount > SYNC_UPDATE_BURST_LIMIT;
67
+ }
68
+
38
69
  export class Cx extends VDOM.Component<CxProps, CxState> {
39
70
  widget: Widget;
40
71
  store: View;
@@ -160,16 +191,26 @@ export class Cx extends VDOM.Component<CxProps, CxState> {
160
191
  let data = this.store.getData();
161
192
  debug(appDataFlag, data);
162
193
  if (this.flags.preparing) this.flags.dirty = true;
163
- else if (isBatchingUpdates() || this.props.immediate) {
194
+ // `immediate` always renders synchronously (its contract). Otherwise, while batching, a Cx that opts in
195
+ // via `limitSyncBursts` defers once a synchronous burst gets too deep (avoids React's max-update-depth).
196
+ // Without the flag the condition is unchanged -- trackSyncUpdateBurst() is never even called, so the
197
+ // guard has zero effect on instances that don't opt in.
198
+ else if (this.props.immediate || (isBatchingUpdates() && !(this.props.limitSyncBursts && trackSyncUpdateBurst()))) {
164
199
  notifyBatchedUpdateStarting();
165
200
  this.setState({ data: data }, notifyBatchedUpdateCompleted);
166
201
  } else {
167
- //in standard mode sequential store commands are batched
202
+ //in standard mode sequential store commands are batched -- as is any update that exceeds the
203
+ //synchronous burst limit above, which falls through here to avoid React's max-update-depth abort
168
204
  if (!this.pendingUpdateTimer) {
169
205
  notifyBatchedUpdateStarting();
170
206
  this.pendingUpdateTimer = setTimeout(() => {
171
207
  delete this.pendingUpdateTimer;
172
- this.setState({ data: data }, notifyBatchedUpdateCompleted);
208
+ //for opted-in instances read fresh data at fire time so a deferred (coalesced) update isn't
209
+ //left stale after a burst; otherwise keep the original captured-data behavior unchanged
210
+ this.setState(
211
+ { data: this.props.limitSyncBursts ? this.store.getData() : data },
212
+ notifyBatchedUpdateCompleted,
213
+ );
173
214
  }, 0);
174
215
  }
175
216
  }
@@ -1,55 +1,55 @@
1
- import { AccessorChain } from "../data";
2
- import { NestedDataView } from "../data/NestedDataView";
3
- import { StructuredProp, WritableProp } from "./Prop";
4
- import { PureContainerBase, PureContainerConfig } from "./PureContainer";
5
- import { StructuredInstanceDataAccessor } from "./StructuredInstanceDataAccessor";
6
-
7
- export interface DataProxyConfig extends PureContainerConfig {
8
- /** Data object with computed values to be exposed in the local store. */
9
- data?: StructuredProp;
10
-
11
- /** Binding to a value to be exposed under the `alias` name. */
12
- value?: WritableProp<any>;
13
-
14
- /** Alias name under which `value` is exposed in the local store. */
15
- alias?: string | AccessorChain<any>;
16
-
17
- /** Indicate that parent store data should not be mutated. */
18
- immutable?: boolean;
19
-
20
- /** Indicate that local store data should not be mutated. */
21
- sealed?: boolean;
22
- }
23
-
24
- export class DataProxy extends PureContainerBase<DataProxyConfig> {
25
- declare data?: any;
26
- declare alias?: string;
27
- declare value?: any;
28
- declare immutable: boolean;
29
- declare sealed: boolean;
30
-
31
- init() {
32
- if (!this.data) this.data = {};
33
-
34
- if (this.alias) this.data[this.alias] = this.value;
35
-
36
- super.init();
37
- }
38
-
39
- initInstance(context: any, instance: any) {
40
- instance.store = new NestedDataView({
41
- store: instance.parentStore,
42
- nestedData: new StructuredInstanceDataAccessor({ instance, data: this.data, useParentStore: true }),
43
- immutable: this.immutable,
44
- sealed: this.sealed,
45
- });
46
- super.initInstance(context, instance);
47
- }
48
-
49
- applyParentStore(instance: any) {
50
- instance.store.setStore(instance.parentStore);
51
- }
52
- }
53
-
54
- DataProxy.prototype.immutable = false;
55
- DataProxy.prototype.sealed = false;
1
+ import { AccessorChain } from "../data";
2
+ import { NestedDataView } from "../data/NestedDataView";
3
+ import { StructuredProp, WritableProp } from "./Prop";
4
+ import { PureContainerBase, PureContainerConfig } from "./PureContainer";
5
+ import { StructuredInstanceDataAccessor } from "./StructuredInstanceDataAccessor";
6
+
7
+ export interface DataProxyConfig extends PureContainerConfig {
8
+ /** Data object with computed values to be exposed in the local store. */
9
+ data?: StructuredProp;
10
+
11
+ /** Binding to a value to be exposed under the `alias` name. */
12
+ value?: WritableProp<any>;
13
+
14
+ /** Alias name under which `value` is exposed in the local store. */
15
+ alias?: string | AccessorChain<any>;
16
+
17
+ /** Indicate that parent store data should not be mutated. */
18
+ immutable?: boolean;
19
+
20
+ /** Indicate that local store data should not be mutated. */
21
+ sealed?: boolean;
22
+ }
23
+
24
+ export class DataProxy extends PureContainerBase<DataProxyConfig> {
25
+ declare data?: any;
26
+ declare alias?: string;
27
+ declare value?: any;
28
+ declare immutable: boolean;
29
+ declare sealed: boolean;
30
+
31
+ init() {
32
+ if (!this.data) this.data = {};
33
+
34
+ if (this.alias) this.data[this.alias] = this.value;
35
+
36
+ super.init();
37
+ }
38
+
39
+ initInstance(context: any, instance: any) {
40
+ instance.store = new NestedDataView({
41
+ store: instance.parentStore,
42
+ nestedData: new StructuredInstanceDataAccessor({ instance, data: this.data, useParentStore: true }),
43
+ immutable: this.immutable,
44
+ sealed: this.sealed,
45
+ });
46
+ super.initInstance(context, instance);
47
+ }
48
+
49
+ applyParentStore(instance: any) {
50
+ instance.store.setStore(instance.parentStore);
51
+ }
52
+ }
53
+
54
+ DataProxy.prototype.immutable = false;
55
+ DataProxy.prototype.sealed = false;
package/src/ui/Rescope.ts CHANGED
@@ -1,50 +1,50 @@
1
- import { Widget } from "./Widget";
2
- import { PureContainerBase, PureContainerConfig } from "./PureContainer";
3
- import { Binding } from "../data/Binding";
4
- import { ZoomIntoPropertyView } from "../data/ZoomIntoPropertyView";
5
- import { StructuredInstanceDataAccessor } from "./StructuredInstanceDataAccessor";
6
- import { isObject } from "../util/isObject";
7
- import { StructuredProp } from "./Prop";
8
- import { AccessorChain } from "../data/createAccessorModelProxy";
9
-
10
- export interface RescopeConfig extends PureContainerConfig {
11
- bind: string | AccessorChain<any>;
12
- rootName?: string | AccessorChain<any>;
13
- rootAlias?: string | AccessorChain<any>;
14
- data?: StructuredProp;
15
- }
16
-
17
- export class Rescope extends PureContainerBase<RescopeConfig> {
18
- declare bind: string;
19
- declare binding: any;
20
- declare rootAlias?: string;
21
- declare rootName: string;
22
- declare data?: any;
23
-
24
- init() {
25
- this.binding = Binding.get(this.bind);
26
- if (this.rootAlias) this.rootName = this.rootAlias;
27
- super.init();
28
- }
29
-
30
- initInstance(context: any, instance: any) {
31
- instance.store = new ZoomIntoPropertyView({
32
- store: instance.parentStore,
33
- binding: this.binding,
34
- rootName: this.rootName,
35
- nestedData: isObject(this.data)
36
- ? new StructuredInstanceDataAccessor({ instance, data: this.data, useParentStore: true })
37
- : undefined,
38
- });
39
- super.initInstance(context, instance);
40
- }
41
-
42
- applyParentStore(instance: any) {
43
- instance.store.setStore(instance.parentStore);
44
- }
45
- }
46
-
47
- Rescope.prototype.bind = "$page";
48
- Rescope.prototype.rootName = "$root";
49
-
50
- Widget.alias("rescope", Rescope);
1
+ import { Widget } from "./Widget";
2
+ import { PureContainerBase, PureContainerConfig } from "./PureContainer";
3
+ import { Binding } from "../data/Binding";
4
+ import { ZoomIntoPropertyView } from "../data/ZoomIntoPropertyView";
5
+ import { StructuredInstanceDataAccessor } from "./StructuredInstanceDataAccessor";
6
+ import { isObject } from "../util/isObject";
7
+ import { StructuredProp } from "./Prop";
8
+ import { AccessorChain } from "../data/createAccessorModelProxy";
9
+
10
+ export interface RescopeConfig extends PureContainerConfig {
11
+ bind: string | AccessorChain<any>;
12
+ rootName?: string | AccessorChain<any>;
13
+ rootAlias?: string | AccessorChain<any>;
14
+ data?: StructuredProp;
15
+ }
16
+
17
+ export class Rescope extends PureContainerBase<RescopeConfig> {
18
+ declare bind: string;
19
+ declare binding: any;
20
+ declare rootAlias?: string;
21
+ declare rootName: string;
22
+ declare data?: any;
23
+
24
+ init() {
25
+ this.binding = Binding.get(this.bind);
26
+ if (this.rootAlias) this.rootName = this.rootAlias;
27
+ super.init();
28
+ }
29
+
30
+ initInstance(context: any, instance: any) {
31
+ instance.store = new ZoomIntoPropertyView({
32
+ store: instance.parentStore,
33
+ binding: this.binding,
34
+ rootName: this.rootName,
35
+ nestedData: isObject(this.data)
36
+ ? new StructuredInstanceDataAccessor({ instance, data: this.data, useParentStore: true })
37
+ : undefined,
38
+ });
39
+ super.initInstance(context, instance);
40
+ }
41
+
42
+ applyParentStore(instance: any) {
43
+ instance.store.setStore(instance.parentStore);
44
+ }
45
+ }
46
+
47
+ Rescope.prototype.bind = "$page";
48
+ Rescope.prototype.rootName = "$root";
49
+
50
+ Widget.alias("rescope", Rescope);
@@ -33,6 +33,12 @@ export interface RestateConfig extends PureContainerConfig {
33
33
  /** Set to `true` to disable batching of updates. */
34
34
  immediate?: boolean;
35
35
 
36
+ /** Cap re-entrant synchronous updates on the detached `Cx` this Restate renders: once a burst of
37
+ * back-to-back updates gets too deep, defer the excess to the coalesced branch instead of updating
38
+ * synchronously. Avoids React's "Maximum update depth exceeded" on very large synchronous render bursts.
39
+ * Only applies in `detached` mode (a non-detached Restate has no `Cx` of its own). Off by default. */
40
+ limitSyncBursts?: boolean;
41
+
36
42
  /** Error handler callback. */
37
43
  onError?: (error: Error, instance: Instance) => void;
38
44
 
@@ -53,6 +59,7 @@ export class Restate<Config extends RestateConfig = RestateConfig> extends PureC
53
59
  declare onError?: (error: Error, instance: Instance) => void;
54
60
  declare waitForIdle: boolean;
55
61
  declare immediate: boolean;
62
+ declare limitSyncBursts: boolean;
56
63
 
57
64
  declareData(...args: any[]) {
58
65
  return super.declareData(...args, {
@@ -138,6 +145,7 @@ export class Restate<Config extends RestateConfig = RestateConfig> extends PureC
138
145
  deferredUntilIdle={instance.data.deferredUntilIdle}
139
146
  idleTimeout={instance.data.idleTimeout}
140
147
  immediate={this.immediate}
148
+ limitSyncBursts={this.limitSyncBursts}
141
149
  cultureInfo={instance.cultureInfo}
142
150
  />
143
151
  );
@@ -147,6 +155,7 @@ export class Restate<Config extends RestateConfig = RestateConfig> extends PureC
147
155
  Restate.prototype.detached = false;
148
156
  Restate.prototype.waitForIdle = false;
149
157
  Restate.prototype.immediate = false;
158
+ Restate.prototype.limitSyncBursts = false;
150
159
  Restate.prototype.culture = null;
151
160
 
152
161
  export const PrivateStore = Restate;
@@ -1,96 +1,96 @@
1
- import { computable } from "../data/computable";
2
- import { AccessorChain } from "../data/createAccessorModelProxy";
3
- import { Selector } from "../data/Selector";
4
- import { Format } from "../util/Format";
5
- import { expr } from "./expr";
6
-
7
- /** Returns a selector that converts the value to boolean using !! */
8
- export function truthy<V>(arg: AccessorChain<V>): Selector<boolean> {
9
- return expr(arg, (x) => !!x);
10
- }
11
-
12
- /** Returns a selector that checks if the value is falsy using ! */
13
- export function falsy<V>(arg: AccessorChain<V>): Selector<boolean> {
14
- return expr(arg, (x) => !x);
15
- }
16
-
17
- /** Returns a selector that checks if the value is strictly true (=== true) */
18
- export function isTrue(arg: AccessorChain<any>): Selector<boolean> {
19
- return expr(arg, (x) => x === true);
20
- }
21
-
22
- /** Returns a selector that checks if the value is strictly false (=== false) */
23
- export function isFalse(arg: AccessorChain<any>): Selector<boolean> {
24
- return expr(arg, (x) => x === false);
25
- }
26
-
27
- /** Returns a selector that checks if the value is not null or undefined (x != null) */
28
- export function hasValue<V>(arg: AccessorChain<V>): Selector<boolean> {
29
- return expr(arg, (x) => x != null);
30
- }
31
-
32
- /** Returns a selector that checks if a string or array is empty (null, undefined, or length === 0) */
33
- export function isEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
34
- return expr(arg, (x) => x == null || x.length === 0);
35
- }
36
-
37
- /** Returns a selector that checks if a string or array is non-empty (not null/undefined and length > 0) */
38
- export function isNonEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
39
- return expr(arg, (x) => x != null && x.length > 0);
40
- }
41
-
42
- /** Returns a selector that checks if the value is less than the given value */
43
- export function lessThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
44
- return expr(arg, (x) => x < value);
45
- }
46
-
47
- /** Returns a selector that checks if the value is less than or equal to the given value */
48
- export function lessThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
49
- return expr(arg, (x) => x <= value);
50
- }
51
-
52
- /** Returns a selector that checks if the value is greater than the given value */
53
- export function greaterThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
54
- return expr(arg, (x) => x > value);
55
- }
56
-
57
- /** Returns a selector that checks if the value is greater than or equal to the given value */
58
- export function greaterThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
59
- return expr(arg, (x) => x >= value);
60
- }
61
-
62
- /** Returns a selector that checks if the value equals the given value using == */
63
- export function equal<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
64
- return expr(arg, (x) => x == value);
65
- }
66
-
67
- /** Returns a selector that checks if the value does not equal the given value using != */
68
- export function notEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
69
- return expr(arg, (x) => x != value);
70
- }
71
-
72
- /** Returns a selector that checks if the value strictly equals the given value using === */
73
- export function strictEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
74
- return expr(arg, (x) => x === value);
75
- }
76
-
77
- /** Returns a selector that checks if the value strictly does not equal the given value using !== */
78
- export function strictNotEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
79
- return expr(arg, (x) => x !== value);
80
- }
81
-
82
- /** Returns a selector that formats the value using the specified format string.
83
- * Format strings use semicolon-separated syntax: "formatType;param1;param2"
84
- * @param arg - The accessor chain to the value
85
- * @param fmt - The format string
86
- * @param nullText - Optional text to display for null/undefined values
87
- * @example
88
- * format(m.price, "n;2") // formats as number with 2 decimal places
89
- * format(m.date, "d") // formats as date
90
- * format(m.value, "p;0;2") // formats as percentage with 0-2 decimal places
91
- * format(m.value, "n;2", "N/A") // shows "N/A" for null values
92
- */
93
- export function format<V>(arg: AccessorChain<V>, fmt: string, nullText?: string): Selector<string> {
94
- let f = nullText != null ? `${fmt}|${nullText}` : fmt;
95
- return computable(arg, (x) => Format.value(x, f));
96
- }
1
+ import { computable } from "../data/computable";
2
+ import { AccessorChain } from "../data/createAccessorModelProxy";
3
+ import { Selector } from "../data/Selector";
4
+ import { Format } from "../util/Format";
5
+ import { expr } from "./expr";
6
+
7
+ /** Returns a selector that converts the value to boolean using !! */
8
+ export function truthy<V>(arg: AccessorChain<V>): Selector<boolean> {
9
+ return expr(arg, (x) => !!x);
10
+ }
11
+
12
+ /** Returns a selector that checks if the value is falsy using ! */
13
+ export function falsy<V>(arg: AccessorChain<V>): Selector<boolean> {
14
+ return expr(arg, (x) => !x);
15
+ }
16
+
17
+ /** Returns a selector that checks if the value is strictly true (=== true) */
18
+ export function isTrue(arg: AccessorChain<any>): Selector<boolean> {
19
+ return expr(arg, (x) => x === true);
20
+ }
21
+
22
+ /** Returns a selector that checks if the value is strictly false (=== false) */
23
+ export function isFalse(arg: AccessorChain<any>): Selector<boolean> {
24
+ return expr(arg, (x) => x === false);
25
+ }
26
+
27
+ /** Returns a selector that checks if the value is not null or undefined (x != null) */
28
+ export function hasValue<V>(arg: AccessorChain<V>): Selector<boolean> {
29
+ return expr(arg, (x) => x != null);
30
+ }
31
+
32
+ /** Returns a selector that checks if a string or array is empty (null, undefined, or length === 0) */
33
+ export function isEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
34
+ return expr(arg, (x) => x == null || x.length === 0);
35
+ }
36
+
37
+ /** Returns a selector that checks if a string or array is non-empty (not null/undefined and length > 0) */
38
+ export function isNonEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
39
+ return expr(arg, (x) => x != null && x.length > 0);
40
+ }
41
+
42
+ /** Returns a selector that checks if the value is less than the given value */
43
+ export function lessThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
44
+ return expr(arg, (x) => x < value);
45
+ }
46
+
47
+ /** Returns a selector that checks if the value is less than or equal to the given value */
48
+ export function lessThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
49
+ return expr(arg, (x) => x <= value);
50
+ }
51
+
52
+ /** Returns a selector that checks if the value is greater than the given value */
53
+ export function greaterThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
54
+ return expr(arg, (x) => x > value);
55
+ }
56
+
57
+ /** Returns a selector that checks if the value is greater than or equal to the given value */
58
+ export function greaterThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
59
+ return expr(arg, (x) => x >= value);
60
+ }
61
+
62
+ /** Returns a selector that checks if the value equals the given value using == */
63
+ export function equal<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
64
+ return expr(arg, (x) => x == value);
65
+ }
66
+
67
+ /** Returns a selector that checks if the value does not equal the given value using != */
68
+ export function notEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
69
+ return expr(arg, (x) => x != value);
70
+ }
71
+
72
+ /** Returns a selector that checks if the value strictly equals the given value using === */
73
+ export function strictEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
74
+ return expr(arg, (x) => x === value);
75
+ }
76
+
77
+ /** Returns a selector that checks if the value strictly does not equal the given value using !== */
78
+ export function strictNotEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
79
+ return expr(arg, (x) => x !== value);
80
+ }
81
+
82
+ /** Returns a selector that formats the value using the specified format string.
83
+ * Format strings use semicolon-separated syntax: "formatType;param1;param2"
84
+ * @param arg - The accessor chain to the value
85
+ * @param fmt - The format string
86
+ * @param nullText - Optional text to display for null/undefined values
87
+ * @example
88
+ * format(m.price, "n;2") // formats as number with 2 decimal places
89
+ * format(m.date, "d") // formats as date
90
+ * format(m.value, "p;0;2") // formats as percentage with 0-2 decimal places
91
+ * format(m.value, "n;2", "N/A") // shows "N/A" for null values
92
+ */
93
+ export function format<V>(arg: AccessorChain<V>, fmt: string, nullText?: string): Selector<string> {
94
+ let f = nullText != null ? `${fmt}|${nullText}` : fmt;
95
+ return computable(arg, (x) => Format.value(x, f));
96
+ }
@@ -80,6 +80,8 @@ export class DocumentTitle extends Widget<DocumentTitleConfig> {
80
80
  }
81
81
 
82
82
  prepare(context: RenderingContext, instance: Instance): void {
83
+ if (typeof document == "undefined") return;
84
+
83
85
  if ((context as any).documentTitle.activeInstance == instance)
84
86
  document.title = (context as any).documentTitle.title;
85
87
  }