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
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,50 @@ export function App() {
}
```

### Enum Change Callbacks

You can subscribe to enum value changes in the visual editor via `onAddEnum` and `onDeleteEnum`.

Both callbacks receive a single context object:

```ts
type EnumChangeContext = {
value: string | number | boolean;
index: number;
schemaKey?: string;
};
```

- `value`: enum value that was added/removed
- `index`: index in the enum list at the time of the change
- `schemaKey`: path-like key of the edited field (for example: `person.firstName`, `hobbies[].name`)

Example:

```tsx
import "jsonjoy-builder/styles.css";
import { type JSONSchema, JsonSchemaEditor } from "jsonjoy-builder";
import { useState } from "react";

export function App() {
const [schema, setSchema] = useState<JSONSchema>({ type: "object" });

return (
<JsonSchemaEditor
schema={schema}
readOnly={false}
setSchema={setSchema}
onAddEnum={({ value, index, schemaKey }) => {
console.log("enum:add", { value, index, schemaKey });
}}
onDeleteEnum={({ value, index, schemaKey }) => {
console.log("enum:delete", { value, index, schemaKey });
}}
/>
);
}
```

### Styling

To style the component, add custom CSS. For basic styling, there are some CSS custom properties ("variables")
Expand Down
9 changes: 9 additions & 0 deletions src/components/SchemaEditor/JsonSchemaEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ import { cn } from "../../lib/utils.ts";
import type { JSONSchema } from "../../types/jsonSchema.ts";
import JsonSchemaVisualizer from "./JsonSchemaVisualizer.tsx";
import SchemaVisualEditor from "./SchemaVisualEditor.tsx";
import type { EnumChangeContext } from "./TypeEditor.tsx";

/** @public */
export interface JsonSchemaEditorProps {
schema?: JSONSchema;
readOnly: boolean;
setSchema?: (schema: JSONSchema) => void;
onAddEnum?: (ctx: EnumChangeContext) => void;
onDeleteEnum?: (ctx: EnumChangeContext) => void;
className?: string;
}

Expand All @@ -30,6 +33,8 @@ const JsonSchemaEditor: FC<JsonSchemaEditorProps> = ({
schema = { type: "object" },
readOnly = false,
setSchema,
onAddEnum,
onDeleteEnum,
className,
}) => {
// Handle schema changes and propagate to parent if needed
Expand Down Expand Up @@ -124,6 +129,8 @@ const JsonSchemaEditor: FC<JsonSchemaEditorProps> = ({
readOnly={readOnly}
schema={schema}
onChange={handleSchemaChange}
onAddEnum={onAddEnum}
onDeleteEnum={onDeleteEnum}
/>
</TabsContent>

Expand Down Expand Up @@ -170,6 +177,8 @@ const JsonSchemaEditor: FC<JsonSchemaEditorProps> = ({
readOnly={readOnly}
schema={schema}
onChange={handleSchemaChange}
onAddEnum={onAddEnum}
onDeleteEnum={onDeleteEnum}
/>
</div>
{/** biome-ignore lint/a11y/noStaticElementInteractions: What exactly does this div do? */}
Expand Down
8 changes: 8 additions & 0 deletions src/components/SchemaEditor/SchemaFieldList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ import {
} from "../../types/jsonSchema.ts";
import { buildValidationTree } from "../../types/validation.ts";
import SchemaPropertyEditor from "./SchemaPropertyEditor.tsx";
import type { EnumChangeContext } from "./TypeEditor.tsx";

interface SchemaFieldListProps {
schema: JSONSchemaType;
readOnly: boolean;
onAddEnum?: (ctx: EnumChangeContext) => void;
onDeleteEnum?: (ctx: EnumChangeContext) => void;
onAddField: (newField: NewField) => void;
onEditField: (name: string, updatedField: NewField) => void;
onDeleteField: (name: string) => void;
Expand All @@ -27,6 +30,8 @@ const SchemaFieldList: FC<SchemaFieldListProps> = ({
schema,
onEditField,
onDeleteField,
onAddEnum,
onDeleteEnum,
readOnly = false,
}) => {
const t = useTranslation();
Expand Down Expand Up @@ -143,9 +148,12 @@ const SchemaFieldList: FC<SchemaFieldListProps> = ({
<SchemaPropertyEditor
key={property.name}
name={property.name}
schemaKey={property.name}
schema={property.schema}
required={property.required}
validationNode={validationTree.children[property.name] ?? undefined}
onAddEnum={onAddEnum}
onDeleteEnum={onDeleteEnum}
onDelete={() => onDeleteField(property.name)}
onNameChange={(newName) => handleNameChange(property.name, newName)}
onRequiredChange={(required) =>
Expand Down
10 changes: 10 additions & 0 deletions src/components/SchemaEditor/SchemaPropertyEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ import type { ValidationTreeNode } from "../../types/validation.ts";
import { Badge } from "../ui/badge.tsx";
import { ButtonToggle } from "../ui/button-toggle.tsx";
import TypeDropdown from "./TypeDropdown.tsx";
import type { EnumChangeContext } from "./TypeEditor.tsx";
import TypeEditor from "./TypeEditor.tsx";
export interface SchemaPropertyEditorProps {
name: string;
schema: JSONSchema;
schemaKey?: string;
required: boolean;
readOnly: boolean;
validationNode?: ValidationTreeNode;
onAddEnum?: (ctx: EnumChangeContext) => void;
onDeleteEnum?: (ctx: EnumChangeContext) => void;
onDelete: () => void;
onNameChange: (newName: string) => void;
onRequiredChange: (required: boolean) => void;
Expand All @@ -34,9 +38,12 @@ export interface SchemaPropertyEditorProps {
export const SchemaPropertyEditor: React.FC<SchemaPropertyEditorProps> = ({
name,
schema,
schemaKey,
required,
readOnly = false,
validationNode,
onAddEnum,
onDeleteEnum,
onDelete,
onNameChange,
onRequiredChange,
Expand Down Expand Up @@ -254,6 +261,9 @@ export const SchemaPropertyEditor: React.FC<SchemaPropertyEditorProps> = ({
readOnly={readOnly}
validationNode={validationNode}
onChange={handleSchemaUpdate}
schemaKey={schemaKey ?? name}
onAddEnum={onAddEnum}
onDeleteEnum={onDeleteEnum}
depth={depth + 1}
/>
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/components/SchemaEditor/SchemaVisualEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,23 @@ import type { JSONSchema, NewField } from "../../types/jsonSchema.ts";
import { asObjectSchema, isBooleanSchema } from "../../types/jsonSchema.ts";
import AddFieldButton from "./AddFieldButton.tsx";
import SchemaFieldList from "./SchemaFieldList.tsx";
import type { EnumChangeContext } from "./TypeEditor.tsx";

/** @public */
export interface SchemaVisualEditorProps {
schema: JSONSchema;
readOnly: boolean;
onChange: (schema: JSONSchema) => void;
onAddEnum?: (ctx: EnumChangeContext) => void;
onDeleteEnum?: (ctx: EnumChangeContext) => void;
}

/** @public */
const SchemaVisualEditor: FC<SchemaVisualEditorProps> = ({
schema,
onChange,
onAddEnum,
onDeleteEnum,
readOnly = false,
}) => {
const t = useTranslation();
Expand Down Expand Up @@ -125,6 +130,8 @@ const SchemaVisualEditor: FC<SchemaVisualEditorProps> = ({
<SchemaFieldList
schema={schema}
readOnly={readOnly}
onAddEnum={onAddEnum}
onDeleteEnum={onDeleteEnum}
onAddField={handleAddField}
onEditField={handleEditField}
onDeleteField={handleDeleteField}
Expand Down
33 changes: 33 additions & 0 deletions src/components/SchemaEditor/TypeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,30 @@ const ObjectEditor = lazy(() => import("./types/ObjectEditor.tsx"));
const ArrayEditor = lazy(() => import("./types/ArrayEditor.tsx"));
const CombinatorEditor = lazy(() => import("./types/CombinatorEditor.tsx"));

export interface EnumChangeContext {
value: string | number | boolean;
index: number;
schemaKey?: string;
}

export interface TypeEditorProps {
schema: JSONSchema;
readOnly: boolean;
validationNode: ValidationTreeNode | undefined;
onChange: (schema: ObjectJSONSchema) => void;
schemaKey?: string;
onAddEnum?: (ctx: EnumChangeContext) => void;
onDeleteEnum?: (ctx: EnumChangeContext) => void;
depth?: number;
}

const TypeEditor: React.FC<TypeEditorProps> = ({
schema,
validationNode,
onChange,
schemaKey,
onAddEnum,
onDeleteEnum,
depth = 0,
readOnly = false,
}) => {
Expand All @@ -37,6 +49,9 @@ const TypeEditor: React.FC<TypeEditorProps> = ({
readOnly={readOnly}
schema={schema}
onChange={onChange}
schemaKey={schemaKey}
onAddEnum={onAddEnum}
onDeleteEnum={onDeleteEnum}
depth={depth}
validationNode={validationNode}
/>
Expand All @@ -46,6 +61,9 @@ const TypeEditor: React.FC<TypeEditorProps> = ({
readOnly={readOnly}
schema={schema}
onChange={onChange}
schemaKey={schemaKey}
onAddEnum={onAddEnum}
onDeleteEnum={onDeleteEnum}
depth={depth}
validationNode={validationNode}
/>
Expand All @@ -55,6 +73,9 @@ const TypeEditor: React.FC<TypeEditorProps> = ({
readOnly={readOnly}
schema={schema}
onChange={onChange}
schemaKey={schemaKey}
onAddEnum={onAddEnum}
onDeleteEnum={onDeleteEnum}
depth={depth}
validationNode={validationNode}
integer
Expand All @@ -65,6 +86,9 @@ const TypeEditor: React.FC<TypeEditorProps> = ({
readOnly={readOnly}
schema={schema}
onChange={onChange}
schemaKey={schemaKey}
onAddEnum={onAddEnum}
onDeleteEnum={onDeleteEnum}
depth={depth}
validationNode={validationNode}
/>
Expand All @@ -74,6 +98,9 @@ const TypeEditor: React.FC<TypeEditorProps> = ({
readOnly={readOnly}
schema={schema}
onChange={onChange}
schemaKey={schemaKey}
onAddEnum={onAddEnum}
onDeleteEnum={onDeleteEnum}
depth={depth}
validationNode={validationNode}
/>
Expand All @@ -83,6 +110,9 @@ const TypeEditor: React.FC<TypeEditorProps> = ({
readOnly={readOnly}
schema={schema}
onChange={onChange}
schemaKey={schemaKey}
onAddEnum={onAddEnum}
onDeleteEnum={onDeleteEnum}
depth={depth}
validationNode={validationNode}
/>
Expand All @@ -92,6 +122,9 @@ const TypeEditor: React.FC<TypeEditorProps> = ({
readOnly={readOnly}
schema={schema}
onChange={onChange}
schemaKey={schemaKey}
onAddEnum={onAddEnum}
onDeleteEnum={onDeleteEnum}
depth={depth}
validationNode={validationNode}
combinator={type}
Expand Down
7 changes: 7 additions & 0 deletions src/components/SchemaEditor/types/ArrayEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const ArrayEditor: React.FC<TypeEditorProps> = ({
readOnly = false,
validationNode,
onChange,
schemaKey,
onAddEnum,
onDeleteEnum,
depth = 0,
}) => {
const t = useTranslation();
Expand All @@ -43,6 +46,7 @@ const ArrayEditor: React.FC<TypeEditorProps> = ({

// Get the array's item schema
const itemsSchema = getArrayItemsSchema(schema) || { type: "string" };
const itemSchemaKey = schemaKey ? `${schemaKey}[]` : undefined;

// Get the type of the array items
const itemType = withObjectSchema(
Expand Down Expand Up @@ -276,6 +280,9 @@ const ArrayEditor: React.FC<TypeEditorProps> = ({
schema={itemsSchema}
validationNode={validationNode}
onChange={handleItemSchemaChange}
schemaKey={itemSchemaKey}
onAddEnum={onAddEnum}
onDeleteEnum={onDeleteEnum}
depth={depth + 1}
/>
</div>
Expand Down
Loading
Loading