Skip to content

Commit

Permalink
upgrade liform-react
Browse files Browse the repository at this point in the history
  • Loading branch information
4rthem committed Dec 21, 2023
1 parent 0e9c31b commit 57281dc
Show file tree
Hide file tree
Showing 42 changed files with 1,953 additions and 84 deletions.
12 changes: 12 additions & 0 deletions lib/js/liform-react/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
node_modules
npm-debug.log
dist
lib
es
.DS_Store
yarn.lock
.nyc_output/
coverage/
examples/bundle*
package-lock.json
built_docs/
37 changes: 37 additions & 0 deletions lib/js/liform-react/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@alchemy/liform-react",
"version": "1.0.0",
"description": "Generate forms from json-schema to use with React (and redux-form)",
"main": "./src/index.jsx",
"scripts": {},
"keywords": [
"react",
"json-schema",
"form",
"redux-form"
],
"repository": {
"type": "git",
"url": "https://github.com/limenius/liform-react.git"
},
"author": "Nacho Martin",
"license": "MIT",
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"dependencies": {
"ajv": "^8.12.0",
"classnames": "^2.2.5",
"deepmerge": "^2.0.1",
"lodash": "^4.17.21",
"prop-types": "^15.5.10",
"react-redux": "^9.0.4",
"redux": "^4.2.1",
"redux-form": "^8.3.10"
},
"devDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
Empty file.
84 changes: 84 additions & 0 deletions lib/js/liform-react/src/buildSyncValidation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Ajv from "ajv";
import merge from "deepmerge";
import { set as _set } from "lodash";

