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
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