Skip to content

Commit 6bce83b

Browse files
authored
Merge branch 'main' into yogesh-xxx-fix-vega-warnings
2 parents efdc7d3 + 0215c05 commit 6bce83b

File tree

11 files changed

+192
-84
lines changed

11 files changed

+192
-84
lines changed

chartlets.js/CHANGES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## Version 0.0.23 (in development)
2+
3+
* Replacing entire components if a related component `StateChange`
4+
has an empty `property`. (#38)
5+
6+
* Added handy hooks `useContributions` and `useComponentChangeHandlers`.
7+
8+
19
## Version 0.0.22 (from 2024/11/19)
210

311
* Improved robustness while rendering the in `Select` component

chartlets.js/src/demo/components/PanelsRow.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
1-
import { type ComponentChangeEvent, handleComponentChange } from "@/lib";
2-
import { usePanelStates } from "@/demo/hooks";
1+
import type { PanelState } from "@/demo/types";
2+
import { useComponentChangeHandlers, useContributions } from "@/lib/hooks";
33
import Panel from "./Panel";
44

55
function PanelsRow() {
6-
const panelStates = usePanelStates();
6+
const panelStates = useContributions<PanelState>("panels");
7+
const panelChangeHandlers = useComponentChangeHandlers(
8+
"panels",
9+
panelStates?.length || 0,
10+
);
711
if (!panelStates) {
812
// Ok, not ready yet
913
return null;
1014
}
1115

12-
const handlePanelChange = (
13-
panelIndex: number,
14-
panelEvent: ComponentChangeEvent,
15-
) => {
16-
handleComponentChange("panels", panelIndex, panelEvent);
17-
};
1816
const panels = panelStates.map((panelState, panelIndex) => {
1917
const { container, component, componentResult } = panelState;
2018
return (
@@ -24,7 +22,7 @@ function PanelsRow() {
2422
componentProps={component}
2523
componentStatus={componentResult.status}
2624
componentError={componentResult.error}
27-
onChange={(e) => handlePanelChange(panelIndex, e)}
25+
onChange={panelChangeHandlers[panelIndex]}
2826
/>
2927
);
3028
});

chartlets.js/src/demo/hooks.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { describe, it, expect } from "vitest";
33
import { type ContribPoint } from "@/lib/types/model/extension";
44
import { type StateChangeRequest } from "@/lib/types/model/callback";
55
import {
6+
type BoxState,
67
type ComponentState,
78
type PlotState,
89
} from "@/lib/types/state/component";
@@ -112,4 +113,24 @@ describe("Test that applyComponentStateChange()", () => {
112113
});
113114
expect(newState).toBe(componentTree);
114115
});
116+
117+
it("replaces state if property is empty string", () => {
118+
const value: BoxState = {
119+
type: "Box",
120+
id: "b1",
121+
children: ["Hello", "World"],
122+
};
123+
const newState = applyComponentStateChange(componentTree, {
124+
link: "component",
125+
id: "b1",
126+
property: "",
127+
value,
128+
});
129+
expect(newState).toBe(value);
130+
expect(newState).toEqual({
131+
type: "Box",
132+
id: "b1",
133+
children: ["Hello", "World"],
134+
});
135+
});
115136
});

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

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,39 +30,6 @@ export function applyStateChangeRequests(
3030
applyHostStateChanges(stateChangeRequests);
3131
}
3232

