Skip to content

Commit bfd2707

Browse files
committed
We now have a component registry
1 parent b5b5dea commit bfd2707

File tree

16 files changed

+209
-115
lines changed

16 files changed

+209
-115
lines changed

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,17 @@ import { describe, it, expect } from "vitest";
33
import { type ComponentState } from "@/lib";
44
import { type ContribPoint } from "@/lib/types/model/extension";
55
import { type StateChangeRequest } from "@/lib/types/model/callback";
6-
import { type BoxState } from "@/lib/components/Box";
7-
import { type PlotState } from "@/lib/components/Plot";
86
import { type ContributionState } from "@/lib/types/state/contribution";
97
import {
108
applyComponentStateChange,
119
applyContributionChangeRequests,
1210
} from "./applyStateChangeRequests";
1311

14-
const componentTree: ComponentState = {
12+
const componentTree = {
1513
type: "Box",
1614
id: "b1",
1715
children: [
18-
{ type: "Plot", id: "p1", chart: null } as PlotState,
16+
{ type: "Plot", id: "p1", chart: null },
1917
{
2018
type: "Box",
2119
id: "b2",
@@ -113,7 +111,7 @@ describe("Test that applyComponentStateChange()", () => {
113111
});
114112

115113
it("replaces state if property is empty string", () => {
116-
const value: BoxState = {
114+
const value = {
117115
type: "Box",
118116
id: "b1",
119117
children: ["Hello", "World"],

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import { describe, it, expect } from "vitest";
22

3-
import type { ComponentState } from "@/lib";
4-
import type { PlotState } from "@/lib/components/Plot";
53
import {
64
getInputValueFromComponent,
75
getInputValueFromState,
86
} from "./getInputValues";
97

10-
const componentState: ComponentState = {
8+
const componentState = {
119
type: "Box",
1210
id: "b1",
1311
children: [
14-
{ type: "Plot", id: "p1", chart: null } as PlotState,
12+
{ type: "Plot", id: "p1", chart: null },
1513
{
1614
type: "Box",
1715
id: "b2",

chartlets.js/src/lib/components/ComponentChildren.tsx renamed to chartlets.js/src/lib/component/Children.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import {
55
} from "@/lib/types/state/component";
66
import { Component } from "./Component";
77

8-
export interface ComponentChildrenProps {
8+
export interface ChildrenProps {
99
nodes?: ComponentNode[];
1010
onChange: ComponentChangeHandler;
1111
}
1212

13-
export function ComponentChildren({ nodes, onChange }: ComponentChildrenProps) {
13+
export function Children({ nodes, onChange }: ChildrenProps) {
1414
if (!nodes || nodes.length === 0) {
1515
return null;
1616
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { type ComponentChangeHandler } from "@/lib/types/state/event";
2+
import { registry } from "@/lib/component/Registry";
3+
4+
export interface ComponentProps {
5+
type: string;
6+
onChange: ComponentChangeHandler;
7+
}
8+
9+
export function Component(props: ComponentProps) {
10+
const { type: componentType } = props;
11+
const ActualComponent = registry.lookup(componentType);
12+
if (typeof ActualComponent === "function") {
13+
return <ActualComponent {...props} />;
14+
} else {
15+
console.error(
16+
`chartlets: invalid component type encountered: ${componentType}`,
17+
);
18+
return null;
19+
}
20+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, it, expect } from "vitest";
2+
3+
import { RegistryImpl } from "@/lib/component/Registry";
4+
5+
describe("Test that RegistryImpl", () => {
6+
it("works", () => {
7+
const registry = new RegistryImpl();
8+
expect(registry.types).toEqual([]);
9+
10+
const A = () => void 0;
11+
const B = () => void 0;
12+
const C = () => void 0;
13+
const unregisterA = registry.register(A);
14+
const unregisterB = registry.register(B);
15+
const unregisterC = registry.register(C);
16+
17+
expect(registry.lookup("A")).toBe(A);
18+
expect(registry.lookup("B")).toBe(B);
19+
expect(registry.lookup("C")).toBe(C);
20+
expect(new Set(registry.types)).toEqual(new Set(["A", "B", "C"]));
21+
22+
unregisterA();
23+
expect(registry.lookup("A")).toBeUndefined();
24+
expect(registry.lookup("B")).toBe(B);
25+
expect(registry.lookup("C")).toBe(C);
26+
expect(new Set(registry.types)).toEqual(new Set(["B", "C"]));
27+
28+
unregisterB();
29+
expect(registry.lookup("A")).toBeUndefined();
30+
expect(registry.lookup("B")).toBeUndefined();
31+
expect(registry.lookup("C")).toBe(C);
32+
expect(new Set(registry.types)).toEqual(new Set(["C"]));
33+
34+
const C2 = () => void 0;
35+
const unregisterC2 = registry.register(C2, "C");
36+
expect(registry.lookup("A")).toBeUndefined();
37+
expect(registry.lookup("B")).toBeUndefined();
38+
expect(registry.lookup("C")).toBe(C2);
39+
expect(new Set(registry.types)).toEqual(new Set(["C"]));
40+
41+
unregisterC2();
42+
expect(registry.lookup("A")).toBeUndefined();
43+
expect(registry.lookup("B")).toBeUndefined();
44+
expect(registry.lookup("C")).toBe(C);
45+
expect(new Set(registry.types)).toEqual(new Set(["C"]));
46+
47+
unregisterC();
48+
expect(registry.lookup("A")).toBeUndefined();
49+
expect(registry.lookup("B")).toBeUndefined();
50+
expect(registry.lookup("C")).toBeUndefined();
51+
expect(registry.types).toEqual([]);
52+
});
53+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { FC } from "react";
2+
import type { ComponentProps } from "@/lib/component/Component";
3+
4+
/**
5+
* A registry for Chartlets components.
6+
*/
7+
export interface Registry {
8+
/**
9+
* Register a React component that renders a Chartlets component.
10+
*
11+
* @param component A functional React component.
12+
* @param type The Chartlets component's type name.
13+
* If not provided, `component.name` is used.
14+
*/
15+
register(component: FC<ComponentProps>, type?: string): () => void;
16+
17+
/**
18+
* Lookup the component of the provided type.
19+
*
20+
* @param type The Chartlets component's type name.
21+
*/
22+
lookup(type: string): FC<ComponentProps> | undefined;
23+
24+
/**
25+
* Get the type names of all registered components.
26+
*/
27+
types: string[];
28+
}
29+
30+
// export for testing only
31+
export class RegistryImpl implements Registry {
32+
private components = new Map<string, FC<ComponentProps>>();
33+
34+
register(component: FC<ComponentProps>, type?: string): () => void {
35+
type = type || component.name;
36+
const oldComponent = this.components.get(type);
37+
this.components.set(type, component);
38+
return () => {
39+
if (typeof oldComponent === "function") {
40+
this.components.set(type, oldComponent);
41+
} else {
42+
this.components.delete(type);
43+
}
44+
};
45+
}
46+
47+
lookup(type: string): FC<ComponentProps> | undefined {
48+
return this.components.get(type);
49+
}
50+
51+
get types(): string[] {
52+
return Array.from(this.components.keys());
53+
}
54+
}
55+
56+
export const registry = new RegistryImpl();
Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
import MuiBox from "@mui/material/Box";
22

3-
import { type ContainerState } from "@/lib/types/state/component";
4-
import { type ComponentChangeHandler } from "@/lib/types/state/event";
5-
import { ComponentChildren } from "./ComponentChildren";
3+
import type { ComponentState } from "@/lib/types/state/component";
4+
import { Children } from "../component/Children";
5+
import type { ComponentProps } from "@/lib/component/Component";
66

7-
export interface BoxState extends ContainerState {
8-
type: "Box";
9-
}
7+
interface BoxState extends ComponentState {}
108

11-
export interface BoxProps extends Omit<BoxState, "type"> {
12-
onChange: ComponentChangeHandler;
13-
}
9+
interface BoxProps extends ComponentProps, BoxState {}
1410

1511
export function Box({ id, style, children, onChange }: BoxProps) {
1612
return (
1713
<MuiBox id={id} style={style}>
18-
<ComponentChildren nodes={children} onChange={onChange} />
14+
<Children nodes={children} onChange={onChange} />
1915
</MuiBox>
2016
);
2117
}

chartlets.js/src/lib/components/Button.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,16 @@ import { type MouseEvent } from "react";
22
import MuiButton from "@mui/material/Button";
33

44
import { type ComponentState } from "@/lib/types/state/component";
5-
import { type ComponentChangeHandler } from "@/lib/types/state/event";
5+
import type { ComponentProps } from "@/lib/component/Component";
66

7-
export interface ButtonState extends ComponentState {
8-
type: "Button";
9-
text: string;
7+
interface ButtonState extends ComponentState {
8+
text?: string;
109
}
1110

12-
export interface ButtonProps extends Omit<ButtonState, "type"> {
13-
onChange: ComponentChangeHandler;
14-
}
11+
interface ButtonProps extends ComponentProps, ButtonState {}
1512

1613
export function Button({
14+
type,
1715
id,
1816
name,
1917
style,
@@ -24,7 +22,7 @@ export function Button({
2422
const handleClick = (_event: MouseEvent<HTMLButtonElement>) => {
2523
if (id) {
2624
onChange({
27-
componentType: "Button",
25+
componentType: type,
2826
id: id,
2927
property: "clicked",
3028
value: true,

chartlets.js/src/lib/components/Checkbox.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,17 @@ import MuiFormControl from "@mui/material/FormControl";
44
import MuiFormControlLabel from "@mui/material/FormControlLabel";
55

66
import { type ComponentState } from "@/lib/types/state/component";
7-
import { type ComponentChangeHandler } from "@/lib/types/state/event";
7+
import type { ComponentProps } from "@/lib/component/Component";
88

9-
export interface CheckboxState extends ComponentState {
10-
type: "Checkbox";
11-
label: string;
12-
value?: boolean;
9+
interface CheckboxState extends ComponentState {
10+
label?: string;
11+
value?: boolean | undefined;
1312
}
1413

15-
export interface CheckboxProps extends Omit<CheckboxState, "type"> {
16-
onChange: ComponentChangeHandler;
17-
}
14+
interface CheckboxProps extends ComponentProps, CheckboxState {}
1815

1916
export function Checkbox({
17+
type,
2018
id,
2119
name,
2220
value,
@@ -28,7 +26,7 @@ export function Checkbox({
2826
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
2927
if (id) {
3028
return onChange({
31-
componentType: "Checkbox",
29+
componentType: type,
3230
id: id,
3331
property: "value",
3432
value: event.currentTarget.checked,

chartlets.js/src/lib/components/CircularProgress.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import MuiCircularProgress from "@mui/material/CircularProgress";
22

33
import { type ComponentState } from "@/lib/types/state/component";
4+
import type { ComponentProps } from "@/lib/component/Component";
45

5-
export interface CircularProgressState extends ComponentState {
6-
type: "CircularProgress";
6+
interface CircularProgressState extends ComponentState {
77
size?: number | string;
88
value?: number;
99
variant?: "determinate" | "indeterminate";
1010
}
1111

12-
export interface CircularProgressProps
13-
extends Omit<CircularProgressState, "type"> {}
12+
interface CircularProgressProps extends ComponentProps, CircularProgressState {}
1413

1514
export function CircularProgress({
1615
id,

0 commit comments

Comments
 (0)