diff --git a/chartlets.js/CHANGES.md b/chartlets.js/CHANGES.md index 5095876..d212f44 100644 --- a/chartlets.js/CHANGES.md +++ b/chartlets.js/CHANGES.md @@ -3,12 +3,13 @@ * Add `multiple` property for `Select` component to enable the selection of multiple elements. The `default` mode is supported at the moment. +* Relaxed requirements for adding new components to Chartlets.js via + plugins. We no longer require registered components + to implement `ComponentType`. `ComponentProps`. (#115) + * Static information about callbacks retrieved from API is not cached reducing unnecessary processing and improving performance. (#113) -* Relaxed requirements for adding new components to Chartlets.js via - plugins. Implementing `ComponentProps` is now optional. (#115) - * Callbacks will now only be invoked when there’s an actual change in state, reducing unnecessary processing and improving performance. (#112) diff --git a/chartlets.js/packages/lib/src/actions/configureFramework.test.tsx b/chartlets.js/packages/lib/src/actions/configureFramework.test.tsx index 1dbb6e1..07b0581 100644 --- a/chartlets.js/packages/lib/src/actions/configureFramework.test.tsx +++ b/chartlets.js/packages/lib/src/actions/configureFramework.test.tsx @@ -1,20 +1,19 @@ -import type { ComponentType, FC } from "react"; +import type { FC } from "react"; import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { configureFramework, resolvePlugin } from "./configureFramework"; import { store } from "@/store"; import { registry } from "@/components/registry"; import type { HostStore } from "@/types/state/host"; -import type { Plugin } from "@/types/state/plugin"; -import type { ComponentProps } from "@/components/Component"; +import type { RegistrableComponent, Plugin } from "@/types/state/plugin"; -function getComponents(): [string, ComponentType][] { - interface DivProps extends ComponentProps { +function getComponents(): [string, RegistrableComponent][] { + interface DivProps { text: string; } const Div: FC = ({ text }) =>
{text}
; return [ - ["A", Div as FC], - ["B", Div as FC], + ["A", Div], + ["B", Div], ]; } @@ -61,6 +60,14 @@ describe("configureFramework", () => { }); expect(registry.types.length).toBe(2); }); + + it("should allow adding plain components", () => { + expect(registry.types.length).toBe(0); + configureFramework({ + plugins: [{ components: getComponents() }], + }); + expect(registry.types.length).toBe(2); + }); }); describe("resolvePlugin", () => { diff --git a/chartlets.js/packages/lib/src/components/Children.test.tsx b/chartlets.js/packages/lib/src/components/Children.test.tsx index d494cc1..6ca79ff 100644 --- a/chartlets.js/packages/lib/src/components/Children.test.tsx +++ b/chartlets.js/packages/lib/src/components/Children.test.tsx @@ -2,7 +2,6 @@ import type { FC } from "react"; import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { render, screen } from "@testing-library/react"; import { registry } from "@/components/registry"; -import { type ComponentProps } from "./Component"; import { Children } from "./Children"; describe("Children", () => { @@ -33,11 +32,11 @@ describe("Children", () => { }); it("should render all child types", () => { - interface DivProps extends ComponentProps { + interface DivProps { text: string; } const Div: FC = ({ text }) =>
{text}
; - registry.register("Div", Div as FC); + registry.register("Div", Div); const divProps = { type: "Div", text: "Hello", diff --git a/chartlets.js/packages/lib/src/components/Children.tsx b/chartlets.js/packages/lib/src/components/Children.tsx index d85cc9b..aa81e19 100644 --- a/chartlets.js/packages/lib/src/components/Children.tsx +++ b/chartlets.js/packages/lib/src/components/Children.tsx @@ -1,3 +1,4 @@ +import { type FC } from "react"; import { type ComponentChangeHandler } from "@/types/state/event"; import { type ComponentNode, isComponentState } from "@/types/state/component"; import { Component } from "./Component"; @@ -7,7 +8,10 @@ export interface ChildrenProps { onChange: ComponentChangeHandler; } -export function Children({ nodes, onChange }: ChildrenProps) { +export const Children: FC = ({ + nodes, + onChange, +}: ChildrenProps) => { if (!nodes || nodes.length === 0) { return null; } @@ -27,4 +31,4 @@ export function Children({ nodes, onChange }: ChildrenProps) { })} ); -} +}; diff --git a/chartlets.js/packages/lib/src/components/Component.test.tsx b/chartlets.js/packages/lib/src/components/Component.test.tsx index cbb1d9d..29ea455 100644 --- a/chartlets.js/packages/lib/src/components/Component.test.tsx +++ b/chartlets.js/packages/lib/src/components/Component.test.tsx @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { render, screen } from "@testing-library/react"; -import { Component, type ComponentProps } from "./Component"; +import { Component } from "./Component"; import { registry } from "@/components/registry"; import type { FC } from "react"; @@ -27,11 +27,11 @@ describe("Component", () => { }); it("should render a known component", () => { - interface DivProps extends ComponentProps { + interface DivProps { text: string; } const Div: FC = ({ text }) =>
{text}
; - registry.register("Div", Div as FC); + registry.register("Div", Div); const divProps = { type: "Div", id: "root", diff --git a/chartlets.js/packages/lib/src/components/Component.tsx b/chartlets.js/packages/lib/src/components/Component.tsx index 238e199..caa39f0 100644 --- a/chartlets.js/packages/lib/src/components/Component.tsx +++ b/chartlets.js/packages/lib/src/components/Component.tsx @@ -1,14 +1,10 @@ -import { type ComponentChangeHandler } from "@/types/state/event"; +import { type FC } from "react"; +import { isString } from "@/utils/isString"; import { registry } from "@/components/registry"; +import type { ComponentProps } from "@/types/state/plugin"; -export interface ComponentProps { - type: string; - onChange: ComponentChangeHandler; -} - -export function Component(props: ComponentProps) { - const { type: componentType } = props; - const ActualComponent = registry.lookup(componentType); +export const Component: FC = (props: ComponentProps) => { + const ActualComponent = isString(props.type) && registry.lookup(props.type); if (typeof ActualComponent === "function") { return ; } else { @@ -20,4 +16,4 @@ export function Component(props: ComponentProps) { // ); return null; } -} +}; diff --git a/chartlets.js/packages/lib/src/components/registry.ts b/chartlets.js/packages/lib/src/components/registry.ts index 9cfc4b9..ce163ca 100644 --- a/chartlets.js/packages/lib/src/components/registry.ts +++ b/chartlets.js/packages/lib/src/components/registry.ts @@ -1,6 +1,4 @@ -import type { ComponentType } from "react"; - -import type { ComponentProps } from "@/components/Component"; +import type { RegistrableComponent } from "@/types/state/plugin"; /** * A registry for Chartlets components. @@ -9,8 +7,10 @@ export interface Registry { /** * Register a React component that renders a Chartlets component. * - * `component` must be a functional React component with at - * least the following two component props: + * `component` can be any React component. However, if you want to register + * a custom, reactive component, then `component` must be of type + * `ComponentType` where a `ComponentProps` has at + * least the following two properties: * * - `type: string`: your component's type name. * This will be the same as the `type` used for registration. @@ -23,14 +23,14 @@ export interface Registry { * @param type The Chartlets component's unique type name. * @param component A functional React component. */ - register(type: string, component: ComponentType): () => void; + register(type: string, component: RegistrableComponent): () => void; /** * Lookup the component of the provided type. * * @param type The Chartlets component's type name. */ - lookup(type: string): ComponentType | undefined; + lookup(type: string): RegistrableComponent | undefined; /** * Clears the registry. @@ -46,9 +46,9 @@ export interface Registry { // export for testing only export class RegistryImpl implements Registry { - private components = new Map>(); + private components = new Map(); - register(type: string, component: ComponentType): () => void { + register(type: string, component: RegistrableComponent): () => void { const oldComponent = this.components.get(type); this.components.set(type, component); return () => { @@ -60,7 +60,7 @@ export class RegistryImpl implements Registry { }; } - lookup(type: string): ComponentType | undefined { + lookup(type: string): RegistrableComponent | undefined { return this.components.get(type); } @@ -77,12 +77,5 @@ export class RegistryImpl implements Registry { * The Chartlets component registry. * * Use `registry.register("C", 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/packages/lib/src/index.ts b/chartlets.js/packages/lib/src/index.ts index 6271e50..cd40e3a 100644 --- a/chartlets.js/packages/lib/src/index.ts +++ b/chartlets.js/packages/lib/src/index.ts @@ -14,7 +14,7 @@ export { handleComponentChange } from "@/actions/handleComponentChange"; export { updateContributionContainer } from "@/actions/updateContributionContainer"; // React components -export { Component, type ComponentProps } from "@/components/Component"; +export { Component } from "@/components/Component"; export { Children, type ChildrenProps } from "@/components/Children"; // React hooks @@ -30,5 +30,5 @@ export { // Application interface export type { HostStore, MutableHostStore } from "@/types/state/host"; -export type { Plugin, PluginLike } from "@/types/state/plugin"; +export type { ComponentProps, Plugin, PluginLike } from "@/types/state/plugin"; export type { FrameworkOptions } from "@/types/state/options"; diff --git a/chartlets.js/packages/lib/src/types/state/plugin.ts b/chartlets.js/packages/lib/src/types/state/plugin.ts index 967ab4c..2985afa 100644 --- a/chartlets.js/packages/lib/src/types/state/plugin.ts +++ b/chartlets.js/packages/lib/src/types/state/plugin.ts @@ -1,11 +1,27 @@ import type { ComponentType } from "react"; -import type { ComponentProps } from "@/components/Component"; + +import type { ComponentChangeHandler } from "@/types/state/event"; + +/** + * Properties that custom components must support. + */ +export interface ComponentProps { + type: string; + onChange: ComponentChangeHandler; +} + +/** + * A component type that is eligible for registration. + */ +export type RegistrableComponent = + | ComponentType + | ComponentType; /** * A component registration - a pair comprising the component type name * and the React component. */ -export type ComponentRegistration = [string, ComponentType]; +export type ComponentRegistration = [string, RegistrableComponent]; /** * A framework plugin.