Skip to content

Commit

Permalink
Make RJSF theme with in-repo components and style
Browse files Browse the repository at this point in the history
  • Loading branch information
tomcur committed Jul 20, 2023
1 parent 542e3f6 commit e7270d2
Show file tree
Hide file tree
Showing 16 changed files with 540 additions and 98 deletions.
2 changes: 1 addition & 1 deletion astroplant-frontend/src/Components/ApiForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { JSONSchema7 } from "json-schema";
import { UiSchema, FormValidation } from "@rjsf/utils";
import validator from "@rjsf/validator-ajv8";

import RjsfForm from "~/rjsf-theme-semantic-ui";
import RjsfForm from "~/rjsf-theme";

import {
Notification,
Expand Down
18 changes: 0 additions & 18 deletions astroplant-frontend/src/rjsf-theme-semantic-ui/index.ts

This file was deleted.

This file was deleted.

34 changes: 34 additions & 0 deletions astroplant-frontend/src/rjsf-theme/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { withTheme, ThemeProps } from "@rjsf/core";

// We make use of RJSF's core templates as much as possible, rather than
// maintaining our own set. Those templates use Bootstrap, but we don't bundle
// Bootstrap. We apply some presentational styling on the HTML tags / CSS
// classes output by RJSF. It's possible for this to go out of sync when RJSF
// updates, but hopefully this does not happen frequently. We can always
// implement our own template set if necessary.
import "./rjsf.css";

import TimeWidget from "./widgets/TimeWidget";
import CoordinateField from "./CoordinateField";
import buttonTemplates from "./templates/ButtonTemplates";
import BaseInputTemplate from "./templates/BaseInputTemplate";
import SelectWidget from "./widgets/SelectWidget";
import TextareaWidget from "./widgets/TextareaWidget";

const theme: ThemeProps = {
fields: {
CoordinateField: CoordinateField,
},
widgets: {
TextareaWidget: TextareaWidget,
TimeWidget: TimeWidget,
SelectWidget: SelectWidget,
},
templates: {
BaseInputTemplate: BaseInputTemplate,
ButtonTemplates: buttonTemplates(),
},
};

const Form = withTheme(theme);
export default Form;
68 changes: 68 additions & 0 deletions astroplant-frontend/src/rjsf-theme/rjsf.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
form.rjsf {
fieldset {
padding: 0;
margin: 0;
margin-bottom: 0.5rem;
border: none;
}

fieldset fieldset {
padding-left: 1rem;
}

legend {
display: inline-block;
width: 100%;
font-weight: 600;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}

label {
font-weight: 600;
font-size: 0.85em;
}

label,
legend {
.required {
color: var(--red-500);
}
}

.field-description {
margin-bottom: 0.5em;
}

.array-item-list {
margin-top: 0.25rem;
margin-bottom: 0.25rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}

.array-item {
display: flex;
border-radius: 0.5rem;
border-left: 0.2rem solid var(--gray-100);
border-right: 0.2rem solid var(--gray-100);

.col-xs-9 {
flex: 1;
}

.array-item-toolbox {
display: flex;
align-items: center;
padding: 0.25rem 1rem;
}
}

[type="checkbox"] {
margin-right: 0.5em;
}

select {
width: 100%;
}
}
115 changes: 115 additions & 0 deletions astroplant-frontend/src/rjsf-theme/templates/BaseInputTemplate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Based on
// https://github.com/rjsf-team/react-jsonschema-form/blob/c0265bd2e388c16b50d5c7ab1d1dafd200d5bb80/packages/core/src/components/templates/BaseInputTemplate.tsx

import { ChangeEvent, FocusEvent, useCallback } from "react";
import {
ariaDescribedByIds,
BaseInputTemplateProps,
examplesId,
getInputProps,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
} from "@rjsf/utils";
import { Input } from "~/Components/Input";

/** The `BaseInputTemplate` is the template to use to render the basic `<input>` component for the `core` theme.
* It is used as the template for rendering many of the <input> based widgets that differ by `type` and callbacks only.
* It can be customized/overridden for other themes or individual implementations as needed.
*
* @param props - The `WidgetProps` for this template
*/
export default function BaseInputTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
>(props: BaseInputTemplateProps<T, S, F>) {
const {
id,
name, // remove this from ...rest
value,
readonly,
disabled,
autofocus,
onBlur,
onFocus,
onChange,
onChangeOverride,
options,
schema,
uiSchema,
formContext,
registry,
rawErrors,
type,
hideLabel, // remove this from ...rest
hideError, // remove this from ...rest
...rest
} = props;

// Note: since React 15.2.0 we can't forward unknown element attributes, so we
// exclude the "options" and "schema" ones here.
if (!id) {
console.log("No id for", props);
throw new Error(`no id for props ${JSON.stringify(props)}`);
}
const inputProps = {
...rest,
...getInputProps<T, S, F>(schema, type, options),
};

let inputValue;
if (inputProps.type === "number" || inputProps.type === "integer") {
inputValue = value || value === 0 ? value : "";
} else {
inputValue = value == null ? "" : value;
}

const _onChange = useCallback(
({ target: { value } }: ChangeEvent<HTMLInputElement>) =>
onChange(value === "" ? options.emptyValue : value),
[onChange, options],
);
const _onBlur = useCallback(
({ target: { value } }: FocusEvent<HTMLInputElement>) => onBlur(id, value),
[onBlur, id],
);
const _onFocus = useCallback(
({ target: { value } }: FocusEvent<HTMLInputElement>) => onFocus(id, value),
[onFocus, id],
);

return (
<>
<Input
id={id}
name={id}
className="form-control"
readOnly={readonly}
disabled={disabled}
autoFocus={autofocus}
value={inputValue}
fullWidth
{...inputProps}
list={schema.examples ? examplesId<T>(id) : undefined}
onChange={onChangeOverride || _onChange}
onBlur={_onBlur}
onFocus={_onFocus}
aria-describedby={ariaDescribedByIds<T>(id, !!schema.examples)}
/>
{Array.isArray(schema.examples) && (
<datalist key={`datalist_${id}`} id={examplesId<T>(id)}>
{(schema.examples as string[])
.concat(
schema.default && !schema.examples.includes(schema.default)
? ([schema.default] as string[])
: [],
)
.map((example: any) => {
return <option key={example} value={example} />;
})}
</datalist>
)}
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
FormContextType,
RJSFSchema,
StrictRJSFSchema,
TemplatesType,
} from "@rjsf/utils";
import { Icon } from "semantic-ui-react";
import { Button } from "~/Components/Button";

function buttonTemplates<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
>(): TemplatesType<T, S, F>["ButtonTemplates"] {
return {
SubmitButton: (props) => <Button {...props}>Submit</Button>,
AddButton: (props) => (
/* a bit hacky to specify the style here */
<Button
{...props}
size="small"
variant="muted"
style={{ float: "right", marginRight: "1.2rem" }}
>
<Icon name="add" />
</Button>
),
CopyButton: (props) => (
<Button {...props} leftAdornment="+">
Add
</Button>
),
MoveDownButton: (props) => (
<Button {...props} size="small" variant="muted">
<Icon name="arrow down" />
</Button>
),
MoveUpButton: (props) => (
<Button {...props} size="small" variant="muted">
<Icon name="arrow up" />
</Button>
),
RemoveButton: (props) => (
<Button {...props} size="small" variant="muted">
<Icon name="delete" />
</Button>
),
};
}

export default buttonTemplates;
Loading

0 comments on commit e7270d2

Please sign in to comment.