Skip to content

Commit 481f12b

Browse files
authored
Merge pull request #45 from bcdev/forman-x-reorganising_components
Reorganising and enhancing components
2 parents f3a299a + f9b1f5b commit 481f12b

File tree

16 files changed

+267
-137
lines changed

16 files changed

+267
-137
lines changed

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

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

3+
import { type ComponentState } from "@/lib";
34
import { type ContribPoint } from "@/lib/types/model/extension";
45
import { type StateChangeRequest } from "@/lib/types/model/callback";
5-
import {
6-
type BoxState,
7-
type ComponentState,
8-
type PlotState,
9-
} from "@/lib/types/state/component";
106
import { type ContributionState } from "@/lib/types/state/contribution";
117
import {
128
applyComponentStateChange,
139
applyContributionChangeRequests,
1410
} from "./applyStateChangeRequests";
1511

16-
const componentTree: ComponentState = {
12+
const componentTree = {
1713
type: "Box",
1814
id: "b1",
1915
children: [
20-
{ type: "Plot", id: "p1", chart: null } as PlotState,
16+
{ type: "Plot", id: "p1", chart: null },
2117
{
2218
type: "Box",
2319
id: "b2",
@@ -115,7 +111,7 @@ describe("Test that applyComponentStateChange()", () => {
115111
});
116112

117113
it("replaces state if property is empty string", () => {
118-
const value: BoxState = {
114+
const value = {
119115
type: "Box",
120116
id: "b1",
121117
children: ["Hello", "World"],

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

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

3-
import type { ComponentState, PlotState } from "@/lib/types/state/component";
43
import {
54
getInputValueFromComponent,
65
getInputValueFromState,
76
} from "./getInputValues";
87

9-
const componentState: ComponentState = {
8+
const componentState = {
109
type: "Box",
1110
id: "b1",
1211
children: [
13-
{ type: "Plot", id: "p1", chart: null } as PlotState,
12+
{ type: "Plot", id: "p1", chart: null },
1413
{
1514
type: "Box",
1615
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: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
/**
57+
* The Chartly component registry.
58+
*
59+
* Use `registry.register(C)` to register your own component `C`.
60+
*
61+
* `C` must be a functional React component with at least the following
62+
* two properties:
63+
*
64+
* - `type: string`: your component's type name.
65+
* - `onChange: ComponentChangeHandler`: an event handler
66+
* that your component may call to signal change events.
67+
*/
68+
export const registry = new RegistryImpl();
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import MuiBox from "@mui/material/Box";
22

3-
import { type BoxState } 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 BoxProps extends Omit<BoxState, "type"> {
8-
onChange: ComponentChangeHandler;
9-
}
7+
interface BoxState extends ComponentState {}
8+
9+
interface BoxProps extends ComponentProps, BoxState {}
1010

1111
export function Box({ id, style, children, onChange }: BoxProps) {
1212
return (
1313
<MuiBox id={id} style={style}>
14-
<ComponentChildren nodes={children} onChange={onChange} />
14+
<Children nodes={children} onChange={onChange} />
1515
</MuiBox>
1616
);
1717
}

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { type MouseEvent } from "react";
22
import MuiButton from "@mui/material/Button";
33

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

7-
export interface ButtonProps extends Omit<ButtonState, "type"> {
8-
onChange: ComponentChangeHandler;
7+
interface ButtonState extends ComponentState {
8+
text?: string;
99
}
1010

11+
interface ButtonProps extends ComponentProps, ButtonState {}
12+
1113
export function Button({
14+
type,
1215
id,
1316
name,
1417
style,
@@ -19,7 +22,7 @@ export function Button({
1922
const handleClick = (_event: MouseEvent<HTMLButtonElement>) => {
2023
if (id) {
2124
onChange({
22-
componentType: "Button",
25+
componentType: type,
2326
id: id,
2427
property: "clicked",
2528
value: true,

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ import MuiCheckbox from "@mui/material/Checkbox";
33
import MuiFormControl from "@mui/material/FormControl";
44
import MuiFormControlLabel from "@mui/material/FormControlLabel";
55

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

9-
export interface CheckboxProps extends Omit<CheckboxState, "type"> {
10-
onChange: ComponentChangeHandler;
9+
interface CheckboxState extends ComponentState {
10+
label?: string;
11+
value?: boolean | undefined;
1112
}
1213

14+
interface CheckboxProps extends ComponentProps, CheckboxState {}
15+
1316
export function Checkbox({
17+
type,
1418
id,
1519
name,
1620
value,
@@ -22,7 +26,7 @@ export function Checkbox({
2226
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
2327
if (id) {
2428
return onChange({
25-
componentType: "Checkbox",
29+
componentType: type,
2630
id: id,
2731
property: "value",
2832
value: event.currentTarget.checked,

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

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

0 commit comments

Comments
 (0)