Skip to content

Commit

Permalink
Merge pull request #45 from bcdev/forman-x-reorganising_components
Browse files Browse the repository at this point in the history
Reorganising and enhancing components
  • Loading branch information
forman authored Nov 22, 2024
2 parents f3a299a + f9b1f5b commit 481f12b
Show file tree
Hide file tree
Showing 16 changed files with 267 additions and 137 deletions.
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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"],
Expand Down
5 changes: 2 additions & 3 deletions chartlets.js/src/lib/actions/helpers/getInputValues.test.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
20 changes: 20 additions & 0 deletions chartlets.js/src/lib/component/Component.tsx
Original file line number Diff line number Diff line change
@@ -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 <ActualComponent {...props} />;
} else {
console.error(
`chartlets: invalid component type encountered: ${componentType}`,
);
return null;
}
}
53 changes: 53 additions & 0 deletions chartlets.js/src/lib/component/Registry.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
68 changes: 68 additions & 0 deletions chartlets.js/src/lib/component/Registry.ts
Original file line number Diff line number Diff line change
@@ -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<ComponentProps>, type?: string): () => void;

/**
* Lookup the component of the provided type.
*
* @param type The Chartlets component's type name.
*/
lookup(type: string): FC<ComponentProps> | undefined;

/**
* Get the type names of all registered components.
*/
types: string[];
}

// export for testing only
export class RegistryImpl implements Registry {
private components = new Map<string, FC<ComponentProps>>();

register(component: FC<ComponentProps>, 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<ComponentProps> | 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();
14 changes: 7 additions & 7 deletions chartlets.js/src/lib/components/Box.tsx
Original file line number Diff line number Diff line change
@@ -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<BoxState, "type"> {
onChange: ComponentChangeHandler;
}
interface BoxState extends ComponentState {}

interface BoxProps extends ComponentProps, BoxState {}

export function Box({ id, style, children, onChange }: BoxProps) {
return (
<MuiBox id={id} style={style}>
<ComponentChildren nodes={children} onChange={onChange} />
<Children nodes={children} onChange={onChange} />
</MuiBox>
);
}
13 changes: 8 additions & 5 deletions chartlets.js/src/lib/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -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<ButtonState, "type"> {
onChange: ComponentChangeHandler;
interface ButtonState extends ComponentState {
text?: string;
}

interface ButtonProps extends ComponentProps, ButtonState {}

export function Button({
type,
id,
name,
style,
Expand All @@ -19,7 +22,7 @@ export function Button({
const handleClick = (_event: MouseEvent<HTMLButtonElement>) => {
if (id) {
onChange({
componentType: "Button",
componentType: type,
id: id,
property: "clicked",
value: true,
Expand Down
14 changes: 9 additions & 5 deletions chartlets.js/src/lib/components/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<CheckboxState, "type"> {
onChange: ComponentChangeHandler;
interface CheckboxState extends ComponentState {
label?: string;
value?: boolean | undefined;
}

interface CheckboxProps extends ComponentProps, CheckboxState {}

export function Checkbox({
type,
id,
name,
value,
Expand All @@ -22,7 +26,7 @@ export function Checkbox({
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
if (id) {
return onChange({
componentType: "Checkbox",
componentType: type,
id: id,
property: "value",
value: event.currentTarget.checked,
Expand Down
34 changes: 0 additions & 34 deletions chartlets.js/src/lib/components/Component.tsx

This file was deleted.

21 changes: 14 additions & 7 deletions chartlets.js/src/lib/components/Plot.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
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<PlotState, "type"> {
onChange: ComponentChangeHandler;
interface PlotState extends ComponentState {
chart?:
| (VisualizationSpec & {
datasets?: Record<string, unknown>; // 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 <div id={id} style={style} />;
}
const { datasets, ...spec } = chart;
const handleSignal = (_signalName: string, value: unknown) => {
if (id) {
return onChange({
componentType: "Plot",
componentType: type,
id: id,
property: "points",
value: value,
Expand Down
Loading

0 comments on commit 481f12b

Please sign in to comment.