Skip to content

Commit cac5221

Browse files
authored
Merge pull request #54 from bcdev/forman-52-ease_defining_channels
Ease defining callback inputs and outputs
2 parents 30b6dc3 + 3b8769d commit cac5221

File tree

17 files changed

+161
-279
lines changed

17 files changed

+161
-279
lines changed

chartlets.js/CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Version 0.0.26 (from 2024/11/23)
2+
3+
* Channels such as `Input`, `State`, `Output` no longer have a `link` property.
4+
Instead, we use a special `id` format, namely `"@app"` and `@container`
5+
to address states other than components. (#52)
6+
17
## Version 0.0.25 (from 2024/11/23)
28

39
* `Registry.register()` now requires the `type`

chartlets.js/package-lock.json

Lines changed: 2 additions & 2 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.25",
3+
"version": "0.0.26",
44
"description": "An experimental library for integrating interactive charts into existing JavaScript applications.",
55
"type": "module",
66
"files": [

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export function handleComponentChange(
2323
contribIndex,
2424
stateChanges: [
2525
{
26-
link: "component",
2726
id: changeEvent.id,
2827
property: changeEvent.property,
2928
value: changeEvent.value,
@@ -65,7 +64,8 @@ function getCallbackRequests(
6564
const inputIndex = inputs.findIndex(
6665
(input) =>
6766
!input.noTrigger &&
68-
(!input.link || input.link === "component") &&
67+
input.id &&
68+
!input.id.startsWith("@") &&
6969
input.id === changeEvent.id &&
7070
equalObjPaths(input.property, changeEvent.property),
7171
);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ function getHostStorePropertyRefs(): PropertyRef[] {
7979
(contribution.callbacks || []).forEach(
8080
(callback, callbackIndex) =>
8181
(callback.inputs || []).forEach((input, inputIndex) => {
82-
if (!input.noTrigger && input.link === "app" && input.property) {
82+
if (!input.noTrigger && input.id === "@app" && input.property) {
8383
propertyRefs.push({
8484
contribPoint,
8585
contribIndex,

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ describe("Test that applyContributionChangeRequests()", () => {
4343
contribIndex: 0,
4444
stateChanges: [
4545
{
46-
link: "component",
4746
id: "dd1",
4847
property: "value",
4948
value: 14,
@@ -56,7 +55,6 @@ describe("Test that applyContributionChangeRequests()", () => {
5655
contribIndex: 0,
5756
stateChanges: [
5857
{
59-
link: "component",
6058
id: "dd1",
6159
property: "value",
6260
value: 13,
@@ -88,7 +86,6 @@ describe("Test that applyContributionChangeRequests()", () => {
8886
describe("Test that applyComponentStateChange()", () => {
8987
it("changes state if values are different", () => {
9088
const newState = applyComponentStateChange(componentTree, {
91-
link: "component",
9289
id: "cb1",
9390
property: "value",
9491
value: false,
@@ -102,7 +99,6 @@ describe("Test that applyComponentStateChange()", () => {
10299

103100
it("doesn't change the state if value stays the same", () => {
104101
const newState = applyComponentStateChange(componentTree, {
105-
link: "component",
106102
id: "cb1",
107103
property: "value",
108104
value: true,
@@ -117,7 +113,6 @@ describe("Test that applyComponentStateChange()", () => {
117113
children: ["Hello", "World"],
118114
};
119115
const newState = applyComponentStateChange(componentTree, {
120-
link: "component",
121116
id: "b1",
122117
property: "",
123118
value,

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ import {
2222
isMutableHostStore,
2323
type MutableHostStore,
2424
} from "@/lib/types/state/options";
25+
import {
26+
isHostChannel,
27+
isComponentChannel,
28+
isContainerChannel,
29+
} from "@/lib/types/model/channel";
2530

2631
export function applyStateChangeRequests(
2732
stateChangeRequests: StateChangeRequest[],
@@ -52,14 +57,11 @@ export function applyContributionChangeRequests(
5257
const contribution = contributionsRecord[contribPoint][contribIndex];
5358
const container = applyStateChanges(
5459
contribution.container,
55-
stateChanges.filter((stateChange) => stateChange.link === "container"),
60+
stateChanges.filter(isContainerChannel),
5661
);
5762
const component = applyComponentStateChanges(
5863
contribution.component,
59-
stateChanges.filter(
60-
(stateChange) =>
61-
!stateChange.link || stateChange.link === "component",
62-
),
64+
stateChanges.filter(isComponentChannel),
6365
);
6466
if (
6567
container !== contribution.container ||
@@ -148,7 +150,7 @@ function applyHostStateChanges(
148150
) {
149151
stateChangeRequests.forEach((stateChangeRequest) => {
150152
stateChangeRequest.stateChanges.forEach((stateChange) => {
151-
if (stateChange.link === "app") {
153+
if (isHostChannel(stateChange)) {
152154
hostStore.set(formatObjPath(stateChange.property), stateChange.value);
153155
}
154156
});

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

Lines changed: 21 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -24,95 +24,48 @@ const componentState = {
2424

2525
describe("Test that getInputValueFromComponent()", () => {
2626
it("works on 1st level", () => {
27-
expect(
28-
getInputValueFromComponent(
29-
{
30-
link: "component",
31-
id: "b1",
32-
property: "value",
33-
},
34-
componentState,
35-
),
36-
).toEqual(14);
27+
expect(getInputValueFromComponent(componentState, "b1", "value")).toEqual(
28+
14,
29+
);
3730
});
3831

3932
it("works on 2nd level", () => {
40-
expect(
41-
getInputValueFromComponent(
42-
{
43-
link: "component",
44-
id: "p1",
45-
property: "chart",
46-
},
47-
componentState,
48-
),
49-
).toEqual(null);
33+
expect(getInputValueFromComponent(componentState, "p1", "chart")).toEqual(
34+
null,
35+
);
5036
});
5137

5238
it("works on 3rd level", () => {
53-
expect(
54-
getInputValueFromComponent(
55-
{
56-
link: "component",
57-
id: "cb1",
58-
property: "value",
59-
},
60-
componentState,
61-
),
62-
).toEqual(true);
39+
expect(getInputValueFromComponent(componentState, "cb1", "value")).toEqual(
40+
true,
41+
);
6342

64-
expect(
65-
getInputValueFromComponent(
66-
{
67-
link: "component",
68-
id: "dd1",
69-
property: "value",
70-
},
71-
componentState,
72-
),
73-
).toEqual(13);
43+
expect(getInputValueFromComponent(componentState, "dd1", "value")).toEqual(
44+
13,
45+
);
7446
});
7547
});
7648

7749
describe("Test that getInputValueFromState()", () => {
78-
it("works with input.id and input.property", () => {
79-
const state = { x: { y: 26 } };
80-
expect(
81-
getInputValueFromState(
82-
{ link: "component", id: "x", property: "y" },
83-
state,
84-
),
85-
).toEqual(26);
86-
});
87-
88-
it("works with arrays indexes", () => {
89-
const state = { x: [4, 5, 6] };
90-
expect(
91-
getInputValueFromState(
92-
{ link: "component", id: "x", property: "1" },
93-
state,
94-
),
95-
).toEqual(5);
96-
});
97-
9850
it("works without input.id", () => {
9951
const state = { x: [4, 5, 6] };
100-
expect(
101-
getInputValueFromState({ link: "container", property: "x" }, state),
102-
).toEqual([4, 5, 6]);
52+
expect(getInputValueFromState(state, "x")).toEqual([4, 5, 6]);
10353
});
10454

10555
it("works on 2nd level", () => {
10656
const state = { x: { y: 15 } };
107-
expect(
108-
getInputValueFromState({ link: "container", property: "x.y" }, state),
109-
).toEqual(15);
57+
expect(getInputValueFromState(state, "x.y")).toEqual(15);
11058
});
11159

11260
it("works on 3nd level", () => {
11361
const state = { x: { y: [4, 5, 6] } };
62+
expect(getInputValueFromState(state, "x.y.2")).toEqual(6);
63+
});
64+
65+
it("works with non-object states", () => {
66+
const state = 13;
11467
expect(
115-
getInputValueFromState({ link: "container", property: "x.y.2" }, state),
116-
).toEqual(6);
68+
getInputValueFromState(state as unknown as object, "x.y.2"),
69+
).toBeUndefined();
11770
});
11871
});
Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import type { Input } from "@/lib/types/model/channel";
1+
import {
2+
type Input,
3+
isComponentChannel,
4+
isContainerChannel,
5+
isHostChannel,
6+
} from "@/lib/types/model/channel";
27
import type { ContributionState } from "@/lib/types/state/contribution";
38
import {
49
type ComponentState,
510
isComponentState,
611
isContainerState,
712
} from "@/lib/types/state/component";
8-
import { formatObjPath, getValue, normalizeObjPath } from "@/lib/utils/objPath";
13+
import { formatObjPath, getValue, type ObjPathLike } from "@/lib/utils/objPath";
914
import { isObject } from "@/lib/utils/isObject";
1015
import type { HostStore } from "@/lib/types/state/options";
1116

@@ -27,13 +32,17 @@ export function getInputValue(
2732
hostStore?: HostStore,
2833
): unknown {
2934
let inputValue: unknown = undefined;
30-
const dataSource = input.link || "component";
31-
if (dataSource === "component" && contributionState.component) {
32-
inputValue = getInputValueFromComponent(input, contributionState.component);
33-
} else if (dataSource === "container" && contributionState.container) {
34-
inputValue = getInputValueFromState(input, contributionState.container);
35-
} else if (dataSource === "app" && hostStore) {
36-
inputValue = getInputValueFromHostStore(input, hostStore);
35+
const { id, property } = input;
36+
if (isComponentChannel(input) && contributionState.component) {
37+
inputValue = getInputValueFromComponent(
38+
contributionState.component,
39+
id,
40+
property,
41+
);
42+
} else if (isContainerChannel(input) && contributionState.container) {
43+
inputValue = getInputValueFromState(contributionState.container, property);
44+
} else if (isHostChannel(input) && hostStore) {
45+
inputValue = getInputValueFromHostStore(hostStore, property);
3746
} else {
3847
console.warn(`input with unknown data source:`, input);
3948
}
@@ -47,16 +56,17 @@ export function getInputValue(
4756

4857
// we export for testing only
4958
export function getInputValueFromComponent(
50-
input: Input,
5159
componentState: ComponentState,
60+
id: string,
61+
property: ObjPathLike,
5262
): unknown {
53-
if (componentState.id === input.id) {
54-
return getValue(componentState, input.property);
63+
if (componentState.id === id) {
64+
return getValue(componentState, property);
5565
} else if (isContainerState(componentState)) {
5666
for (let i = 0; i < componentState.children.length; i++) {
5767
const item = componentState.children[i];
5868
if (isComponentState(item)) {
59-
const itemValue = getInputValueFromComponent(input, item);
69+
const itemValue = getInputValueFromComponent(item, id, property);
6070
if (itemValue !== noValue) {
6171
return itemValue;
6272
}
@@ -68,24 +78,16 @@ export function getInputValueFromComponent(
6878

6979
// we export for testing only
7080
export function getInputValueFromState(
71-
input: Input,
7281
state: object | undefined,
82+
property: ObjPathLike,
7383
): unknown {
74-
let inputValue: unknown = state;
75-
if (input.id && isObject(inputValue)) {
76-
inputValue = inputValue[input.id];
77-
}
78-
if (isObject(inputValue)) {
79-
const state = inputValue;
80-
const property = normalizeObjPath(input.property);
81-
inputValue = getValue(state, property);
82-
}
83-
return inputValue;
84+
return isObject(state) ? getValue(state, property) : undefined;
8485
}
8586

87+
// we export for testing only
8688
export function getInputValueFromHostStore(
87-
input: Input,
8889
hostStore: HostStore,
90+
property: ObjPathLike,
8991
): unknown {
90-
return hostStore.get(formatObjPath(input.property));
92+
return hostStore.get(formatObjPath(property));
9193
}
Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
import type { ObjPathLike } from "@/lib/utils/objPath";
22

3-
export type Link = "component" | "container" | "app";
4-
53
/**
64
* Base for `Input` and `Output`.
75
*/
86
export interface Channel {
97
/**
10-
* The link provides the source for inputs and that target for outputs.
8+
* The identifier for a component or state.
9+
*
10+
* Special identifiers are:
11+
* - `"@app"` the application state referred to by `HostStore`
12+
* - `"@container"` the state referred to by contribution's container
1113
*/
12-
link: Link;
13-
14-
/**
15-
* The identifier of a subcomponent.
16-
* `id` is not needed if link == "AppInput" | "AppOutput".
17-
*/
18-
id?: string;
14+
id: "@app" | "@container" | string;
1915

2016
/**
2117
* The property of an object or array index.
@@ -28,3 +24,15 @@ export interface Input extends Channel {
2824
}
2925

3026
export interface Output extends Channel {}
27+
28+
export function isComponentChannel(channel: Channel): boolean {
29+
return Boolean(channel.id) && !channel.id.startsWith("@");
30+
}
31+
32+
export function isHostChannel(channel: Channel): boolean {
33+
return channel.id === "@app";
34+
}
35+
36+
export function isContainerChannel(channel: Channel): boolean {
37+
return channel.id === "@container";
38+
}

0 commit comments

Comments
 (0)