Skip to content

Commit

Permalink
make dnd optional
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelgrc committed Jan 22, 2024
1 parent 6419ae1 commit 3bc6437
Show file tree
Hide file tree
Showing 10 changed files with 369 additions and 182 deletions.
52 changes: 29 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ Formule is a **powerful, user-friendly, extensible and mobile-friendly form buil

It originated from the need of a flexible tool for physicists at CERN to create their custom forms in the [CERN Analysis Preservation](https://github.com/cernanalysispreservation/analysispreservation.cern.ch) application (a process that was originally done by the CAP team who had to manually define the JSON schemas for every member experiment) in a zero-code fashion. This tool proved to be very useful for us to more easily scalate and expand, reaching a wider audience here at CERN. So, we thought it could also be useful for other people and decided to decouple it from CAP and release it as an open source library.

>[!WARNING]
>react-formule has just come out and is undergoing active development, so please feel free to share any issue you find with us and/or to contribute!
> [!WARNING]
> react-formule has just come out and is undergoing active development, so please feel free to share any issue you find with us and/or to contribute!
## :carousel_horse: How it looks like

Expand All @@ -21,19 +21,20 @@ Formule consists of the following main components:

- **`FormuleContext`**: Formule components need to be wrapped by a FormuleContext. It also allows you to provide an antd theme and your own custom fields and widgets.
- The form editor, which has been split into three different components that work together for more flexibility:
- **`SelectOrEdit`** (or, separately, **`SelectFieldType`** and **`PropertyEditor`**): You can select fields to add to the form and customize their properties.
- **`SchemaPreview`**: A tree view of the fields where you can rearrange or select fields to be edited.
- **`FormPreview`**: A live, iteractive preview of the form.
- **`SelectOrEdit`** (or, separately, **`SelectFieldType`** and **`PropertyEditor`**): You can select fields to add to the form and customize their properties.
- **`SchemaPreview`**: A tree view of the fields where you can rearrange or select fields to be edited.
- **`FormPreview`**: A live, iteractive preview of the form.
- **`FormuleForm`**: You can use it to display a form (JSON Schema) generated by Formule.

It also exports the following functions:

- **`initFormuleSchema`**: Inits the JSONSchema, *needs* to be run on startup.
- **`initFormuleSchema`**: Inits the JSONSchema, _needs_ to be run on startup.
- **`getFormuleState`**: Formule has its own internal redux state. You can retrieve it at any moment if you so require for more advanced use cases. If you want to continuosly synchronize the Formule state in your app, you can pass a callback function to FormuleContext instead (see below), which will be called every time the form state changes.

### Field types

Formule includes a variety of predefined field types, grouped in three categories:

- **Simple fields**: `Text`, `Text area`, `Number`, `Checkbox`, `Switch`, `Radio`, `Select` and `Date` fields.
- **Collections**:
- `Object`: Use it of you want to group fields or to add several of them inside of a `List`.
Expand All @@ -45,23 +46,25 @@ Formule includes a variety of predefined field types, grouped in three categorie

You can freely remove some of these predefined fields and add your own custom fields and widgets following the JSON Schema specifications. More details below.

All of these items contain different settings that you can tinker with, separated into **Schema Settings** (*generally* affecting how the field *works*) and **UI Schema Settings** (*generally* affecting how the field *looks* like).
All of these items contain different settings that you can tinker with, separated into **Schema Settings** (_generally_ affecting how the field _works_) and **UI Schema Settings** (_generally_ affecting how the field _looks_ like).

## :horse_racing: Setting it up

### Installation

```sh
npm install react-formule
# or
yarn add react-formule
```

### Basic setup

```jsx
import {
FormuleContext,
SelectOrEdit,
SchemaPreview,
import {
FormuleContext,
SelectOrEdit,
SchemaPreview,
FormPreview,
initFormuleSchema
} from "react-formule";
Expand All @@ -75,7 +78,10 @@ const useEffect(() => initFormuleSchema(), []);
</FormuleContext>
```

If you want to disable the [DnD](https://github.com/react-dnd/react-dnd) functionality, you can pass `dnd={false}` to `FormuleContext`. This will enable an alternative, button-based method to add and move fields.

### Customizing and adding new field types

```jsx
<FormuleContext theme={{token: {colorPrimary: "blue"}}} customFieldTypes={...} customFields={...} customWidgets={...}>
// ...
Expand All @@ -86,26 +92,25 @@ If you use Formule to edit existing JSON schemas that include extra fields (e.g.

```jsx
const transformSchema = (schema) => {
// Remove properties...
return transformedSchema
}
// Remove properties...
return transformedSchema;
};

<FormuleContext transformSchema={transformSchema}>
// ...
</FormuleContext>
<FormuleContext transformSchema={transformSchema}>// ...</FormuleContext>;
```

### Syncing Formule state

If you want to run some logic in your application every time the current Formule state changes in any way (e.g. to run some action every time a new field is added to the form) you can pass a function to be called back when that happens:

```jsx
const handleFormuleStateChange = newState => {
// Do something when the state changes
}
const handleFormuleStateChange = (newState) => {
// Do something when the state changes
};

<FormuleContext synchonizeState={handleFormuleStateChange}>
// ...
</FormuleContext>
// ...
</FormuleContext>;
```

Alternatively, you can pull the current state on demand by calling `getFormuleState` at any moment.
Expand All @@ -114,4 +119,5 @@ Alternatively, you can pull the current state on demand by calling `getFormuleSt
> For more examples, feel free to browse around the [CERN Analysis Preservation](https://github.com/cernanalysispreservation/analysispreservation.cern.ch) repository, where we use all the features mentioned above.
## :space_invader: Local demo & how to contribute
You can also clone the repo and run `formule-demo` to play around. Follow the instructions in its [README](./formule-demo/README.md): it will explain how to install `react-formule` as a local dependency (with either `yarn link` or, better, `yalc`) so that you can modify Formule and test the changes live in your host app, which will be ideal if you want to troubleshoot or contribute to the project.

You can also clone the repo and run `formule-demo` to play around. Follow the instructions in its [README](./formule-demo/README.md): it will explain how to install `react-formule` as a local dependency (with either `yarn link` or, better, `yalc`) so that you can modify Formule and test the changes live in your host app, which will be ideal if you want to troubleshoot or contribute to the project.
26 changes: 14 additions & 12 deletions formule-demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { initFormuleSchema } from "react-formule";
import { useEffect } from "react";
import { Row, Col } from "antd";

import "./style.css"
import "./style.css";

const PRIMARY_COLOR = "#006996";

Expand All @@ -15,16 +15,18 @@ function App() {
}, []);

return (
<FormuleContext theme={{
token: {
colorPrimary: PRIMARY_COLOR,
colorLink: PRIMARY_COLOR,
colorLinkHover: "#1a7fa3",
borderRadius: 2,
colorBgLayout: "#f0f2f5",
fontFamily: "Titillium Web",
},
}}>
<FormuleContext
theme={{
token: {
colorPrimary: PRIMARY_COLOR,
colorLink: PRIMARY_COLOR,
colorLinkHover: "#1a7fa3",
borderRadius: 2,
colorBgLayout: "#f0f2f5",
fontFamily: "Titillium Web",
},
}}
>
<Row style={{ height: "100%" }}>
<Col
xs={10}
Expand All @@ -33,6 +35,7 @@ function App() {
overflowX: "hidden",
height: "100%",
display: "flex",
padding: "0px 15px",
}}
className="tour-field-types"
>
Expand All @@ -59,7 +62,6 @@ function App() {
<FormPreview liveValidate={true} />
</Col>
</Row>

</FormuleContext>
);
}
Expand Down
22 changes: 22 additions & 0 deletions src/admin/components/SelectFieldModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Button, Modal } from "antd";
import SelectFieldType from "./SelectFieldType";