33-
function applyHostStateChanges(stateChangeRequests: StateChangeRequest[]) {
34-
const { configuration } = store.getState();
35-
const { hostStore } = configuration;
36-
if (hostStore) {
37-
const hostStateOld = hostStore.getState();
38-
let hostState: object | undefined = hostStateOld;
39-
stateChangeRequests.forEach((stateChangeRequest) => {
40-
hostState = applyStateChanges(
41-
hostState,
42-
stateChangeRequest.stateChanges.filter(
43-
(stateChange) => stateChange.link === "app",
44-
),
45-
);
46-
});
47-
if (hostState !== hostStateOld) {
48-
hostStore.setState(hostState);
49-
}
50-
}
51-
}
52-
53-
function applyComponentStateChanges(
54-
componentOld: ComponentState | undefined,
55-
stateChanges: StateChange[],
56-
) {
57-
let component = componentOld;
58-
if (component) {
59-
stateChanges.forEach((stateChange) => {
60-
component = applyComponentStateChange(component!, stateChange);
61-
});
62-
}
63-
return component;
64-
}
65-
6633
// we export for testing only
6734
export function applyContributionChangeRequests(
6835
contributionsRecord: Record<ContribPoint, ContributionState[]>,
@@ -101,22 +68,40 @@ export function applyContributionChangeRequests(
10168
return contributionsRecord;
10269
}
10370

71+
function applyComponentStateChanges(
72+
componentOld: ComponentState | undefined,
73+
stateChanges: StateChange[],
74+
) {
75+
let component = componentOld;
76+
if (component) {
77+
stateChanges.forEach((stateChange) => {
78+
component = applyComponentStateChange(component!, stateChange);
79+
});
80+
}
81+
return component;
82+
}
83+
10484
// we export for testing only
10585
export function applyComponentStateChange(
10686
component: ComponentState,
10787
stateChange: StateChange,
10888
): ComponentState {
109-
if (component.id === stateChange.id) {
89+
if (isComponentState(component) && component.id === stateChange.id) {
11090
const property = normalizeObjPath(stateChange.property);
111-
const valueOld = getValue(component, property);
11291
const valueNew = stateChange.value;
92+
if (property.length === 0) {
93+
// Special case: If no property given, replace entire component.
94+
// But only if it is one, otherwise don't change state.
95+
return isComponentState(valueNew) ? valueNew : component;
96+
}
97+
const valueOld = getValue(component, property);
11398
if (
11499
property[property.length - 1] === "children" &&
115100
!Array.isArray(valueNew) &&
116101
valueNew !== null &&
117102
valueNew !== undefined
118103
) {
119-
// Special case if the value of "children" is changed:
104+
// Special case: if the value of "children" is changed:
120105
// convert scalar valueNew into one-element array
121106
return setValue(component, property, [valueNew]);
122107
} else if (valueOld !== valueNew) {
@@ -145,6 +130,26 @@ export function applyComponentStateChange(
145130
return component;
146131
}
147132

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+
);
146+
});
147+
if (hostState !== hostStateOld) {
148+
hostStore.setState(hostState);
149+
}
150+
}
151+
}
152+
148153
// we export for testing only
149154
export function applyStateChanges<S extends object | undefined>(
150155
state: S,

chartlets.js/src/lib/hooks.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import type { StoreState } from "@/lib/types/state/store";
22
import { store } from "@/lib/store";
33
import { useCallback, useMemo } from "react";
44
import type { ContributionState } from "@/lib/types/state/contribution";
5-
import type { ComponentChangeHandler } from "@/lib/types/state/event";
65
import {
76
isTopLevelSelectionParameter,
87
type SignalHandler,
98
} from "@/lib/types/state/vega";
109
import type { TopLevelSpec } from "vega-lite/src/spec";
1110
import { isString } from "@/lib/utils/isString";
11+
import type {
12+
ComponentChangeEvent,
13+
ComponentChangeHandler,
14+
} from "@/lib/types/state/event";
15+
import { handleComponentChange } from "@/lib/actions/handleComponentChange";
1216

1317
const selectConfiguration = (state: StoreState) => state.configuration;
1418

@@ -135,5 +139,44 @@ export function useSignalListeners(
135139
return useMemo(
136140
() => createSignalListeners(signals),
137141
[createSignalListeners, signals],
142+
143+
/**
144+
* A hook that retrieves the contributions for the given contribution
145+
* point given by `contribPoint`.
146+
*
147+
* @param contribPoint Contribution point name.
148+
* @typeParam S Type of the container state.
149+
*/
150+
export function useContributions<S extends object = object>(
151+
contribPoint: string,
152+
): ContributionState<S>[] {
153+
const contributionsRecord = useContributionsRecord();
154+
return contributionsRecord[contribPoint] as ContributionState<S>[];
155+
}
156+
157+
/**
158+
* A hook that creates an array of length `numContribs` with stable
159+
* component change handlers of type `ComponentChangeHandler` for
160+
* the contribution point given by `contribPoint`.
161+
*
162+
* @param contribPoint Contribution point name.
163+
* @param numContribs Number of contributions. This should be the length
164+
* of the array of contributions you get using the `useContributions` hook.
165+
*/
166+
export function useComponentChangeHandlers(
167+
contribPoint: string,
168+
numContribs: number,
169+
): ComponentChangeHandler[] {
170+
return useMemo(
171+
() =>
172+
Array.from({ length: numContribs }).map(
173+
(_, contribIndex) => (componentEvent: ComponentChangeEvent) =>
174+
void handleComponentChange(
175+
contribPoint,
176+
contribIndex,
177+
componentEvent,
178+
),
179+
),
180+
[contribPoint, numContribs],
138181
);
139182
}

chartlets.js/src/lib/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,7 @@ export {
1818
useExtensions,
1919
useContributionsResult,
2020
useContributionsRecord,
21+
useContributions,
22+
useComponentChangeHandlers,
2123
makeContributionsHook,
2224
} from "@/lib/hooks";

chartlets.py/CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
## Version 0.0.22 (in development)
22

3+
* Replacing components is now possible by using an
4+
`Output` with `property` set to an empty string. (#38)
5+
36
* `Component` children can now also be text nodes (of type `string`).
47

58
* `Typography` component has children instead of `text`.

chartlets.py/chartlets/channel.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
from abc import ABC
22
from typing import Any, Literal
33

4-
from .util.assertions import assert_is_instance_of
5-
from .util.assertions import assert_is_none
6-
from .util.assertions import assert_is_one_of
4+
from .util.assertions import (
5+
assert_is_given,
6+
assert_is_instance_of,
7+
assert_is_none,
8+
assert_is_one_of,
9+
)
710

811

912
Link = Literal["component"] | Literal["container"] | Literal["app"]
1013

14+
COMPONENT = ""
15+
"""Special property value that can be used
16+
to refer to the entire component.
17+
"""
18+
1119

1220
class Channel(ABC):
1321
"""Base class for `Input`, `State`, and `Output`.
@@ -53,7 +61,8 @@ class Input(Channel):
5361
Args:
5462
id:
5563
Value of a component's "id" property.
56-
Used only if `source` is `"component"`.
64+
Required, if `source` is `"component"` (the default).
65+
Otherwise, it must not be passed.
5766
property:
5867
Name of the property of a component or state.
5968
To address properties in nested objects or arrays
@@ -82,7 +91,7 @@ class State(Input):
8291
8392
Just like an `Input`, a `State` describes from which property in which state
8493
a parameter value is read, but according state changes
85-
will **not*Ü* trigger callback invocation.
94+
will **not* trigger callback invocation.
8695
8796
Args:
8897
id:
@@ -125,6 +134,8 @@ class Output(Channel):
125134
To address properties in nested objects or arrays
126135
use a dot (`.`) to separate property names and array
127136
indexes.
137+
If `target` is `"component"` the empty string can be used
138+
to refer to entire components.
128139
target: One of `"component"` (the default), `"container"`,
129140
or `"app"`.
130141
"""
@@ -154,22 +165,37 @@ def _validate_input_params(
154165
def _validate_output_params(
155166
target: Link | None, id: str | None, property: str | None
156167
) -> tuple[Link, str | None, str | None]:
157-
return _validate_params("target", target, id, property)
168+
return _validate_params("target", target, id, property, output=True)
158169

159170

160171
# noinspection PyShadowingBuiltins
161172
def _validate_params(
162-
link_name: str, link: Link | None, id: str | None, property: str | None
173+
link_name: str,
174+
link: Link | None,
175+
id: str | None,
176+
property: str | None,
177+
output: bool = False,
163178
) -> tuple[Link, str | None, str | None]:
164-
assert_is_one_of(link_name, link, ("component", "container", "app", None))
165-
if not link or link == "component":
166-
assert_is_instance_of("id", id, (str, NoneType))
179+
if link is None or link == "component":
180+
# Component states require an id
181+
# and property which defaults to "value"
182+
link = "component"
183+
assert_is_given("id", id)
184+
assert_is_instance_of("id", id, str)
167185
assert_is_instance_of("property", id, (str, NoneType))
168-
link = link or "component"
169-
if property is None and id is not None:
186+
if property is None:
187+
# property, if not provided, defaults to "value"
170188
property = "value"
189+
elif not output:
190+
# outputs are allowed to have an empty property value
191+
assert_is_given("property", property)
171192
else:
172-
assert_is_none("id", id)
193+
# Other states require a link and property
194+
# and should have no id
195+
assert_is_given(link_name, link)
196+
assert_is_one_of(link_name, link, ("container", "app"))
197+
assert_is_given("property", property)
173198
assert_is_instance_of("property", property, str)
199+
assert_is_none("id", id)
174200
# noinspection PyTypeChecker
175201
return link, id, property

chartlets.py/chartlets/util/assertions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,8 @@ def assert_is_instance_of(name: str, value: Any, type_set: Type | tuple[Type, ..
1818
def assert_is_none(name: str, value: Any):
1919
if value is not None:
2020
raise TypeError(f"value of {name!r} must be None, but was {value!r}")
21+
22+
23+
def assert_is_given(name: str, value: Any):
24+
if not value:
25+
raise ValueError(f"value for {name!r} must be given")

0 commit comments

Comments
 (0)