Skip to content

Commit e2bcc97

Browse files
authored
Merge pull request #51 from bcdev/forman-43-derived_host_state
Introduced derived host state
2 parents ee45b22 + 1140346 commit e2bcc97

16 files changed

+182
-191
lines changed

chartlets.js/CHANGES.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
## Version 0.0.23 (in development)
1+
## Version 0.0.24 (from 2024/11/23)
2+
3+
* Exporting required `HostStore` type.
4+
5+
## Version 0.0.23 (from 2024/11/23)
6+
7+
* Introduced new interface `HostState` that applications may implement
8+
to provide computed properties, i.e., a derived state. (#43)
29

310
* Replacing entire components if a related component `StateChange`
411
has an empty `property`. (#38)

chartlets.js/package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

chartlets.js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "chartlets",
3-
"version": "0.0.22",
3+
"version": "0.0.24",
44
"description": "An experimental library for integrating interactive charts into existing JavaScript applications.",
55
"type": "module",
66
"files": [

chartlets.js/src/demo/App.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,18 @@ import ControlBar from "@/demo/components/ControlBar";
77
import PanelsControl from "./components/PanelsControl";
88
import PanelsRow from "./components/PanelsRow";
99
import { appStore } from "@/demo/store";
10+
import { getValue, setValue } from "@/lib/utils/objPath";
1011

1112
initializeContributions({
12-
hostStore: appStore,
13+
hostStore: {
14+
// Let Chartlets listen to changes in application state.
15+
subscribe: (listener: () => void) => appStore.subscribe(listener),
16+
// Compute a property value and return it. We simply use getValue() here.
17+
get: (property: string): unknown => getValue(appStore.getState(), property),
18+
// Set a property value in the application state.
19+
set: (property: string, value: unknown) =>
20+
void appStore.setState(setValue(appStore.getState(), property, value)),
21+
},
1322
logging: { enabled: true },
1423
});
1524

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import { store } from "@/lib/store";
2-
import type { FrameworkOptions } from "@/lib/types/state/store";
2+
import type { FrameworkOptions } from "@/lib/types/state/options";
33
import { configureLogging } from "@/lib/actions/helpers/configureLogging";
44
import { handleHostStoreChange } from "./handleHostStoreChange";
55

6-
export function configureFramework<S extends object = object>(
7-
options: FrameworkOptions<S>,
8-
) {
6+
export function configureFramework(options: FrameworkOptions) {
97
if (options.logging) {
108
configureLogging(options.logging);
119
}
1210
if (options.hostStore) {
1311
options.hostStore.subscribe(handleHostStoreChange);
1412
}
1513
store.setState({
16-
configuration: { ...options } as FrameworkOptions<object>,
14+
configuration: { ...options } as FrameworkOptions,
1715
});
1816
}

chartlets.js/src/lib/actions/handleComponentChange.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ export function handleComponentChange(
3636
contribIndex,
3737
changeEvent,
3838
);
39-
invokeCallbacks(callbackRequests);
39+
if (callbackRequests && callbackRequests.length > 0) {
40+
invokeCallbacks(callbackRequests);
41+
}
4042
}
4143

4244
/**
@@ -53,8 +55,7 @@ function getCallbackRequests(
5355
changeEvent: ComponentChangeEvent,
5456
): CallbackRequest[] {
5557
const { configuration, contributionsRecord } = store.getState();
56-
const { hostStore, getDerivedHostState } = configuration;
57-
const hostState = hostStore?.getState();
58+
const { hostStore } = configuration;
5859
const contributions = contributionsRecord[contribPoint];
5960
const contribution = contributions[contribIndex];
6061
const callbackRequests: CallbackRequest[] = [];
@@ -75,12 +76,7 @@ function getCallbackRequests(
7576
contribIndex,
7677
callbackIndex,
7778
inputIndex,
78-
inputValues: getInputValues(
79-
inputs,
80-
contribution,
81-
hostState,
82-
getDerivedHostState,
83-
),
79+
inputValues: getInputValues(inputs, contribution, hostStore),
8480
});
8581
}
8682
}

chartlets.js/src/lib/actions/handleHostStoreChange.ts

Lines changed: 41 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -7,73 +7,65 @@ import type {
77
} from "@/lib/types/model/callback";
88
import type { Input } from "@/lib/types/model/channel";
99
import { getInputValues } from "@/lib/actions/helpers/getInputValues";
10-
import {
11-
getValue,
12-
type ObjPath,
13-
normalizeObjPath,
14-
formatObjPath,
15-
} from "@/lib/utils/objPath";
10+
import { formatObjPath } from "@/lib/utils/objPath";
1611
import { invokeCallbacks } from "@/lib/actions/helpers/invokeCallbacks";
1712
import type { ContributionState } from "@/lib/types/state/contribution";
18-
import type { GetDerivedState } from "@/lib/types/state/store";
13+
import type { HostStore } from "@/lib/types/state/options";
1914

2015
/**
2116
* A reference to a property of an input of a callback of a contribution.
2217
*/
2318
export interface PropertyRef extends ContribRef, CallbackRef, InputRef {
24-
/** The property name as path. */
25-
propertyPath: ObjPath;
19+
/** The property. */
20+
property: string;
2621
}
2722

28-
export function handleHostStoreChange<S extends object = object>(
29-
currState: S,
30-
prevState: S,
31-
) {
32-
if (store.getState().extensions.length === 0) {
33-
// Exit immediately if there are no extensions (yet)
23+
export function handleHostStoreChange() {
24+
const { extensions, configuration, contributionsRecord } = store.getState();
25+
const { hostStore } = configuration;
26+
if (!hostStore || extensions.length === 0) {
27+
// Exit if no host store configured or
28+
// there are no extensions (yet)
29+
return;
30+
}
31+
const propertyRefs = getHostStorePropertyRefs();
32+
if (!propertyRefs || propertyRefs.length === 0) {
33+
// Exit if there are is nothing to be changed
3434
return;
3535
}
36-
const { configuration, contributionsRecord } = store.getState();
37-
const { getDerivedHostState } = configuration;
3836
const callbackRequests = getCallbackRequests(
37+
propertyRefs,
3938
contributionsRecord,
40-
currState,
41-
prevState,
42-
getDerivedHostState,
39+
hostStore,
4340
);
44-
invokeCallbacks(callbackRequests);
41+
if (callbackRequests && callbackRequests.length > 0) {
42+
invokeCallbacks(callbackRequests);
43+
}
4544
}
4645

47-
function getCallbackRequests<S extends object = object>(
46+
function getCallbackRequests(
47+
propertyRefs: PropertyRef[],
4848
contributionsRecord: Record<string, ContributionState[]>,
49-
hostState: S,
50-
prevHostState: S,
51-
getDerivedHostState: GetDerivedState<S> | undefined,
49+
hostStore: HostStore,
5250
): CallbackRequest[] {
53-
return getHostStorePropertyRefs()
54-
.filter((propertyRef) =>
55-
hasPropertyChanged(
56-
propertyRef.propertyPath,
57-
hostState,
58-
prevHostState,
59-
getDerivedHostState,
60-
),
61-
)
62-
.map((propertyRef) => {
63-
const contributions = contributionsRecord[propertyRef.contribPoint];
64-
const contribution = contributions[propertyRef.contribIndex];
65-
const callback = contribution.callbacks![propertyRef.callbackIndex];
66-
const inputValues = getInputValues(
67-
callback.inputs!,
68-
contribution,
69-
hostState,
70-
getDerivedHostState as GetDerivedState,
71-
);
72-
return { ...propertyRef, inputValues };
73-
});
51+
return propertyRefs.map((propertyRef) => {
52+
const contributions = contributionsRecord[propertyRef.contribPoint];
53+
const contribution = contributions[propertyRef.contribIndex];
54+
const callback = contribution.callbacks![propertyRef.callbackIndex];
55+
const inputValues = getInputValues(
56+
callback.inputs!,
57+
contribution,
58+
hostStore,
59+
);
60+
return { ...propertyRef, inputValues };
61+
});
7462
}
7563

76-
// const getHostStorePropertyRefs = memoizeOne(_getHostStorePropertyRefs);
64+
// TODO: use a memoized selector to get hostStorePropertyRefs
65+
// Note that this will only be effective and once we split the
66+
// static contribution infos and dynamic contribution states.
67+
// The hostStorePropertyRefs only depend on the static
68+
// contribution infos.
7769

7870
/**
7971
* Get the static list of host state property references for all contributions.
@@ -87,13 +79,13 @@ function getHostStorePropertyRefs(): PropertyRef[] {
8779
(contribution.callbacks || []).forEach(
8880
(callback, callbackIndex) =>
8981
(callback.inputs || []).forEach((input, inputIndex) => {
90-
if (!input.noTrigger && input.link === "app") {
82+
if (!input.noTrigger && input.link === "app" && input.property) {
9183
propertyRefs.push({
9284
contribPoint,
9385
contribIndex,
9486
callbackIndex,
9587
inputIndex,
96-
propertyPath: normalizeObjPath(input.property!),
88+
property: formatObjPath(input.property),
9789
});
9890
}
9991
}),
@@ -103,25 +95,3 @@ function getHostStorePropertyRefs(): PropertyRef[] {
10395
});
10496
return propertyRefs;
10597
}
106-
107-
function hasPropertyChanged<S extends object = object>(
108-
propertyPath: ObjPath,
109-
currState: S,
110-
prevState: S,
111-
getDerivedHostState: GetDerivedState<S> | undefined,
112-
): boolean {
113-
const currValue = getValue(currState, propertyPath);
114-
const prevValue = getValue(prevState, propertyPath);
115-
if (
116-
currValue === undefined &&
117-
prevValue === undefined &&
118-
getDerivedHostState !== undefined
119-
) {
120-
const propertyName = formatObjPath(propertyPath);
121-
return !Object.is(
122-
getDerivedHostState(currState, propertyName),
123-
getDerivedHostState(prevState, propertyName),
124-
);
125-
}
126-
return !Object.is(currValue, prevValue);
127-
}

chartlets.js/src/lib/actions/helpers/applyStateChangeRequests.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,21 @@ import {
1212
import type { ContribPoint } from "@/lib/types/model/extension";
1313
import type { ContributionState } from "@/lib";
1414
import { updateArray } from "@/lib/utils/updateArray";
15-
import { getValue, normalizeObjPath, setValue } from "@/lib/utils/objPath";
15+
import {
16+
formatObjPath,
17+
getValue,
18+
normalizeObjPath,
19+
setValue,
20+
} from "@/lib/utils/objPath";
21+
import {
22+
isMutableHostStore,
23+
type MutableHostStore,
24+
} from "@/lib/types/state/options";
1625

1726
export function applyStateChangeRequests(
1827
stateChangeRequests: StateChangeRequest[],
1928
) {
20-
const { contributionsRecord } = store.getState();
29+
const { configuration, contributionsRecord } = store.getState();
2130
const contributionsRecordNew = applyContributionChangeRequests(
2231
contributionsRecord,
2332
stateChangeRequests,
@@ -27,7 +36,10 @@ export function applyStateChangeRequests(
2736
contributionsRecord: contributionsRecordNew,
2837
});
2938
}
30-
applyHostStateChanges(stateChangeRequests);
39+
const { hostStore } = configuration;
40+
if (isMutableHostStore(hostStore)) {
41+
applyHostStateChanges(stateChangeRequests, hostStore);
42+
}
3143
}
3244

3345
// we export for testing only
@@ -130,24 +142,17 @@ export function applyComponentStateChange(
130142
return component;
131143
}
132144

133-
function applyHostStateChanges(stateChangeRequests: StateChangeRequest[]) {
134-
const { configuration } = store.getState();
135-
const { hostStore } = configuration;
136-
if (hostStore) {
137-
const hostStateOld = hostStore.getState();
138-
let hostState: object | undefined = hostStateOld;
139-
stateChangeRequests.forEach((stateChangeRequest) => {
140-
hostState = applyStateChanges(
141-
hostState,
142-
stateChangeRequest.stateChanges.filter(
143-
(stateChange) => stateChange.link === "app",
144-
),
145-
);
145+
function applyHostStateChanges(
146+
stateChangeRequests: StateChangeRequest[],
147+
hostStore: MutableHostStore,
148+
) {
149+
stateChangeRequests.forEach((stateChangeRequest) => {
150+
stateChangeRequest.stateChanges.forEach((stateChange) => {
151+
if (stateChange.link === "app") {
152+
hostStore.set(formatObjPath(stateChange.property), stateChange.value);
153+
}
146154
});
147-
if (hostState !== hostStateOld) {
148-
hostStore.setState(hostState);
149-
}
150-
}
155+
});
151156
}
152157

153158
// we export for testing only

chartlets.js/src/lib/actions/helpers/getInputValues.test.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -115,22 +115,4 @@ describe("Test that getInputValueFromState()", () => {
115115
getInputValueFromState({ link: "container", property: "x.y.2" }, state),
116116
).toEqual(6);
117117
});
118-
119-
it("works with derived state", () => {
120-
const state = { x: { y: [4, 5, 6] } };
121-
const getDerivedState = (
122-
_state: object,
123-
name: string,
124-
): number | undefined =>
125-
name === "x.ySum"
126-
? state.x.y[0] + state.x.y[1] + state.x.y[2]
127-
: undefined;
128-
expect(
129-
getInputValueFromState(
130-
{ link: "container", property: "x.ySum" },
131-
state,
132-
getDerivedState,
133-
),
134-
).toEqual(4 + 5 + 6);
135-
});
136118
});

0 commit comments

Comments
 (0)