Skip to content

Commit 492bd58

Browse files
committed
feat(form): improve collection fields
Closes #19
1 parent 2ac547c commit 492bd58

25 files changed

+429
-194
lines changed

README.md

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
[![Try our demo](https://img.shields.io/badge/try_our-🕹️_demo_🕹️-deepskyblue.svg?style=for-the-badge)](https://cern-sis.github.io/react-formule/)
66

7-
</div>
8-
97
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
108
[![NPM Version](https://img.shields.io/npm/v/react-formule?style=flat-square&color=orchid)](https://www.npmjs.com/package/react-formule?activeTab=readme)
119
[![GitHub commits since tagged version](https://img.shields.io/github/commits-since/cern-sis/react-formule/latest?style=flat-square&color=orange)](https://github.com/cern-sis/react-formule/commits/master/)
@@ -15,6 +13,8 @@
1513
[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/cern-sis/react-formule/cypress.yml?style=flat-square&label=cypress)](https://github.com/cern-sis/react-formule/actions/workflows/cypress.yml)
1614
[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/cern-sis/react-formule/deploy-demo.yml?style=flat-square&label=deploy-demo)](https://github.com/cern-sis/react-formule/actions/workflows/commit-lint.yml)
1715

16+
</div>
17+
1818
## :horse: What is Formule?
1919

2020
Formule is a **powerful, user-friendly, extensible and mobile-friendly form building library** based on [JSON Schema](https://json-schema.org/) and [RJSF](https://github.com/rjsf-team/react-jsonschema-form), which aims to make form creation easier for both technical and non-technical people.
@@ -70,8 +70,8 @@ Formule includes a variety of predefined field types, grouped in three categorie
7070
- **Collections**:
7171
- `Object`: Use it of you want to group fields or to add several of them inside of a `List`.
7272
- `List`: It allows you to have as many instances of a field or `Object` as you want.
73-
- `Accordion`: When containing a `List`, it works as a `List` with collapsible entries.
74-
- `Layer`: When containing a `List`, it works as a `List` whose entries will open in a dialog window.
73+
- `Accordion`: It works as a `List` with collapsible entries.
74+
- `Layer`: It works as a `List` whose entries will open in a dialog window.
7575
- `Tab`: It's commonly supposed to be used as a wrapper around the rest of the elements. You will normally want to add an `Object` inside and you can use it to separate the form in different pages or sections.
7676
- **Advanced fields**: More complex or situational fields such as `URI`, `Rich/Latex editor`, `Tags`, `ID Fetcher`, `Code Editor` and `Files`.
7777

@@ -110,27 +110,39 @@ return (
110110

111111
### Customizing and adding new field types
112112

113-
Override (if existing) or create your own field types (rjsf type definitions) similarly to how it's done in `fieldTypes.jsx`, passing them as `customFieldTypes`. Implement your own custom fields and widgets (react components) by passing them as `customFields` and/or `customWidgets` (see `forms/fields/` and `forms/widgets/` for examples). If you also want to use a different published version of a field or widget, pass the component in `customPublishedFields` or `customPublishedWidgets`.
113+
Override (if existing) or create your own field types (rjsf type definitions) similarly to how it's done in `fieldTypes.jsx`, passing them as `customFieldTypes`. Implement your own custom fields and widgets (react components) by passing them as `customFields` and/or `customWidgets` (see `forms/fields/` and `forms/widgets/` for examples). If you also want to use a different published version of a field or widget, pass the component in `customPublishedFields` or `customPublishedWidgets`. You can read more about the difference between fields and widgets and how to customize or wrap them in the [rjsf docs](https://rjsf-team.github.io/react-jsonschema-form/docs/advanced-customization/custom-widgets-fields), but make sure you provide Formule with something like the following:
114114

115115
```jsx
116+
const CustomWidget = ({value, required, onChange}) => {
117+
return (
118+
<input
119+
type='text'
120+
className='custom'
121+
value={value}
122+
required={required}
123+
onChange={(event) => onChange(event.target.value)}
124+
/>
125+
);
126+
};
127+
116128
const customFieldTypes = {
117129
advanced: {
118-
myfield: {
130+
myCustomWidget: {
119131
title: ...
120132
...
121133
}
122134
}
123135
}
124136

125-
const customFields: {
126-
myfield: MyField // react component
137+
const customWidgets: {
138+
myCustomWidget: CustomWidget
127139
}
128140

129141
<FormuleContext
130142
theme={{token: {colorPrimary: "blue"}}} // antd theme
131143
customFieldTypes={customFieldTypes}
132-
customFields={customFields}
133-
customWidgets={...}
144+
customFields={...}
145+
customWidgets={customWidgets}
134146
customPublishedFields={...}
135147
customPublishedWidgets={...}>
136148
// ...

formule-demo/cypress/e2e/builder.cy.ts

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,9 @@ describe("test basic functionality", () => {
312312
.should("exist");
313313
cy.get(`fieldset#root${SEP}enum`).getByDataCy("addItemButton").click();
314314
cy.get(`input#root${SEP}enum${SEP}0`).clearTypeBlur("First option");
315-
cy.get(`fieldset#root${SEP}enum`).getByDataCy("addItemButton").click();
315+
cy.get(`fieldset#root${SEP}enum`)
316+
.getByDataCy("addItemButton")
317+
.click({ force: true });
316318
cy.get(`input#root${SEP}enum${SEP}1`).clearTypeBlur("Second option");
317319
cy.get(`#root${SEP}enum .arrayFieldRow`)
318320
.eq(0)
@@ -335,7 +337,9 @@ describe("test basic functionality", () => {
335337
.parent()
336338
.find('[title="Select one value (number)"]')
337339
.should("exist");
338-
cy.get(`fieldset#root${SEP}enum`).getByDataCy("addItemButton").click();
340+
cy.get(`fieldset#root${SEP}enum`)
341+
.getByDataCy("addItemButton")
342+
.click({ force: true });
339343
cy.get(`input#root${SEP}enum${SEP}0`).clearTypeBlur("asd");
340344
cy.get(`input#root${SEP}enum${SEP}0`).should("have.value", "");
341345
cy.get(`input#root${SEP}enum${SEP}0`).clearTypeBlur("1");
@@ -551,7 +555,7 @@ describe("test basic functionality", () => {
551555
cy.getByDataCy("treeItem").contains("myarray").as("arrayField");
552556
cy.addField("text", "@arrayField");
553557

554-
// test basic add, move, delete functionality
558+
// Test basic add, move, delete functionality
555559
cy.getByDataCy("formPreview")
556560
.find(`fieldset#root${SEP}myarray`)
557561
.getByDataCy("addItemButton")
@@ -576,29 +580,76 @@ describe("test basic functionality", () => {
576580
cy.getByDataCy("formPreview")
577581
.find(`input#root${SEP}myarray${SEP}1`)
578582
.should("not.exist");
583+
584+
// Test multiple fields inside
585+
// check that with one item the array contains the item as direct child
586+
cy.getByDataCy("treeItem")
587+
.contains("items")
588+
.parents("[data-cy=treeItem]")
589+
.find(".anticon-font-size");
590+
// check that with two items the array now contains an object containing the items
591+
cy.addField("text", "@arrayField");
592+
cy.getByDataCy("treeItem")
593+
.contains("items")
594+
.parents("[data-cy=treeItem]")
595+
.contains("{ }");
596+
cy.getByDataCy("treeItem")
597+
.find(".anticon-font-size")
598+
.should("have.length", 2);
599+
// remove one item and check that the object is removed and the remaining item is a direct child of the array
600+
cy.getByDataCy("treeItem").find(".anticon-font-size").first().click();
601+
cy.getByDataCy("deleteField").click();
602+
cy.get("div.ant-popconfirm").find("button").contains("Delete").click();
603+
cy.getByDataCy("treeItem")
604+
.contains("items")
605+
.parents("[data-cy=treeItem]")
606+
.find(".anticon-font-size")
607+
.should("have.length", 1);
608+
cy.getByDataCy("treeItem")
609+
.contains("items")
610+
.parents("[data-cy=treeItem]")
611+
.contains("{ }")
612+
.should("not.exist");
579613
});
580614

581-
it.skip("tests accordion field", () => {
582-
// cy.addFieldWithName("array", "myarray")
583-
// cy.getByDataCy("treeItem").contains("myarray").as("arrayField")
584-
// cy.addField("accordionObjectField", "@arrayField")
585-
// cy.getByDataCy("treeItem").contains("items").as("accordionItems")
586-
// cy.addField("text", "@accordionItems")
587-
// cy.getByDataCy("formPreview").find(`fieldset#root${SEP}myarray`).getByDataCy("addItemButton").as("addItem").click()
588-
// cy.get("@addItem").click()
589-
// cy.getByDataCy("formPreview").find(".ant-collapse-item").first().click()
590-
// FIXME: To be properly tested once the accordion field is reviewed and its function is more clearly defined
615+
it("tests accordion field", () => {
616+
cy.addFieldWithName("accordion", "myaccordion");
617+
cy.getByDataCy("treeItem").contains("myaccordion").as("accordionField");
618+
cy.addField("text", "@accordionField");
619+
620+
cy.getByDataCy("formPreview")
621+
.find(`fieldset#root${SEP}myaccordion`)
622+
.getByDataCy("addItemButton")
623+
.as("addItem")
624+
.click();
625+
cy.get("@addItem").click();
626+
cy.getByDataCy("formPreview")
627+
.find(".ant-collapse-item")
628+
.as("accordionItems")
629+
.first()
630+
.click();
631+
cy.getByDataCy("formPreview")
632+
.find(`input#root${SEP}myaccordion${SEP}0`)
633+
.clearTypeBlur("First item");
634+
cy.get("@accordionItems").last().click();
635+
cy.getByDataCy("formPreview")
636+
.find(`.ant-collapse-content-hidden`)
637+
.should("have.length", 1);
638+
cy.getByDataCy("formPreview")
639+
.find(`input#root${SEP}myaccordion${SEP}1`)
640+
.clearTypeBlur("Second item");
641+
cy.getByDataCy("formPreview")
642+
.find(`.ant-collapse-content-hidden`)
643+
.should("have.length", 1);
591644
});
592645

593646
it("tests layer field", () => {
594-
cy.addFieldWithName("array", "myarray");
595-
cy.getByDataCy("treeItem").contains("myarray").as("arrayField");
596-
cy.addField("layerObjectField", "@arrayField");
597-
cy.getByDataCy("treeItem").contains("items").as("layerItems");
598-
cy.addFieldWithName("text", "myfield", "@layerItems");
647+
cy.addFieldWithName("layer", "mylayer");
648+
cy.getByDataCy("treeItem").contains("mylayer").as("layerField");
649+
cy.addField("text", "@layerField");
599650

600651
cy.getByDataCy("formPreview")
601-
.find(`fieldset#root${SEP}myarray`)
652+
.find(`fieldset#root${SEP}mylayer`)
602653
.getByDataCy("addItemButton")
603654
.as("addItem")
604655
.click();
@@ -610,14 +661,14 @@ describe("test basic functionality", () => {
610661
.first()
611662
.click();
612663
cy.getByDataCy("layerModal")
613-
.find(`input#root${SEP}myarray${SEP}0${SEP}myfield`)
664+
.find(`input#root${SEP}mylayer${SEP}0`)
614665
.as("input0")
615666
.clearTypeBlur("First item");
616667
cy.getByDataCy("layerModal").find("button").contains("OK").click();
617668

618669
cy.get("@layerItem").last().click();
619670
cy.getByDataCy("layerModal")
620-
.find(`input#root${SEP}myarray${SEP}1${SEP}myfield`)
671+
.find(`input#root${SEP}mylayer${SEP}1`)
621672
.as("input1")
622673
.should("exist");
623674
cy.getByDataCy("layerModal").find("button").contains("Cancel").click();

formule-demo/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
5+
<link rel="icon" type="image/png" href="/formule-icon.png" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>Formule demo</title>
88
</head>

formule-demo/public/formule-icon.png

52.2 KB
Loading

formule-demo/src/App.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -350,11 +350,7 @@ function App() {
350350
<Col
351351
xs={24}
352352
md={14}
353-
style={{
354-
overflowX: "hidden",
355-
height: "100%",
356-
padding: "0px 25px",
357-
}}
353+
style={{ overflowX: "hidden", height: "100%" }}
358354
>
359355
<FormPreview liveValidate={true} hideAnchors={false} />
360356
</Col>

index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
65
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
76
<title>Formule</title>
87
</head>

public/vite.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/admin/components/FormPreview.jsx

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const FormPreview = ({ liveValidate, hideAnchors }) => {
2828
}}
2929
data-cy="formPreview"
3030
>
31-
<Row justify="center" style={{ margin: "15px" }}>
31+
<Row justify="center" style={{ margin: "18px" }}>
3232
<Segmented
3333
options={[
3434
{ label: "Editable", value: "editable", icon: <EditOutlined /> },
@@ -39,17 +39,26 @@ const FormPreview = ({ liveValidate, hideAnchors }) => {
3939
onChange={handleSegmentChange}
4040
/>
4141
</Row>
42-
{segment === "editable" ? (
43-
<EditablePreview hideTitle liveValidate={liveValidate} />
44-
) : (
45-
<Form
46-
schema={customizationContext.transformSchema(schema)}
47-
uiSchema={uiSchema}
48-
formData={formData}
49-
hideAnchors={hideAnchors}
50-
isPublished
51-
/>
52-
)}
42+
<div
43+
style={{
44+
padding: "0 25px",
45+
flex: 1,
46+
overflowY: "auto",
47+
overflowX: "hidden",
48+
}}
49+
>
50+
{segment === "editable" ? (
51+
<EditablePreview hideTitle liveValidate={liveValidate} />
52+
) : (
53+
<Form
54+
schema={customizationContext.transformSchema(schema)}
55+
uiSchema={uiSchema}
56+
formData={formData}
57+
hideAnchors={hideAnchors}
58+
isPublished
59+
/>
60+
)}
61+
</div>
5362
</div>
5463
);
5564
};

src/admin/components/PropertyEditor.jsx

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Grid,
77
Popconfirm,
88
Row,
9+
Space,
910
Typography,
1011
} from "antd";
1112
import { PageHeader } from "@ant-design/pro-layout";
@@ -114,26 +115,38 @@ const PropertyEditor = () => {
114115
<Typography.Title
115116
level={5}
116117
editable={
117-
path.length && {
118+
path.length &&
119+
path[path.length - 1] != "items" && {
118120
text: name,
119-
onChange: (value) =>
121+
onChange: (value) => {
120122
dispatch(
121123
renameIdByPath({
122124
path: { path, uiPath },
123125
newName: value,
124126
separator: customizationContext.separator,
125127
}),
126-
),
128+
);
129+
},
130+
onStart: () => {
131+
// using setTimeout to ensure the input is mounted
132+
setTimeout(() => {
133+
document
134+
.querySelector(".ant-typography-edit-content textarea")
135+
?.select();
136+
}, 0);
137+
},
127138
}
128139
}
129140
style={{ textAlign: "center", margin: "10px 0" }}
130141
>
131-
{getIconByType(
132-
schema,
133-
uiSchema,
134-
customizationContext.allFieldTypes,
135-
)}{" "}
136-
{name}
142+
<Space wrap={false}>
143+
{getIconByType(
144+
schema,
145+
uiSchema,
146+
customizationContext.allFieldTypes,
147+
)}
148+
{name}
149+
</Space>
137150
</Typography.Title>
138151
</Col>
139152
</Row>

src/admin/formComponents/ArrayFieldTemplate.jsx

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,23 +44,26 @@ const ArrayFieldTemplate = (props) => {
4444
{Object.keys(props.schema.items).length == 0 ? (
4545
<DropArea />
4646
) : (
47-
<Form
48-
schema={props.schema.items}
49-
uiSchema={props.uiSchema.items}
50-
formData={{}}
51-
tagName="div"
52-
showErrorList={false}
53-
FieldTemplate={FieldTemplate}
54-
ObjectFieldTemplate={ObjectFieldTemplate}
55-
ArrayFieldTemplate={ArrayFieldTemplate}
56-
liveValidate={true}
57-
validate={_validate}
58-
noHtml5Validate={true}
59-
onChange={() => {}}
60-
formContext={{ ..._path, nestedForm: true }}
61-
>
62-
<span />
63-
</Form>
47+
<>
48+
<Form
49+
schema={props.schema.items}
50+
uiSchema={props.uiSchema.items}
51+
formData={{}}
52+
tagName="div"
53+
showErrorList={false}
54+
FieldTemplate={FieldTemplate}
55+
ObjectFieldTemplate={ObjectFieldTemplate}
56+
ArrayFieldTemplate={ArrayFieldTemplate}
57+
liveValidate={true}
58+
validate={_validate}
59+
noHtml5Validate={true}
60+
onChange={() => {}}
61+
formContext={{ ..._path, nestedForm: true }}
62+
>
63+
<span />
64+
</Form>
65+
{!props.schema.items.properties && <DropArea />}
66+
</>
6467
)}
6568
</div>
6669
)}

0 commit comments

Comments
 (0)