diff --git a/chartlets.js/src/lib/actions/helpers/applyStateChangeRequests.test.ts b/chartlets.js/src/lib/actions/helpers/applyStateChangeRequests.test.ts
index 45db5b6b..b78a9ae7 100644
--- a/chartlets.js/src/lib/actions/helpers/applyStateChangeRequests.test.ts
+++ b/chartlets.js/src/lib/actions/helpers/applyStateChangeRequests.test.ts
@@ -1,23 +1,19 @@
import { describe, it, expect } from "vitest";
+import { type ComponentState } from "@/lib";
import { type ContribPoint } from "@/lib/types/model/extension";
import { type StateChangeRequest } from "@/lib/types/model/callback";
-import {
- type BoxState,
- type ComponentState,
- type PlotState,
-} from "@/lib/types/state/component";
import { type ContributionState } from "@/lib/types/state/contribution";
import {
applyComponentStateChange,
applyContributionChangeRequests,
} from "./applyStateChangeRequests";
-const componentTree: ComponentState = {
+const componentTree = {
type: "Box",
id: "b1",
children: [
- { type: "Plot", id: "p1", chart: null } as PlotState,
+ { type: "Plot", id: "p1", chart: null },
{
type: "Box",
id: "b2",
@@ -115,7 +111,7 @@ describe("Test that applyComponentStateChange()", () => {
});
it("replaces state if property is empty string", () => {
- const value: BoxState = {
+ const value = {
type: "Box",
id: "b1",
children: ["Hello", "World"],
diff --git a/chartlets.js/src/lib/actions/helpers/getInputValues.test.ts b/chartlets.js/src/lib/actions/helpers/getInputValues.test.ts
index fbf314a3..145643d1 100644
--- a/chartlets.js/src/lib/actions/helpers/getInputValues.test.ts
+++ b/chartlets.js/src/lib/actions/helpers/getInputValues.test.ts
@@ -1,16 +1,15 @@
import { describe, it, expect } from "vitest";
-import type { ComponentState, PlotState } from "@/lib/types/state/component";
import {
getInputValueFromComponent,
getInputValueFromState,
} from "./getInputValues";
-const componentState: ComponentState = {
+const componentState = {
type: "Box",
id: "b1",
children: [
- { type: "Plot", id: "p1", chart: null } as PlotState,
+ { type: "Plot", id: "p1", chart: null },
{
type: "Box",
id: "b2",
diff --git a/chartlets.js/src/lib/components/ComponentChildren.tsx b/chartlets.js/src/lib/component/Children.tsx
similarity index 86%
rename from chartlets.js/src/lib/components/ComponentChildren.tsx
rename to chartlets.js/src/lib/component/Children.tsx
index 18ff8193..cbed58e4 100644
--- a/chartlets.js/src/lib/components/ComponentChildren.tsx
+++ b/chartlets.js/src/lib/component/Children.tsx
@@ -5,12 +5,12 @@ import {
} from "@/lib/types/state/component";
import { Component } from "./Component";
-export interface ComponentChildrenProps {
+export interface ChildrenProps {
nodes?: ComponentNode[];
onChange: ComponentChangeHandler;
}
-export function ComponentChildren({ nodes, onChange }: ComponentChildrenProps) {
+export function Children({ nodes, onChange }: ChildrenProps) {
if (!nodes || nodes.length === 0) {
return null;
}
diff --git a/chartlets.js/src/lib/component/Component.tsx b/chartlets.js/src/lib/component/Component.tsx
new file mode 100644
index 00000000..2cad0537
--- /dev/null
+++ b/chartlets.js/src/lib/component/Component.tsx
@@ -0,0 +1,20 @@
+import { type ComponentChangeHandler } from "@/lib/types/state/event";
+import { registry } from "@/lib/component/Registry";
+
+export interface ComponentProps {
+ type: string;
+ onChange: ComponentChangeHandler;
+}
+
+export function Component(props: ComponentProps) {
+ const { type: componentType } = props;
+ const ActualComponent = registry.lookup(componentType);
+ if (typeof ActualComponent === "function") {
+ return ;
+ } else {
+ console.error(
+ `chartlets: invalid component type encountered: ${componentType}`,
+ );
+ return null;
+ }
+}
diff --git a/chartlets.js/src/lib/component/Registry.test.ts b/chartlets.js/src/lib/component/Registry.test.ts
new file mode 100644
index 00000000..bbbff336
--- /dev/null
+++ b/chartlets.js/src/lib/component/Registry.test.ts
@@ -0,0 +1,53 @@
+import { describe, it, expect } from "vitest";
+
+import { RegistryImpl } from "@/lib/component/Registry";
+
+describe("Test that RegistryImpl", () => {
+ it("works", () => {
+ const registry = new RegistryImpl();
+ expect(registry.types).toEqual([]);
+
+ const A = () => void 0;
+ const B = () => void 0;
+ const C = () => void 0;
+ const unregisterA = registry.register(A);
+ const unregisterB = registry.register(B);
+ const unregisterC = registry.register(C);
+
+ expect(registry.lookup("A")).toBe(A);
+ expect(registry.lookup("B")).toBe(B);
+ expect(registry.lookup("C")).toBe(C);
+ expect(new Set(registry.types)).toEqual(new Set(["A", "B", "C"]));
+
+ unregisterA();
+ expect(registry.lookup("A")).toBeUndefined();
+ expect(registry.lookup("B")).toBe(B);
+ expect(registry.lookup("C")).toBe(C);
+ expect(new Set(registry.types)).toEqual(new Set(["B", "C"]));
+
+ unregisterB();
+ expect(registry.lookup("A")).toBeUndefined();
+ expect(registry.lookup("B")).toBeUndefined();
+ expect(registry.lookup("C")).toBe(C);
+ expect(new Set(registry.types)).toEqual(new Set(["C"]));
+
+ const C2 = () => void 0;
+ const unregisterC2 = registry.register(C2, "C");
+ expect(registry.lookup("A")).toBeUndefined();
+ expect(registry.lookup("B")).toBeUndefined();
+ expect(registry.lookup("C")).toBe(C2);
+ expect(new Set(registry.types)).toEqual(new Set(["C"]));
+
+ unregisterC2();
+ expect(registry.lookup("A")).toBeUndefined();
+ expect(registry.lookup("B")).toBeUndefined();
+ expect(registry.lookup("C")).toBe(C);
+ expect(new Set(registry.types)).toEqual(new Set(["C"]));
+
+ unregisterC();
+ expect(registry.lookup("A")).toBeUndefined();
+ expect(registry.lookup("B")).toBeUndefined();
+ expect(registry.lookup("C")).toBeUndefined();
+ expect(registry.types).toEqual([]);
+ });
+});
diff --git a/chartlets.js/src/lib/component/Registry.ts b/chartlets.js/src/lib/component/Registry.ts
new file mode 100644
index 00000000..c58e6a6e
--- /dev/null
+++ b/chartlets.js/src/lib/component/Registry.ts
@@ -0,0 +1,68 @@
+import type { FC } from "react";
+import type { ComponentProps } from "@/lib/component/Component";
+
+/**
+ * A registry for Chartlets components.
+ */
+export interface Registry {
+ /**
+ * Register a React component that renders a Chartlets component.
+ *
+ * @param component A functional React component.
+ * @param type The Chartlets component's type name.
+ * If not provided, `component.name` is used.
+ */
+ register(component: FC, type?: string): () => void;
+
+ /**
+ * Lookup the component of the provided type.
+ *
+ * @param type The Chartlets component's type name.
+ */
+ lookup(type: string): FC | undefined;
+
+ /**
+ * Get the type names of all registered components.
+ */
+ types: string[];
+}
+
+// export for testing only
+export class RegistryImpl implements Registry {
+ private components = new Map>();
+
+ register(component: FC, type?: string): () => void {
+ type = type || component.name;
+ const oldComponent = this.components.get(type);
+ this.components.set(type, component);
+ return () => {
+ if (typeof oldComponent === "function") {
+ this.components.set(type, oldComponent);
+ } else {
+ this.components.delete(type);
+ }
+ };
+ }
+
+ lookup(type: string): FC | undefined {
+ return this.components.get(type);
+ }
+
+ get types(): string[] {
+ return Array.from(this.components.keys());
+ }
+}
+
+/**
+ * The Chartly component registry.
+ *
+ * Use `registry.register(C)` to register your own component `C`.
+ *
+ * `C` must be a functional React component with at least the following
+ * two properties:
+ *
+ * - `type: string`: your component's type name.
+ * - `onChange: ComponentChangeHandler`: an event handler
+ * that your component may call to signal change events.
+ */
+export const registry = new RegistryImpl();
diff --git a/chartlets.js/src/lib/components/Box.tsx b/chartlets.js/src/lib/components/Box.tsx
index 5fe17018..b6335544 100644
--- a/chartlets.js/src/lib/components/Box.tsx
+++ b/chartlets.js/src/lib/components/Box.tsx
@@ -1,17 +1,17 @@
import MuiBox from "@mui/material/Box";
-import { type BoxState } from "@/lib/types/state/component";
-import { type ComponentChangeHandler } from "@/lib/types/state/event";
-import { ComponentChildren } from "./ComponentChildren";
+import type { ComponentState } from "@/lib/types/state/component";
+import { Children } from "../component/Children";
+import type { ComponentProps } from "@/lib/component/Component";
-export interface BoxProps extends Omit {
- onChange: ComponentChangeHandler;
-}
+interface BoxState extends ComponentState {}
+
+interface BoxProps extends ComponentProps, BoxState {}
export function Box({ id, style, children, onChange }: BoxProps) {
return (
-
+
);
}
diff --git a/chartlets.js/src/lib/components/Button.tsx b/chartlets.js/src/lib/components/Button.tsx
index f444641e..2f5f40a4 100644
--- a/chartlets.js/src/lib/components/Button.tsx
+++ b/chartlets.js/src/lib/components/Button.tsx
@@ -1,14 +1,17 @@
import { type MouseEvent } from "react";
import MuiButton from "@mui/material/Button";
-import { type ButtonState } from "@/lib/types/state/component";
-import { type ComponentChangeHandler } from "@/lib/types/state/event";
+import { type ComponentState } from "@/lib/types/state/component";
+import type { ComponentProps } from "@/lib/component/Component";
-export interface ButtonProps extends Omit {
- onChange: ComponentChangeHandler;
+interface ButtonState extends ComponentState {
+ text?: string;
}
+interface ButtonProps extends ComponentProps, ButtonState {}
+
export function Button({
+ type,
id,
name,
style,
@@ -19,7 +22,7 @@ export function Button({
const handleClick = (_event: MouseEvent) => {
if (id) {
onChange({
- componentType: "Button",
+ componentType: type,
id: id,
property: "clicked",
value: true,
diff --git a/chartlets.js/src/lib/components/Checkbox.tsx b/chartlets.js/src/lib/components/Checkbox.tsx
index 1f048277..140ef8fb 100644
--- a/chartlets.js/src/lib/components/Checkbox.tsx
+++ b/chartlets.js/src/lib/components/Checkbox.tsx
@@ -3,14 +3,18 @@ import MuiCheckbox from "@mui/material/Checkbox";
import MuiFormControl from "@mui/material/FormControl";
import MuiFormControlLabel from "@mui/material/FormControlLabel";
-import { type CheckboxState } from "@/lib/types/state/component";
-import { type ComponentChangeHandler } from "@/lib/types/state/event";
+import { type ComponentState } from "@/lib/types/state/component";
+import type { ComponentProps } from "@/lib/component/Component";
-export interface CheckboxProps extends Omit {
- onChange: ComponentChangeHandler;
+interface CheckboxState extends ComponentState {
+ label?: string;
+ value?: boolean | undefined;
}
+interface CheckboxProps extends ComponentProps, CheckboxState {}
+
export function Checkbox({
+ type,
id,
name,
value,
@@ -22,7 +26,7 @@ export function Checkbox({
const handleChange = (event: ChangeEvent) => {
if (id) {
return onChange({
- componentType: "Checkbox",
+ componentType: type,
id: id,
property: "value",
value: event.currentTarget.checked,
diff --git a/chartlets.js/src/lib/components/Component.tsx b/chartlets.js/src/lib/components/Component.tsx
deleted file mode 100644
index be06837a..00000000
--- a/chartlets.js/src/lib/components/Component.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { type ComponentState } from "@/lib/types/state/component";
-import { type ComponentChangeHandler } from "@/lib/types/state/event";
-import { Button, type ButtonProps } from "./Button";
-import { Box, type BoxProps } from "./Box";
-import { Checkbox, type CheckboxProps } from "./Checkbox";
-import { Select, type SelectProps } from "./Select";
-import { Plot, type PlotProps } from "./Plot";
-import { Typography, type TypographyProps } from "@/lib/components/Typography";
-
-export interface ComponentProps extends ComponentState {
- onChange: ComponentChangeHandler;
-}
-
-export function Component({ type, ...props }: ComponentProps) {
- // TODO: allow for registering children via their types
- // and make following code generic.
- //
- // const DashiComp = Registry.getComponent(link);
- // return return ;
- //
- if (type === "Plot") {
- return ;
- } else if (type === "Select") {
- return ;
- } else if (type === "Button") {
- return ;
- } else if (type === "Box") {
- return ;
- } else if (type === "Checkbox") {
- return ;
- } else if (type === "Typography") {
- return ;
- }
-}
diff --git a/chartlets.js/src/lib/components/Plot.tsx b/chartlets.js/src/lib/components/Plot.tsx
index 62d939b7..c4cd31b1 100644
--- a/chartlets.js/src/lib/components/Plot.tsx
+++ b/chartlets.js/src/lib/components/Plot.tsx
@@ -1,13 +1,20 @@
-import { VegaLite } from "react-vega";
+import { VegaLite, type VisualizationSpec } from "react-vega";
-import { type PlotState } from "@/lib/types/state/component";
-import { type ComponentChangeHandler } from "@/lib/types/state/event";
+import { type ComponentState } from "@/lib/types/state/component";
+import type { ComponentProps } from "@/lib/component/Component";
-export interface PlotProps extends Omit {
- onChange: ComponentChangeHandler;
+interface PlotState extends ComponentState {
+ chart?:
+ | (VisualizationSpec & {
+ datasets?: Record; // Add the datasets property
+ })
+ | null
+ | undefined;
}
-export function Plot({ id, style, chart, onChange }: PlotProps) {
+interface PlotProps extends ComponentProps, PlotState {}
+
+export function Plot({ type, id, style, chart, onChange }: PlotProps) {
if (!chart) {
return ;
}
@@ -15,7 +22,7 @@ export function Plot({ id, style, chart, onChange }: PlotProps) {
const handleSignal = (_signalName: string, value: unknown) => {
if (id) {
return onChange({
- componentType: "Plot",
+ componentType: type,
id: id,
property: "points",
value: value,
diff --git a/chartlets.js/src/lib/components/Select.tsx b/chartlets.js/src/lib/components/Select.tsx
index 24610abd..1aed5c62 100644
--- a/chartlets.js/src/lib/components/Select.tsx
+++ b/chartlets.js/src/lib/components/Select.tsx
@@ -3,17 +3,24 @@ import MuiInputLabel from "@mui/material/InputLabel";
import MuiMenuItem from "@mui/material/MenuItem";
import MuiSelect, { type SelectChangeEvent } from "@mui/material/Select";
-import {
- type SelectOption,
- type SelectState,
-} from "@/lib/types/state/component";
-import { type ComponentChangeHandler } from "@/lib/types/state/event";
+import { type ComponentState } from "@/lib/types/state/component";
+import type { ComponentProps } from "@/lib/component/Component";
-export interface SelectProps extends Omit {
- onChange: ComponentChangeHandler;
+export type SelectOption =
+ | string
+ | number
+ | [string, string]
+ | [number, string]
+ | { value: string | number; label?: string };
+
+interface SelectState extends ComponentState {
+ options?: SelectOption[];
}
+interface SelectProps extends ComponentProps, SelectState {}
+
export function Select({
+ type,
id,
name,
value,
@@ -31,9 +38,8 @@ export function Select({
if (typeof value == "number") {
newValue = Number.parseInt(newValue);
}
-
return onChange({
- componentType: "Select",
+ componentType: type,
id: id,
property: "value",
value: newValue,
diff --git a/chartlets.js/src/lib/components/Typography.tsx b/chartlets.js/src/lib/components/Typography.tsx
index 5e1d1620..88a65ce5 100644
--- a/chartlets.js/src/lib/components/Typography.tsx
+++ b/chartlets.js/src/lib/components/Typography.tsx
@@ -1,17 +1,39 @@
import MuiTypography from "@mui/material/Typography";
+import { type TypographyVariant } from "@mui/material";
-import type { TypographyState } from "@/lib/types/state/component";
-import { ComponentChildren } from "@/lib/components/ComponentChildren";
-import type { ComponentChangeHandler } from "@/lib";
+import { Children } from "@/lib/component/Children";
+import type { ComponentState } from "@/lib/types/state/component";
+import type { ComponentProps } from "@/lib/component/Component";
-export interface TypographyProps extends Omit {
- onChange: ComponentChangeHandler;
+interface TypographyState extends ComponentState {
+ align?: "right" | "left" | "center" | "inherit" | "justify";
+ gutterBottom?: boolean;
+ noWrap?: boolean;
+ variant?: TypographyVariant;
}
-export function Typography({ id, style, children, onChange }: TypographyProps) {
+interface TypographyProps extends ComponentProps, TypographyState {}
+
+export function Typography({
+ id,
+ style,
+ align,
+ gutterBottom,
+ noWrap,
+ variant,
+ children: nodes,
+ onChange,
+}: TypographyProps) {
return (
-
-
+
+
);
}
diff --git a/chartlets.js/src/lib/index.ts b/chartlets.js/src/lib/index.ts
index de27e62a..12631ce2 100644
--- a/chartlets.js/src/lib/index.ts
+++ b/chartlets.js/src/lib/index.ts
@@ -1,7 +1,13 @@
+/////////////////////////////////////////////////////////////////////
+// The Chartlets TypeScript API
+
// Types
export { type Contribution } from "@/lib/types/model/contribution";
export { type ContributionState } from "@/lib/types/state/contribution";
-export { type ComponentState } from "@/lib/types/state/component";
+export type {
+ ComponentState,
+ ContainerState,
+} from "@/lib/types/state/component";
export {
type ComponentChangeEvent,
type ComponentChangeHandler,
@@ -10,9 +16,12 @@ export {
export { initializeContributions } from "@/lib/actions/initializeContributions";
export { handleComponentChange } from "@/lib/actions/handleComponentChange";
export { updateContributionContainer } from "@/lib/actions/updateContributionContainer";
-// React Components
-export { Component } from "@/lib/components/Component";
-// React Hooks
+// Component registry
+export { type Registry, registry } from "@/lib/component/Registry";
+// React components
+export { Component } from "@/lib/component/Component";
+export { Children } from "@/lib/component/Children";
+// React hooks
export {
useConfiguration,
useExtensions,
@@ -22,3 +31,22 @@ export {
useComponentChangeHandlers,
makeContributionsHook,
} from "@/lib/hooks";
+
+/////////////////////////////////////////////////////////////////////
+// Register standard Chartlets components
+
+import { registry } from "@/lib/component/Registry";
+
+import { Box } from "@/lib/components/Box";
+import { Button } from "@/lib/components/Button";
+import { Checkbox } from "@/lib/components/Checkbox";
+import { Plot } from "@/lib/components/Plot";
+import { Select } from "@/lib/components/Select";
+import { Typography } from "@/lib/components/Typography";
+
+registry.register(Box);
+registry.register(Button);
+registry.register(Checkbox);
+registry.register(Plot);
+registry.register(Select);
+registry.register(Typography);
diff --git a/chartlets.js/src/lib/types/state/component.ts b/chartlets.js/src/lib/types/state/component.ts
index f0a72141..ba600bf4 100644
--- a/chartlets.js/src/lib/types/state/component.ts
+++ b/chartlets.js/src/lib/types/state/component.ts
@@ -1,5 +1,4 @@
import { type CSSProperties } from "react";
-import type { VisualizationSpec } from "react-vega";
import { isObject } from "@/lib/utils/isObject";
export type ComponentType =
@@ -21,12 +20,12 @@ export type ComponentNode =
export interface ComponentState {
// TODO: Rename to tag, so we can also have
// (Html)ElementState along with ComponentState
- type: ComponentType;
+ type: string;
children?: ComponentNode[];
// common HTML attributes
id?: string;
name?: string;
- value?: boolean | string | number;
+ value?: unknown;
style?: CSSProperties;
disabled?: boolean;
label?: string;
@@ -36,46 +35,6 @@ export interface ContainerState extends ComponentState {
children: ComponentNode[];
}
-export type SelectOption =
- | string
- | number
- | [string, string]
- | [number, string]
- | { value: string | number; label?: string };
-
-export interface SelectState extends ComponentState {
- type: "Select";
- options: SelectOption[];
-}
-
-export interface ButtonState extends ComponentState {
- type: "Button";
- text: string;
-}
-
-export interface CheckboxState extends ComponentState {
- type: "Checkbox";
- label: string;
- value?: boolean;
-}
-
-export interface PlotState extends ComponentState {
- type: "Plot";
- chart:
- | (VisualizationSpec & {
- datasets?: Record; // Add the datasets property
- })
- | null;
-}
-
-export interface BoxState extends ContainerState {
- type: "Box";
-}
-
-export interface TypographyState extends ContainerState {
- type: "Typography";
-}
-
export function isComponentState(object: unknown): object is ComponentState {
return isObject(object) && typeof object.type === "string";
}
diff --git a/chartlets.js/src/lib/types/state/event.ts b/chartlets.js/src/lib/types/state/event.ts
index 672e9ca3..a6766096 100644
--- a/chartlets.js/src/lib/types/state/event.ts
+++ b/chartlets.js/src/lib/types/state/event.ts
@@ -1,8 +1,7 @@
-import { type ComponentType } from "@/lib/types/state/component";
import type { ObjPathLike } from "@/lib/utils/objPath";
export interface ComponentChangeEvent {
- componentType: ComponentType;
+ componentType: string;
// See commonality with StateChange
id: string;
property: ObjPathLike;