Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions chartlets.js/CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Version 0.0.26 (from 2024/11/23)

* Channels such as `Input`, `State`, `Output` no longer have a `link` property.
Instead, we use a special `id` format, namely `"@app"` and `@container`
to address states other than components. (#52)

## Version 0.0.25 (from 2024/11/23)

* `Registry.register()` now requires the `type`
Expand Down
4 changes: 2 additions & 2 deletions chartlets.js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion chartlets.js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "chartlets",
"version": "0.0.25",
"version": "0.0.26",
"description": "An experimental library for integrating interactive charts into existing JavaScript applications.",
"type": "module",
"files": [
Expand Down
4 changes: 2 additions & 2 deletions chartlets.js/src/lib/actions/handleComponentChange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export function handleComponentChange(
contribIndex,
stateChanges: [
{
link: "component",
id: changeEvent.id,
property: changeEvent.property,
value: changeEvent.value,
Expand Down Expand Up @@ -65,7 +64,8 @@ function getCallbackRequests(
const inputIndex = inputs.findIndex(
(input) =>
!input.noTrigger &&
(!input.link || input.link === "component") &&
input.id &&
!input.id.startsWith("@") &&
input.id === changeEvent.id &&
equalObjPaths(input.property, changeEvent.property),
);
Expand Down
2 changes: 1 addition & 1 deletion chartlets.js/src/lib/actions/handleHostStoreChange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function getHostStorePropertyRefs(): PropertyRef[] {
(contribution.callbacks || []).forEach(
(callback, callbackIndex) =>
(callback.inputs || []).forEach((input, inputIndex) => {
if (!input.noTrigger && input.link === "app" && input.property) {
if (!input.noTrigger && input.id === "@app" && input.property) {
propertyRefs.push({
contribPoint,
contribIndex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ describe("Test that applyContributionChangeRequests()", () => {
contribIndex: 0,
stateChanges: [
{
link: "component",
id: "dd1",
property: "value",
value: 14,
Expand All @@ -56,7 +55,6 @@ describe("Test that applyContributionChangeRequests()", () => {
contribIndex: 0,
stateChanges: [
{
link: "component",
id: "dd1",
property: "value",
value: 13,
Expand Down Expand Up @@ -88,7 +86,6 @@ describe("Test that applyContributionChangeRequests()", () => {
describe("Test that applyComponentStateChange()", () => {
it("changes state if values are different", () => {
const newState = applyComponentStateChange(componentTree, {
link: "component",
id: "cb1",
property: "value",
value: false,
Expand All @@ -102,7 +99,6 @@ describe("Test that applyComponentStateChange()", () => {

it("doesn't change the state if value stays the same", () => {
const newState = applyComponentStateChange(componentTree, {
link: "component",
id: "cb1",
property: "value",
value: true,
Expand All @@ -117,7 +113,6 @@ describe("Test that applyComponentStateChange()", () => {
children: ["Hello", "World"],
};
const newState = applyComponentStateChange(componentTree, {
link: "component",
id: "b1",
property: "",
value,
Expand Down
14 changes: 8 additions & 6 deletions chartlets.js/src/lib/actions/helpers/applyStateChangeRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ import {
isMutableHostStore,
type MutableHostStore,
} from "@/lib/types/state/options";
import {
isHostChannel,
isComponentChannel,
isContainerChannel,
} from "@/lib/types/model/channel";

export function applyStateChangeRequests(
stateChangeRequests: StateChangeRequest[],
Expand Down Expand Up @@ -52,14 +57,11 @@ export function applyContributionChangeRequests(
const contribution = contributionsRecord[contribPoint][contribIndex];
const container = applyStateChanges(
contribution.container,
stateChanges.filter((stateChange) => stateChange.link === "container"),
stateChanges.filter(isContainerChannel),
);
const component = applyComponentStateChanges(
contribution.component,
stateChanges.filter(
(stateChange) =>
!stateChange.link || stateChange.link === "component",
),
stateChanges.filter(isComponentChannel),
);
if (
container !== contribution.container ||
Expand Down Expand Up @@ -148,7 +150,7 @@ function applyHostStateChanges(
) {
stateChangeRequests.forEach((stateChangeRequest) => {
stateChangeRequest.stateChanges.forEach((stateChange) => {
if (stateChange.link === "app") {
if (isHostChannel(stateChange)) {
hostStore.set(formatObjPath(stateChange.property), stateChange.value);
}
});
Expand Down
89 changes: 21 additions & 68 deletions chartlets.js/src/lib/actions/helpers/getInputValues.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,95 +24,48 @@ const componentState = {

describe("Test that getInputValueFromComponent()", () => {
it("works on 1st level", () => {
expect(
getInputValueFromComponent(
{
link: "component",
id: "b1",
property: "value",
},
componentState,
),
).toEqual(14);
expect(getInputValueFromComponent(componentState, "b1", "value")).toEqual(
14,
);
});

it("works on 2nd level", () => {
expect(
getInputValueFromComponent(
{
link: "component",
id: "p1",
property: "chart",
},
componentState,
),
).toEqual(null);
expect(getInputValueFromComponent(componentState, "p1", "chart")).toEqual(
null,
);
});

it("works on 3rd level", () => {
expect(
getInputValueFromComponent(
{
link: "component",
id: "cb1",
property: "value",
},
componentState,
),
).toEqual(true);
expect(getInputValueFromComponent(componentState, "cb1", "value")).toEqual(
true,
);

expect(
getInputValueFromComponent(
{
link: "component",
id: "dd1",
property: "value",
},
componentState,
),
).toEqual(13);
expect(getInputValueFromComponent(componentState, "dd1", "value")).toEqual(
13,
);
});
});

describe("Test that getInputValueFromState()", () => {
it("works with input.id and input.property", () => {
const state = { x: { y: 26 } };
expect(
getInputValueFromState(
{ link: "component", id: "x", property: "y" },
state,
),
).toEqual(26);
});

it("works with arrays indexes", () => {
const state = { x: [4, 5, 6] };
expect(
getInputValueFromState(
{ link: "component", id: "x", property: "1" },
state,
),
).toEqual(5);
});

it("works without input.id", () => {
const state = { x: [4, 5, 6] };
expect(
getInputValueFromState({ link: "container", property: "x" }, state),
).toEqual([4, 5, 6]);
expect(getInputValueFromState(state, "x")).toEqual([4, 5, 6]);
});

it("works on 2nd level", () => {
const state = { x: { y: 15 } };
expect(
getInputValueFromState({ link: "container", property: "x.y" }, state),
).toEqual(15);
expect(getInputValueFromState(state, "x.y")).toEqual(15);
});

it("works on 3nd level", () => {
const state = { x: { y: [4, 5, 6] } };
expect(getInputValueFromState(state, "x.y.2")).toEqual(6);
});

it("works with non-object states", () => {
const state = 13;
expect(
getInputValueFromState({ link: "container", property: "x.y.2" }, state),
).toEqual(6);
getInputValueFromState(state as unknown as object, "x.y.2"),
).toBeUndefined();
});
});
54 changes: 28 additions & 26 deletions chartlets.js/src/lib/actions/helpers/getInputValues.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import type { Input } from "@/lib/types/model/channel";
import {
type Input,
isComponentChannel,
isContainerChannel,
isHostChannel,
} from "@/lib/types/model/channel";
import type { ContributionState } from "@/lib/types/state/contribution";
import {
type ComponentState,
isComponentState,
isContainerState,
} from "@/lib/types/state/component";
import { formatObjPath, getValue, normalizeObjPath } from "@/lib/utils/objPath";
import { formatObjPath, getValue, type ObjPathLike } from "@/lib/utils/objPath";
import { isObject } from "@/lib/utils/isObject";
import type { HostStore } from "@/lib/types/state/options";

Expand All @@ -27,13 +32,17 @@ export function getInputValue(
hostStore?: HostStore,
): unknown {
let inputValue: unknown = undefined;
const dataSource = input.link || "component";
if (dataSource === "component" && contributionState.component) {
inputValue = getInputValueFromComponent(input, contributionState.component);
} else if (dataSource === "container" && contributionState.container) {
inputValue = getInputValueFromState(input, contributionState.container);
} else if (dataSource === "app" && hostStore) {
inputValue = getInputValueFromHostStore(input, hostStore);
const { id, property } = input;
if (isComponentChannel(input) && contributionState.component) {
inputValue = getInputValueFromComponent(
contributionState.component,
id,
property,
);
} else if (isContainerChannel(input) && contributionState.container) {
inputValue = getInputValueFromState(contributionState.container, property);
} else if (isHostChannel(input) && hostStore) {
inputValue = getInputValueFromHostStore(hostStore, property);
} else {
console.warn(`input with unknown data source:`, input);
}
Expand All @@ -47,16 +56,17 @@ export function getInputValue(

// we export for testing only
export function getInputValueFromComponent(
input: Input,
componentState: ComponentState,
id: string,
property: ObjPathLike,
): unknown {
if (componentState.id === input.id) {
return getValue(componentState, input.property);
if (componentState.id === id) {
return getValue(componentState, property);
} else if (isContainerState(componentState)) {
for (let i = 0; i < componentState.children.length; i++) {
const item = componentState.children[i];
if (isComponentState(item)) {
const itemValue = getInputValueFromComponent(input, item);
const itemValue = getInputValueFromComponent(item, id, property);
if (itemValue !== noValue) {
return itemValue;
}
Expand All @@ -68,24 +78,16 @@ export function getInputValueFromComponent(

// we export for testing only
export function getInputValueFromState(
input: Input,
state: object | undefined,
property: ObjPathLike,
): unknown {
let inputValue: unknown = state;
if (input.id && isObject(inputValue)) {
inputValue = inputValue[input.id];
}
if (isObject(inputValue)) {
const state = inputValue;
const property = normalizeObjPath(input.property);
inputValue = getValue(state, property);
}
return inputValue;
return isObject(state) ? getValue(state, property) : undefined;
}

// we export for testing only
export function getInputValueFromHostStore(
input: Input,
hostStore: HostStore,
property: ObjPathLike,
): unknown {
return hostStore.get(formatObjPath(input.property));
return hostStore.get(formatObjPath(property));
}
28 changes: 18 additions & 10 deletions chartlets.js/src/lib/types/model/channel.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import type { ObjPathLike } from "@/lib/utils/objPath";

export type Link = "component" | "container" | "app";

/**
* Base for `Input` and `Output`.
*/
export interface Channel {
/**
* The link provides the source for inputs and that target for outputs.
* The identifier for a component or state.
*
* Special identifiers are:
* - `"@app"` the application state referred to by `HostStore`
* - `"@container"` the state referred to by contribution's container
*/
link: Link;

/**
* The identifier of a subcomponent.
* `id` is not needed if link == "AppInput" | "AppOutput".
*/
id?: string;
id: "@app" | "@container" | string;

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

export interface Output extends Channel {}

export function isComponentChannel(channel: Channel): boolean {
return Boolean(channel.id) && !channel.id.startsWith("@");
}

export function isHostChannel(channel: Channel): boolean {
return channel.id === "@app";
}

export function isContainerChannel(channel: Channel): boolean {
return channel.id === "@container";
}
Loading