const SelectFieldModal = ({ visible, setVisible, insertInPath }) => {
return (
<Modal
destroyOnClose
open={visible}
onCancel={() => setVisible(false)}
footer={
<Button type="primary" onClick={() => setVisible(false)}>
Ok
</Button>
}
width={450}
>
<SelectFieldType insertInPath={insertInPath} />
</Modal>
);
};

export default SelectFieldModal;
85 changes: 61 additions & 24 deletions src/admin/components/SelectFieldType.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,49 @@
import { Col, Collapse, Row, Space, Typography } from "antd";
import { Button, Col, Collapse, Row, Space, Typography } from "antd";
import Draggable from "./Draggable";
import { useContext } from "react";
import CustomizationContext from "../../contexts/CustomizationContext";
import PlusOutlined from "@ant-design/icons/PlusOutlined";
import { useDispatch } from "react-redux";
import { addByPath } from "../../store/schemaWizard";

const SelectFieldType = () => {

const customizationContext = useContext(CustomizationContext)
const SelectFieldType = ({ insertInPath }) => {
const dispatch = useDispatch();

const customizationContext = useContext(CustomizationContext);

const DraggableOrNot = ({ index, type, objectKey, children }) => {
if (customizationContext.dnd && !insertInPath) {
return (
<Draggable key={index} data={type} type={objectKey}>
{children}
</Draggable>
);
} else {
return (
<Row>
<Col flex="auto">{children}</Col>
<Col>
<Button
icon={<PlusOutlined style={{ fontSize: "14px" }} />}
onClick={() =>
dispatch(
addByPath({
path: insertInPath || { schema: [], uiSchema: [] },
value: type.default,
}),
)
}
type="link"
size="small"
/>
</Col>
</Row>
);
}
};

return (
<div style={{ width: "100%", padding: "0px 15px" }}>
<div style={{ width: "100%" }}>
<Typography.Title
level={4}
style={{ textAlign: "center", margin: "15px 0" }}
Expand All @@ -18,25 +53,27 @@ const SelectFieldType = () => {
<Collapse
defaultActiveKey={["simple", "collections"]}
ghost
items={Object.entries(customizationContext.allFieldTypes).map(([key, type]) => ({
key: key,
label: type.title,
children: (
<Row gutter={[16, 8]}>
{Object.entries(type.fields).map(([key, type], index) => (
<Col xs={22} xl={12} key={key} style={{ width: "100%" }}>
<Draggable key={index} data={type} type={key}>
<Space style={{ padding: "2px 5px" }}>
{type.icon}
{type.title}
</Space>
</Draggable>
</Col>
))}
</Row>
),
className: type.className,
}))}
items={Object.entries(customizationContext.allFieldTypes).map(
([key, type]) => ({
key: key,
label: type.title,
children: (
<Row gutter={[16, 8]}>
{Object.entries(type.fields).map(([key, type], index) => (
<Col xs={22} xl={12} key={key} style={{ width: "100%" }}>
<DraggableOrNot index={index} type={type} objectKey={key}>
<Space style={{ padding: "2px 5px" }}>
{type.icon}
{type.title}
</Space>
</DraggableOrNot>
</Col>
))}
</Row>
),
className: type.className,
}),
)}
/>
</div>
);
Expand Down
14 changes: 9 additions & 5 deletions src/admin/formComponents/ArrayFieldTemplate.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { useState } from "react";
import { useContext, useState } from "react";
import PropTypes from "prop-types";
import SchemaTreeItem from "./SchemaTreeItem";
import Form from "../../forms/Form";
import ObjectFieldTemplate from "./ObjectFieldTemplate";
import FieldTemplate from "./FieldTemplate";
import { _validate } from "../utils";
import DropArea from "./DropArea";
import CustomizationContext from "../../contexts/CustomizationContext";

const ArrayFieldTemplate = props => {
const ArrayFieldTemplate = (props) => {
const [display, setDisplay] = useState(false);

const customizationContext = useContext(CustomizationContext);

let schemaPath = [];
let uiSchemaPath = [];
if (props.rawErrors) {
let _rawErrors = props.rawErrors.filter(i => (i.schema ? i : false));
let _rawErrors = props.rawErrors.filter((i) => (i.schema ? i : false));
let { schema, uiSchema } = _rawErrors[0];
schemaPath = schema;
uiSchemaPath = uiSchema;
Expand Down Expand Up @@ -41,7 +44,8 @@ const ArrayFieldTemplate = props => {

{display && (
<div style={{ marginLeft: "10px" }}>
{Object.keys(props.schema.items).length == 0 ? (
{Object.keys(props.schema.items).length == 0 &&
customizationContext.dnd ? (
<DropArea />
) : (
<Form
Expand Down Expand Up @@ -77,4 +81,4 @@ ArrayFieldTemplate.propTypes = {
id: PropTypes.string,
};

export default ArrayFieldTemplate
export default ArrayFieldTemplate;
Loading

0 comments on commit 3bc6437

Please sign in to comment.