Skip to content
Open
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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ It also exports the following functions:

- **`initFormuleSchema`**: Inits or resets the JSONSchema. You can also load an existing schema by passing it as an argument.
- **`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.
- **`getFormState`**: Formule also has a separate state for the form data, which stores the data input into each of the fields of the form. You can also synchronize this state, but keep in mind that doing it can cause performance issues with big forms.

And the following utilities:

Expand Down Expand Up @@ -172,7 +173,7 @@ const transformErrors = (errors) => {
<FormuleForm transformErrors={transformErrors} />
```

### Syncing Formule state
### Syncing Formule / Form 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:

Expand All @@ -181,13 +182,15 @@ const handleFormuleStateChange = (newState) => {
// Do something when the state changes
};

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

Alternatively, you can pull the current state on demand by calling `getFormuleState` at any moment.

You can also synchronize the form state (e.g. `formData`) by provinding a callback via `syncFormState`, but beware of the performance impact on big forms and prefer calling `getFormState` instead when needed.

### Loading form data / prefill form

If you want to prefill the form with existing data, you can provide the form data to `FormuleForm`. This will fill in the corresponding fields with the information in `formData`:
Expand Down
11 changes: 6 additions & 5 deletions formule-demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
saveToLocalStorage,
loadFromLocalStorage,
AiChatFooter,
getFormState,
} from "react-formule";
import { theme } from "./theme";
import formuleLogo from "./assets/logo.png";
Expand Down Expand Up @@ -127,7 +128,7 @@ const App = () => {
};

