Customize fields
In this tutorial, we will see how to create a custom field component for a specific type in your eidos.ts
, and then how to apply it in generation using the eidos_config.yaml
.
Creating custom fields is highly recommended as a way to implement bespoke functionality in your app. They are simple components that are reusable in every interface that interacts with the type in question, and if styled effectively they can be made to fit into any layout.
Field signature
A standard eidos field component has the following signature:
import { FieldComponentProps } from "@bronscode/eidos/dist/ui/fields/Field.types";
export default function MyField({
value,
onChange,
name,
label,
editable,
className,
}: FieldComponentProps<some_type>) {
return (
<div
className={`Field MyField ${className ?? ""} ${name ?? ""}`}
>
{/* do stuff */}
</div>
);
}
Here, FieldComponentProps
has the following type definition:
interface FieldComponentProps<T> {
/** Value of the field */
value: T;
/** Callback on change of value, required when editable=true */
onChange?: (value: T) => void;
/** id of component */
name?: string;
/** Display label */
label?: string;
/** Editable boolean, default false */
editable?: boolean;
/** Class name for either the field itself, or the surrounding div */
className?: string;
}
For styling purposes, it is important to include both the className
and the name
property in the className
of the root element in your field.
The name
property will be populated by the generation with the name of the property that this field is editing, and allows you to position your field in the layout of any containing field. The className
allows you to add more variants of your field through CSS later on.
Example
This example shows the canonical way to create a field for Ref<T>
, where T
is any type in your top level endpoints.
Here, our type is called Subject
, with endpoint /subjects
.
import { map, mapValues } from "lodash-es";
import { useResource, endpoints, UnionField } from "@bronscode/eidos";
export default function RefSubjectField({ value, onChange, editable, label, name, className }) {
const [subjects] = useResource(endpoints.subjects);
if (!subjects) {
return <div />;
}
return (
<UnionField
name={name}
className={className}
label={label}
value={value?.id}
options={map(subjects, (subject) => subject.id)}
labels={mapValues(subjects, (subject) => subject.name)}
onChange={(subjectId) => onChange(subjects[subjectId])}
editable={editable}
/>
);
}
Here we used the built-in UnionField
Handling undefined values
If the value you pass to an input component like <input/>
or <textarea/>
is undefined
, that field is considered uncontrolled.
When a user's action then changes the value to something that is defined, the field will transform into a controlled field.
This will cause React to produce the following warning in the dev console:
Warning: A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component.
This can cause confusing behaviour that is hard to debug, until you realize that this is the problem.
So, if you can define some default value for your field, it is wise to use this. For instance, if your field is handling strings,
use value ?? ""
to ensure the component is always controlled.
See also the react docs.
Applying your override
Say we saved our field in src/fields/MyCustomField.js
. Now we want to tell the generator to output this field whenever it encounters a certain type, say MyType
.
To do this, we create a new override in our eidos_config.yaml
file:
react:
output: "./src/fields/fields.js"
overrides:
MyType:
importPath: ./MyCustomField.js
componentName: default
The importPath
is relative to the output file location, in this case our custom field and the generated fields.js
are in the same folder.
The componentName
is used to specify the name of your function if you are using named exports. If you are using a default export, the componentName
should be default
.
This override syntax suppports generic types, in their unaltered form. So the RefSubjectField
we defined in the example above can be added as follows:
react:
output: "./src/fields/fields.js"
overrides:
MyType:
importPath: ./MyCustomField.js
componentName: default
Ref<Subject>:
importPath: ./RefSubjectField.js
componentName: default
Using aliases
Say we want to create a custom field for a phone number, which allows selecting some area code and then nicely formats the user's input.
A phone number will likely be represented as a string
, but we do not want to override all strings.
So, we add a new type PhoneNumber
, which is just an alias for string
, and then override this.
So your definition in eidos.ts
might look like:
type PhoneNumber = string;
interface User extends BaseObject {
name: string;
phoneNumber: PhoneNumber;
age: number;
}
and then you can override just the field that is generated for the phoneNumber
property, by specifying the override for the PhoneNumber
type in eidos_config.yaml
.