Create Element
Widget View
Learn how to create the widget view and understand how it fits into the system.
Widget View
We'll now implement the function that receives the element's widget and returns the correct component, and for that we'll create the file shown below:
TSX
// src/shared/kits/workflow/mvc/view/widget/widget/element-type/parallel/index.tsx
import { zet } from "@/shared/lib/zet";
import type {
Workflow,
ParallelWidget,
} from "@/shared/kits/workflow/mvc/model/workflow";
import type { OnWorkflowChange } from "@/shared/kits/workflow/mvc/model/on-workflow-change";
import { ParallelCreateBranchView } from "./create-branch";
import { ParallelBranchInfoView } from "./branch-info";
import { ParallelAddBranchView } from "./add-branch";
interface Zet {
object: ParallelWidget;
nested: [];
filter: ["widget"];
params: [Workflow, OnWorkflowChange];
return: React.ReactNode;
}
const dispatch = zet<Zet>([], ["widget"], {
createBranch: (widget, workflow, onWorkflowChange) => (
<ParallelCreateBranchView
key={workflow.active}
widget={widget}
workflow={workflow}
onWorkflowChange={onWorkflowChange}
/>
),
branchInfo: (widget, workflow, onWorkflowChange) => (
<ParallelBranchInfoView
key={workflow.active}
widget={widget}
workflow={workflow}
onWorkflowChange={onWorkflowChange}
/>
),
addBranch: (widget, workflow, onWorkflowChange) => (
<ParallelAddBranchView
key={workflow.active}
widget={widget}
workflow={workflow}
onWorkflowChange={onWorkflowChange}
/>
),
});
export function parallelView(
widget: ParallelWidget,
workflow: Workflow,
onWorkflowChange: OnWorkflowChange,
): React.ReactNode {
return dispatch(widget, workflow, onWorkflowChange);
}
We'll also need to create the following components that are imported.
The ParallelCreateBranchView component:
TSX
// src/shared/kits/workflow/mvc/view/widget/widget/element-type/parallel/create-branch.tsx
import { useCallback } from "react";
import { Formity, type s, type Flow, type ReturnOutput } from "@formity/react";
import type {
Workflow,
ParallelCreateBranchWidget,
} from "@/shared/kits/workflow/mvc/model/workflow";
import type { OnWorkflowChange } from "@/shared/kits/workflow/mvc/model/on-workflow-change";
import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";
import { Button } from "@/shared/ui/button";
import { Sidebar } from "@/shared/ui/sidebar";
import { Editor, LongText } from "@/shared/kits/editor";
type Schema = {
render: React.ReactNode;
struct: [s.Form<{ label: string }>, s.Return<{ label: string }>];
inputs: {
label: string;
};
params: Record<never, never>;
};
const flow: Flow<Schema> = [
{
form: {
fields: ({ label }) => ({
label: [label, []],
}),
render: ({ fields, next }) => (
<Editor
defaultValues={fields}
resolver={zodResolver(
z.object({
label: z.string(),
}),
)}
onSubmit={next}
>
<LongText name="label">
<LongText.Label>Label</LongText.Label>
<LongText.Input maxLength={160} placeholder="Enter the label" />
</LongText>
<Button type="submit">Save</Button>
</Editor>
),
},
},
{
return: ({ label }) => ({
label: label,
}),
},
];
interface ParallelCreateBranchViewProps {
widget: ParallelCreateBranchWidget;
workflow: Workflow;
onWorkflowChange: OnWorkflowChange;
}
export function ParallelCreateBranchView({
widget,
onWorkflowChange,
}: ParallelCreateBranchViewProps): React.ReactNode {
const onSubmit = useCallback(
(output: ReturnOutput<Schema>) => {
onWorkflowChange([
{
type: "elementType",
elementType: "parallel",
change: "createBranch",
id: widget.element.id,
label: output.label,
},
]);
},
[widget.element.id, onWorkflowChange],
);
return (
<Sidebar className="absolute inset-y-0 right-0 overflow-auto border-l border-l-gray-200">
<Sidebar.Top>
<Sidebar.Heading>Create branch</Sidebar.Heading>
<Sidebar.Close
onClick={() => {
onWorkflowChange([{ type: "global", change: "removeWidget" }]);
}}
/>
</Sidebar.Top>
<Sidebar.Content>
<Formity<Schema>
flow={flow}
inputs={{
label: "",
}}
onReturn={onSubmit}
/>
</Sidebar.Content>
<Sidebar.Divider />
</Sidebar>
);
}
The ParallelBranchInfoView component:
TSX
// src/shared/kits/workflow/mvc/view/widget/widget/element-type/parallel/branch-info.tsx
import { useCallback } from "react";
import { Formity, type s, type Flow, type ReturnOutput } from "@formity/react";
import type {
Workflow,
ParallelBranchInfoWidget,
} from "@/shared/kits/workflow/mvc/model/workflow";
import type { OnWorkflowChange } from "@/shared/kits/workflow/mvc/model/on-workflow-change";
import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";
import { Button } from "@/shared/ui/button";
import { Sidebar } from "@/shared/ui/sidebar";
import { Editor, LongText } from "@/shared/kits/editor";
type Schema = {
render: React.ReactNode;
struct: [s.Form<{ label: string }>, s.Return<{ label: string }>];
inputs: {
label: string;
};
params: Record<never, never>;
};
const flow: Flow<Schema> = [
{
form: {
fields: ({ label }) => ({
label: [label, []],
}),
render: ({ fields, next }) => (
<Editor
defaultValues={fields}
resolver={zodResolver(
z.object({
label: z.string(),
}),
)}
onSubmit={next}
>
<LongText name="label">
<LongText.Label>Label</LongText.Label>
<LongText.Input maxLength={160} placeholder="Enter the label" />
</LongText>
<Button type="submit">Save</Button>
</Editor>
),
},
},
{
return: ({ label }) => ({
label: label,
}),
},
];
interface ParallelBranchInfoViewProps {
widget: ParallelBranchInfoWidget;
workflow: Workflow;
onWorkflowChange: OnWorkflowChange;
}
export function ParallelBranchInfoView({
widget,
onWorkflowChange,
}: ParallelBranchInfoViewProps): React.ReactNode {
const onSubmit = useCallback(
(output: ReturnOutput<Schema>) => {
onWorkflowChange([
{
type: "elementType",
elementType: "parallel",
change: "branchInfo",
id: widget.element.id,
branch: widget.branch,
label: output.label,
},
]);
},
[widget.element.id, widget.branch, onWorkflowChange],
);
return (
<Sidebar className="absolute inset-y-0 right-0 overflow-auto border-l border-l-gray-200">
<Sidebar.Top>
<Sidebar.Heading>Branch</Sidebar.Heading>
<Sidebar.Close
onClick={() => {
onWorkflowChange([{ type: "global", change: "removeWidget" }]);
}}
/>
</Sidebar.Top>
<Sidebar.Content>
<Formity<Schema>
flow={flow}
inputs={{
label: widget.element.branches[widget.branch]!.label,
}}
onReturn={onSubmit}
/>
</Sidebar.Content>
<Sidebar.Divider />
</Sidebar>
);
}
The ParallelAddBranchView component:
TSX
// src/shared/kits/workflow/mvc/view/widget/widget/element-type/parallel/add-branch.tsx
import type {
Workflow,
ParallelAddBranchWidget,
} from "@/shared/kits/workflow/mvc/model/workflow";
import type { OnWorkflowChange } from "@/shared/kits/workflow/mvc/model/on-workflow-change";
import { AddElement } from "@/shared/kits/workflow/mvc/view/widget/widget-components/add-element";
interface ParallelAddBranchViewProps {
widget: ParallelAddBranchWidget;
workflow: Workflow;
onWorkflowChange: OnWorkflowChange;
}
export function ParallelAddBranchView({
widget,
onWorkflowChange,
}: ParallelAddBranchViewProps): React.ReactNode {
return (
<AddElement
onClose={() => {
onWorkflowChange([{ type: "global", change: "removeWidget" }]);
}}
onAdd={(element) => {
onWorkflowChange([
{
type: "elementType",
elementType: "parallel",
change: "addBranch",
id: widget.element.id,
branch: widget.branch,
element,
},
]);
}}
/>
);
}
Finally, we'll update the following file to include the new function we created:
TSX
// src/shared/kits/workflow/mvc/view/widget/widget/element-type/index.tsx
import { zet } from "@/shared/lib/zet";
import type {
Workflow,
ElementTypeWidget,
} from "@/shared/kits/workflow/mvc/model/workflow";
import type { OnWorkflowChange } from "@/shared/kits/workflow/mvc/model/on-workflow-change";
import { actionView } from "./action";
import { conditionView } from "./condition";
import { switchView } from "./switch";
import { loopView } from "./loop";
import { parallelView } from "./parallel";
interface Zet {
object: ElementTypeWidget;
nested: [];
filter: ["elementType"];
params: [Workflow, OnWorkflowChange];
return: React.ReactNode;
}
const dispatch = zet<Zet>([], ["elementType"], {
action: actionView,
condition: conditionView,
switch: switchView,
loop: loopView,
parallel: parallelView,
});
export function elementTypeView(
widget: ElementTypeWidget,
workflow: Workflow,
onWorkflowChange: OnWorkflowChange,
): React.ReactNode {
return dispatch(widget, workflow, onWorkflowChange);
}
Add Element Widget
We also need to add the new element to the component responsible for adding elements to the workflow, as shown in the following file:
TSX
// src/shared/kits/workflow/mvc/view/widget/widget-components/add-element.tsx
import { Sidebar } from "@/shared/ui/sidebar";
import { Button } from "@/shared/ui/button";
import { ActionIcon } from "@/shared/kits/workflow/icons/action";
import { ConditionIcon } from "@/shared/kits/workflow/icons/condition";
import { SwitchIcon } from "@/shared/kits/workflow/icons/switch";
import { LoopIcon } from "@/shared/kits/workflow/icons/loop";
import { ParallelIcon } from "@/shared/kits/workflow/icons/parallel";
import type { ElementFlow } from "@/shared/kits/workflow/mvc/model/workflow";
interface AddElementProps {
onClose: () => void;
onAdd: (element: ElementFlow) => void;
}
export function AddElement({
onClose,
onAdd,
}: AddElementProps): React.ReactNode {
return (
<Sidebar className="absolute inset-y-0 right-0 overflow-auto border-l border-l-gray-200">
<Sidebar.Top>
<Sidebar.Heading>Add element</Sidebar.Heading>
<Sidebar.Close onClick={onClose} />
</Sidebar.Top>
<Sidebar.Content>
<Button
variant="light"
className="w-full justify-start gap-2 px-3 [&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:stroke-gray-500"
onClick={() =>
onAdd({
type: "action",
id: crypto.randomUUID(),
message: "",
dragging: null,
dropNext: new Set(),
})
}
>
<ActionIcon />
Action
</Button>
<Button
variant="light"
className="w-full justify-start gap-2 px-3 [&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:stroke-gray-500"
onClick={() =>
onAdd({
type: "condition",
id: crypto.randomUUID(),
if: "",
then: [],
else: [],
dragging: null,
dropNext: new Set(),
dropThen: new Set(),
dropElse: new Set(),
})
}
>
<ConditionIcon />
Condition
</Button>
<Button
variant="light"
className="w-full justify-start gap-2 px-3 [&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:stroke-gray-500"
onClick={() =>
onAdd({
type: "switch",
id: crypto.randomUUID(),
branches: [],
default: [],
dragging: null,
dropNext: new Set(),
dropBranches: [],
dropDefault: new Set(),
})
}
>
<SwitchIcon />
Switch
</Button>
<Button
variant="light"
className="w-full justify-start gap-2 px-3 [&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:stroke-gray-500"
onClick={() =>
onAdd({
type: "loop",
id: crypto.randomUUID(),
while: "",
into: [],
dragging: null,
dropNext: new Set(),
dropInto: new Set(),
})
}
>
<LoopIcon />
Loop
</Button>
<Button
variant="light"
className="w-full justify-start gap-2 px-3 [&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:stroke-gray-500"
onClick={() =>
onAdd({
type: "parallel",
id: crypto.randomUUID(),
branches: [],
dragging: null,
dropNext: new Set(),
dropBranches: [],
})
}
>
<ParallelIcon />
Parallel
</Button>
</Sidebar.Content>
</Sidebar>
);
}