return (
<FormuleContext theme={theme} synchronizeState={handleFormuleStateChange}>
<FormuleContext theme={theme} syncFormuleState={handleFormuleStateChange}>
<Layout hasSider style={{ height: "100vh" }}>
<Layout.Sider
hidden={menuHidden}
Expand Down Expand Up @@ -350,7 +351,7 @@ const App = () => {
value={JSON.stringify(
formuleState?.current.uiSchema,
null,
2
2,
)}
lang="json"
height="25vh"
Expand All @@ -366,7 +367,7 @@ const App = () => {
>
<Typography.Text strong>Form data</Typography.Text>
<CodeViewer
value={JSON.stringify(formuleState?.formData, null, 2)}
value={JSON.stringify(getFormState().formData, null, 2)}
lang="json"
height="25vh"
reset
Expand Down Expand Up @@ -434,7 +435,7 @@ const App = () => {
const reader = new FileReader();
reader.onload = (event) => {
const newSchema = JSON.parse(
event?.target?.result as string
event?.target?.result as string,
);
const { schema, uiSchema } = newSchema;
if (schema && uiSchema) {
Expand All @@ -443,7 +444,7 @@ const App = () => {
message.success("Uploaded and loaded successfully");
} else {
message.error(
"Your json should include a schema and a uiSchema key"
"Your json should include a schema and a uiSchema key",
);
}
};
Expand Down
15 changes: 10 additions & 5 deletions src/StateSynchronizer.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { useSelector } from "react-redux";
import { isEqual } from "lodash-es";

const StateSynchronizer = ({ synchronizeState, children }) => {
const state = useSelector((state) => state.schemaWizard);
const StateSynchronizer = ({ callback, slice = "schemaWizard", children }) => {
const state = useSelector((state) => state[slice]);
const previousStateRef = useRef(state);

useEffect(() => {
synchronizeState(state);
}, [state, synchronizeState]);
if (!isEqual(previousStateRef.current, state)) {
callback(state);
previousStateRef.current = state;
}
}, [state, callback]);

return children;
};
Expand Down
36 changes: 22 additions & 14 deletions src/admin/components/EditablePreview.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import { useContext } from "react";
import React, { useMemo } from "react";
import { useDispatch } from "react-redux";
import { debounce } from "lodash-es";
import Form from "../../forms/Form";
import { shoudDisplayGuideLinePopUp } from "../utils";
import { Row, Empty, Space, Typography } from "antd";
import { useDispatch, useSelector } from "react-redux";
import CustomizationContext from "../../contexts/CustomizationContext";
import { updateFormData } from "../../store/schemaWizard";

const EditablePreview = ({ hideTitle, liveValidate }) => {
const schema = useSelector((state) => state.schemaWizard.current.schema);
const uiSchema = useSelector((state) => state.schemaWizard.current.uiSchema);
const formData = useSelector((state) => state.schemaWizard.formData);
import { updateFormData } from "../../store/form";

const EditablePreview = ({
hideTitle,
liveValidate,
schema,
uiSchema,
formData,
}) => {
const dispatch = useDispatch();

const customizationContext = useContext(CustomizationContext);
const debouncedDispatch = useMemo(() => {
return debounce((newFormData) => {
dispatch(updateFormData({ value: newFormData }));
}, 500);
}, [dispatch]);

const handleFormChange = (change) => {
debouncedDispatch(change.formData);
};

return (
<>
Expand Down Expand Up @@ -47,12 +57,10 @@ const EditablePreview = ({ hideTitle, liveValidate }) => {
</Row>
) : (
<Form
schema={customizationContext.transformSchema(schema)}
schema={schema}
uiSchema={uiSchema}
formData={formData || {}}
onChange={(change) =>
dispatch(updateFormData({ value: change.formData }))
}
onChange={handleFormChange}
liveValidate={liveValidate}
/>
)}
Expand Down
18 changes: 14 additions & 4 deletions src/admin/components/FormPreview.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useContext, useState } from "react";
import { useContext, useState, useMemo } from "react";
import Form from "../../forms/Form";
import { Segmented, Row } from "antd";
import { useSelector } from "react-redux";
Expand All @@ -9,10 +9,14 @@ import CustomizationContext from "../../contexts/CustomizationContext";
const FormPreview = ({ liveValidate, hideAnchors }) => {
const schema = useSelector((state) => state.schemaWizard.current.schema);
const uiSchema = useSelector((state) => state.schemaWizard.current.uiSchema);
const formData = useSelector((state) => state.schemaWizard.formData);
const formData = useSelector((state) => state.form.formData);

const customizationContext = useContext(CustomizationContext);

const transformedSchema = useMemo(() => {
return customizationContext.transformSchema(schema);
}, [customizationContext, schema]);

const [segment, setSegment] = useState("editable");

const handleSegmentChange = (value) => {
Expand Down Expand Up @@ -48,10 +52,16 @@ const FormPreview = ({ liveValidate, hideAnchors }) => {
}}
>
{segment === "editable" ? (
<EditablePreview hideTitle liveValidate={liveValidate} />
<EditablePreview
hideTitle
liveValidate={liveValidate}
schema={transformedSchema}
uiSchema={uiSchema}
formData={formData}
/>
) : (
<Form
schema={customizationContext.transformSchema(schema)}
schema={transformedSchema}
uiSchema={uiSchema}
formData={formData}
hideAnchors={hideAnchors}
Expand Down
32 changes: 23 additions & 9 deletions src/exposed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import store from "./store/configureStore";
import fieldTypes from "./admin/utils/fieldTypes";
import { SetStateAction } from "react";
import { RJSFSchema } from "@rjsf/utils";
import { initialState, schemaInit, updateFormData } from "./store/schemaWizard";
import { initialState, schemaInit } from "./store/schemaWizard";
import { updateFormData } from "./store/form.ts";
import StateSynchronizer from "./StateSynchronizer";
import { isEqual, pick } from "lodash-es";
import { itemIdGenerator } from "./utils";
Expand All @@ -30,17 +31,26 @@ export const FormuleContext = ({
theme,
separator = "::",
errorBoundary,
synchronizeState,
syncFormuleState,
syncFormState,
transformSchema = (schema: RJSFSchema) => schema,
ai = { providers: defaultProviders },
}: FormuleContextProps) => {
const content = synchronizeState ? (
<StateSynchronizer synchronizeState={synchronizeState}>
{children}
</StateSynchronizer>
) : (
children
);
let content = children;

[
{ callback: syncFormState, slice: "form" },
{ callback: syncFormuleState, slice: "schemaWizard" },
].forEach(({ callback, slice }) => {
if (callback) {
content = (
<StateSynchronizer callback={callback} slice={slice}>
{content}
</StateSynchronizer>
);
}
});

return (
<Provider store={store}>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
Expand Down Expand Up @@ -111,6 +121,10 @@ export const getFormuleState = () => {
return store.getState().schemaWizard;
};

export const getFormState = () => {
return store.getState().form;
};

export const getAllFromLocalStorage = () => {
return Object.entries(localStorage)
.filter(([k, v]) => {
Expand Down
14 changes: 6 additions & 8 deletions src/forms/widgets/base/SelectWidget.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { useState } from "react";
import { useSelector } from "react-redux";
import Select from "antd/lib/select";
import { debounce } from "lodash-es";
import axios from "axios";
import { Empty } from "antd";
import { Empty, Select } from "antd";
import PropTypes from "prop-types";

import {
Expand All @@ -12,6 +10,7 @@ import {
enumOptionsValueForIndex,
} from "@rjsf/utils";
import isString from "lodash-es";
import store from "../../../store/configureStore";

const SelectWidget = ({
autofocus,
Expand All @@ -35,8 +34,6 @@ const SelectWidget = ({
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);

const formData = useSelector((state) => state.schemaWizard.formData);

const handleChange = (nextValue) => {
onChange(
enumOptionsValueForIndex(nextValue, enumOptions || data, emptyValue),
Expand Down Expand Up @@ -81,7 +78,7 @@ const SelectWidget = ({
};

const updateSearch = (value, callback) => {
let data = formData;
const data = store.getState().form.formData;
if (params) {
Object.entries(params).map((param) => {
const path = _replace_hash_with_current_indexes(param[1]);
Expand Down Expand Up @@ -140,6 +137,7 @@ const SelectWidget = ({
},
)
}
style={{ width: "100%" }}
/>
);
};
Expand All @@ -149,10 +147,10 @@ SelectWidget.propTypes = {
autofocus: PropTypes.bool,
formContext: PropTypes.object,
id: PropTypes.string,
multiple: PropTypes.string,
multiple: PropTypes.bool,
placeholder: PropTypes.string,
readonly: PropTypes.bool,
value: PropTypes.object,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
options: PropTypes.object,
onBlur: PropTypes.func,
onChange: PropTypes.func,
Expand Down
9 changes: 5 additions & 4 deletions src/forms/widgets/base/TextWidget.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { Button, Descriptions, InputNumber, Popover } from "antd";
import PropTypes from "prop-types";
import { isRegExp } from "lodash-es";
import axios from "axios";
import { useDispatch, useSelector } from "react-redux";
import { updateFormData } from "../../../store/schemaWizard";
import { useDispatch } from "react-redux";
import { updateFormData } from "../../../store/form";
import { set, cloneDeep } from "lodash-es";
import { InfoCircleOutlined } from "@ant-design/icons";
import store from "../../../store/configureStore";

const INPUT_STYLE = {
width: "100%",
Expand Down Expand Up @@ -35,8 +36,6 @@ const TextWidget = ({
useState(false);
const [apiCalling, setApiCalling] = useState(false);

const formData = useSelector((state) => state.schemaWizard.formData);

const dispatch = useDispatch();

const handleNumberChange = (nextValue) => onChange(nextValue);
Expand Down Expand Up @@ -69,6 +68,7 @@ const TextWidget = ({
};

const autoFillOtherFields = (event) => {
const formData = store.getState().form.formData;
let newFormData = cloneDeep(formData);
const url = options.autofill_from;
const fieldsMap = options.autofill_fields;
Expand All @@ -80,6 +80,7 @@ const TextWidget = ({
return;

fieldsMap.map((el) => {
// TODO: error on modifying root title: cannot read properties of undefined (reading 'map')
let destination = _replace_hash_with_current_indexes(el[1]);
set(newFormData, destination, undefined);
});
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { initFormuleSchema } from "./exposed";
export { getFormuleState } from "./exposed";
export { getFormState } from "./exposed";
export { FormuleContext } from "./exposed";
export { getAllFromLocalStorage } from "./exposed";
export { saveToLocalStorage } from "./exposed";
Expand Down
7 changes: 7 additions & 0 deletions src/store/configureStore.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import schemaWizard, { initialState } from "./schemaWizard";
import form from "./form";
import { configureStore } from "@reduxjs/toolkit";

const preloadedState = () => {
Expand All @@ -19,6 +20,11 @@ const preloadedState = () => {
export const persistMiddleware = ({ getState }) => {
return (next) => (action) => {
const result = next(action);

if (action.type.startsWith("form/")) {
return result;
}

const state = getState();
const persistedData = {
schemaWizard: {
Expand All @@ -34,6 +40,7 @@ export const persistMiddleware = ({ getState }) => {
const store = configureStore({
reducer: {
schemaWizard,
form,
},
preloadedState: preloadedState(),
middleware: (getDefaultMiddleware) =>
Expand Down
Loading