const setError = (error, schema) => {
// convert property accessor (.xxx[].xxx) notation to jsonPointers notation
if (error.instancePath.charAt(0) === ".") {
error.instancePath = error.instancePath.replace(/[.[]/gi, "/");
error.instancePath = error.instancePath.replace(/[\]]/gi, "");
}
const instancePathParts = error.instancePath.split("/").slice(1);
let instancePath = error.instancePath.slice(1).replace(/\//g, ".");
const type = findTypeInSchema(schema, instancePathParts);

let errorToSet;
if (type === "array" || type === "allOf" || type === "oneOf") {
errorToSet = { _error: error.message };
} else {
errorToSet = error.message;
}

let errors = {};
_set(errors, instancePath, errorToSet);
return errors;
};

const findTypeInSchema = (schema, instancePath) => {
if (!schema) {
return;
} else if (instancePath.length === 0 && schema.hasOwnProperty("type")) {
return schema.type;
} else {
if (schema.type === "array") {
return findTypeInSchema(schema.items, instancePath.slice(1));
} else if (schema.hasOwnProperty("allOf")) {
if (instancePath.length === 0) return "allOf";
schema = { ...schema, ...merge.all(schema.allOf) };
delete schema.allOf;
return findTypeInSchema(schema, instancePath);
} else if (schema.hasOwnProperty("oneOf")) {
if (instancePath.length === 0) return "oneOf";
schema.oneOf.forEach(item => {
let type = findTypeInSchema(item, instancePath);
if (type) {
return type;
}
});
} else {
return findTypeInSchema(
schema.properties[instancePath[0]],
instancePath.slice(1)
);
}
}
};

const buildSyncValidation = (schema, ajvParam = null) => {
let ajv = ajvParam;
if (ajv === null) {
ajv = new Ajv({
allErrors: true,
strict: false
});
}
return values => {
const valid = ajv.validate(schema, values);
if (valid) {
return {};
}
const ajvErrors = ajv.errors;

let errors = ajvErrors.map(error => {
return setError(error, schema);
});
// We need at least two elements
errors.push({});
errors.push({});
return merge.all(errors);
};
};

export default buildSyncValidation;

export { setError };
44 changes: 44 additions & 0 deletions lib/js/liform-react/src/compileSchema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
function isObject(thing) {
return typeof thing === "object" && thing !== null && !Array.isArray(thing);
}

function compileSchema(schema, root) {
if (!root) {
root = schema;
}
let newSchema;

if (isObject(schema)) {
newSchema = {};
for (let i in schema) {
if (schema.hasOwnProperty(i)) {
if (i === "$ref") {
newSchema = compileSchema(resolveRef(schema[i], root), root);
} else {
newSchema[i] = compileSchema(schema[i], root);
}
}
}
return newSchema;
}

if (Array.isArray(schema)) {
newSchema = [];
for (let i = 0; i < schema.length; i += 1) {
newSchema[i] = compileSchema(schema[i], root);
}
return newSchema;
}

return schema;
}

function resolveRef(uri, schema) {
uri = uri.replace("#/", "");
const tokens = uri.split("/");
const tip = tokens.reduce((obj, token) => obj[token], schema);

return tip;
}

export default compileSchema;
68 changes: 68 additions & 0 deletions lib/js/liform-react/src/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from "react";
import PropTypes from "prop-types";
import DefaultTheme from "./themes/bootstrap3";
import { reduxForm } from "redux-form";
import renderFields from "./renderFields";
import renderField from "./renderField";
import processSubmitErrors from "./processSubmitErrors";
import buildSyncValidation from "./buildSyncValidation";
import { setError } from "./buildSyncValidation";
import compileSchema from "./compileSchema";

const BaseForm = props => {
const { schema, handleSubmit, theme, error, submitting, context } = props;
return (
<form onSubmit={handleSubmit}>
{renderField(schema, null, theme || DefaultTheme, "", context)}
<div>{error && <strong>{error}</strong>}</div>
<button className="btn btn-primary" type="submit" disabled={submitting}>
Submit
</button>
</form>
);
};

const Liform = props => {
const schema = compileSchema(props.schema);
props.schema.showLabel = false;
const schemaWithOptions = compileSchema(props.schema);
const formName = props.formKey || props.schema.title || "form";

const FinalForm = reduxForm({
form: props.formKey || props.schema.title || "form",
validate: props.syncValidation || buildSyncValidation(schema, props.ajv),
initialValues: props.initialValues,
context: { ...props.context, formName }
})(props.baseForm || BaseForm);

return (
<FinalForm
renderFields={renderField.bind(this)}
{...props}
schema={schemaWithOptions}
/>
);
};

Liform.propTypes = {
schema: PropTypes.object,
onSubmit: PropTypes.func,
initialValues: PropTypes.object,
syncValidation: PropTypes.func,
formKey: PropTypes.string,
baseForm: PropTypes.func,
context: PropTypes.object,
ajv: PropTypes.object
};

export default Liform;

export {
renderFields,
renderField,
processSubmitErrors,
DefaultTheme,
setError,
buildSyncValidation,
compileSchema,
};
40 changes: 40 additions & 0 deletions lib/js/liform-react/src/processSubmitErrors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { SubmissionError } from "redux-form";
import { isEmpty as _isEmpty } from "lodash"; // added for empty check

const convertToReduxFormErrors = obj => {
let objectWithoutChildrenAndFalseErrors = {};
Object.keys(obj).map(name => {
if (name === "children") {
objectWithoutChildrenAndFalseErrors = {
...objectWithoutChildrenAndFalseErrors,
...convertToReduxFormErrors(obj[name])
};
} else {
if (obj[name].hasOwnProperty("children")) {
// if children, take field from it and set them directly as own field
objectWithoutChildrenAndFalseErrors[name] = convertToReduxFormErrors(
obj[name]
);
} else {
if (
obj[name].hasOwnProperty("errors") &&
!_isEmpty(obj[name]["errors"])
) {
// using lodash for empty error check, dont add them if empty
objectWithoutChildrenAndFalseErrors[name] = obj[name]["errors"];
}
}
}
return null;
});
return objectWithoutChildrenAndFalseErrors;
};

const processSubmitErrors = errors => {
if (errors.hasOwnProperty("errors")) {
errors = convertToReduxFormErrors(errors.errors);
throw new SubmissionError(errors);
}
};

export default processSubmitErrors;
51 changes: 51 additions & 0 deletions lib/js/liform-react/src/renderField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from "react";
import deepmerge from "deepmerge";

const guessWidget = (fieldSchema, theme) => {
if (fieldSchema.widget) {
return fieldSchema.widget;
} else if (fieldSchema.hasOwnProperty("enum")) {
return "choice";
} else if (fieldSchema.hasOwnProperty("oneOf")) {
return "oneOf";
} else if (theme[fieldSchema.format]) {
return fieldSchema.format;
}
return fieldSchema.type || "object";
};

const renderField = (
fieldSchema,
fieldName,
theme,
prefix = "",
context = {},
required = false
) => {
if (fieldSchema.hasOwnProperty("allOf")) {
fieldSchema = { ...fieldSchema, ...deepmerge.all(fieldSchema.allOf) };
delete fieldSchema.allOf;
}

const widget = guessWidget(fieldSchema, theme);

if (!theme[widget]) {
throw new Error("liform: " + widget + " is not defined in the theme");
}

const newFieldName = prefix ? prefix + fieldName : fieldName;

return React.createElement(theme[widget], {
key: fieldName,
fieldName: widget === "oneOf" ? fieldName : newFieldName,
label:
fieldSchema.showLabel === false ? "" : fieldSchema.title || fieldName,
required: required,
schema: fieldSchema,
theme,
context,
prefix
});
};

export default renderField;
38 changes: 38 additions & 0 deletions lib/js/liform-react/src/renderFields.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import renderField from "./renderField";

export const isRequired = (schema, fieldName) => {
if (!schema.required) {
return false;
}
return schema.required.indexOf(fieldName) !== -1;
};

const renderFields = (schema, theme, prefix = null, context = {}) => {
let props = [];
for (let i in schema.properties) {
props.push({ prop: i, propertyOrder: schema.properties[i].propertyOrder });
}
props = props.sort((a, b) => {
if (a.propertyOrder > b.propertyOrder) {
return 1;
} else if (a.propertyOrder < b.propertyOrder) {
return -1;
} else {
return 0;
}
});
return props.map(item => {
const name = item.prop;
const field = schema.properties[name];
return renderField(
field,
name,
theme,
prefix,
context,
isRequired(schema, name)
);
});
};

export default renderFields;
Loading

0 comments on commit 57281dc

Please sign in to comment.