Skip to content

Commit 92d30ce

Browse files
authored
Add support for customizing Add component buttons (#455)
#274: Add support for customizing Add popover radio options
1 parent 4e340b7 commit 92d30ce

File tree

11 files changed

+301
-175
lines changed

11 files changed

+301
-175
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ example/build
1313

1414
.DS_Store
1515
*.swp
16+
.vscode
1617

1718
npm-debug.log*

docs/Mods.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ declare type Mods = {|
1414
[string]: FormInput,
1515
...
1616
},
17+
components?: {|
18+
add?: (properties: { [string]: any }) => void,
19+
},
1720
tooltipDescriptions?: {|
1821
add?: string,
1922
cardObjectName?: string,
@@ -31,6 +34,8 @@ declare type Mods = {|
3134
displayNameLabel?: string,
3235
descriptionLabel?: string,
3336
inputTypeLabel?: string,
37+
addElementLabel?: string,
38+
addSectionLabel?: string,
3439
|},
3540
showFormHead?: boolean,
3641
deactivatedFormInputs?: Array<string>,
@@ -185,3 +190,5 @@ The text for the labels of a few of the inputs in the Form Builder can similarly
185190
- `displayNameLabel` - The label for the "Display Name" field.
186191
- `descriptionLabel` - The label for the "Description" field.
187192
- `inputTypeLabel` - The label for the "Input Type" field.
193+
- `addElementLabel` - The label for the popover option when adding a new form element.
194+
- `addSectionLabel` - The label for the popover option when adding a new form section.

docs/Usage.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,119 @@ mods = {
306306

307307
will set the UI schema for a new element to use the `customWidget` widget.
308308

309+
### Customizing "Add" buttons
310+
311+
This form builder includes a button to add new form elements or sections (the button with a + symbol). In some cases you may want to modify this feature or provide custom button components. There are a few options to customize this component.
312+
313+
#### **1. Custom Labels**
314+
315+
One simple way to provide customization is to change the labels for "Add form element" and "Add form section". You can update these labels with the following mods:
316+
317+
```js
318+
const mods = {
319+
//...
320+
labels: {
321+
//...
322+
addElementLabel: 'my element label',
323+
addSectionLabel: 'my section label',
324+
}
325+
}
326+
```
327+
328+
#### **2. Invoking the "add" functions**
329+
330+
You can invoke the following two functions anywhere in your app in order to add form elements and/or sections within the Form Builder component:
331+
332+
```js
333+
import { addCardObj, addSectionObj } from '@ginkgo-bioworks/react-json-schema-form-builder';
334+
```
335+
336+
Both of these functions require the following properties:
337+
338+
```js
339+
type properties = {
340+
schema: object, // FormBuilder schema
341+
uischema: object, // FormBuilder uiSchema
342+
mods: object, // FormBuilder mods
343+
onChange: (newSchema: object, newUiSchema: object) => {
344+
// update schemas when button clicked...
345+
},
346+
definitionData: string, // see: schema.definitions
347+
definitionUi: string, // see: uischema.definitions
348+
categoryHash: [string], // see: FormBuilder.onMount()
349+
};
350+
```
351+
352+
The `categoryHash` helps FormBuilder match input types and is generated by FormBuilder upon rendering. You can get the generated hash through the `onMount` callback.
353+
354+
Example:
355+
356+
```js
357+
<FormBuilder
358+
//...
359+
onMount={({ categoryHash }) => {
360+
// Here you can save categoryHash to props or state
361+
setCategoryHash(categoryHash);
362+
}}
363+
/>
364+
```
365+
366+
#### **3. Overriding the "Add" component**
367+
368+
By providing a custom component through `mods`, you can completely override the `Add` component.
369+
370+
To do this, provide a callback function to the `components.add` mod. This callback should expect one argument, which provides properties that are required to add elements and sections to the form builder.
371+
372+
```js
373+
import { addCardObj } from '@ginkgo-bioworks/react-json-schema-form-builder';
374+
375+
mods = {
376+
components: {
377+
add: (properties) => <MyComponent onClick={() => { addCardObj(properties) }} />
378+
}
379+
}
380+
```
381+
382+
Putting it all together, the following snippet is an example showing two fully functioning buttons:
383+
384+
```js
385+
import React, { useState } from 'react';
386+
import { addCardObj, addSectionObj } from '@ginkgo-bioworks/react-json-schema-form-builder';
387+
388+
const mods = {
389+
//...
390+
};
391+
392+
function MyFormBuilder() {
393+
const [schema, setSchema] = useState({});
394+
const [uiSchema, setUiSchema] = useState({});
395+
const [categoryHash, setCategoryHash] = useState('');
396+
397+
mods.components: {
398+
add: (addProps) => {
399+
return (
400+
<div>
401+
<Button onClick={() => { addCardObj(addProps) }}>Add Form Element</Button>
402+
<Button onClick={() => { addSectionObj(addProps) }}>Add Form Section</Button>
403+
</div>
404+
);
405+
},
406+
};
407+
408+
return (
409+
<FormBuilder
410+
schema={JSON.stringify(schema)}
411+
uischema={JSON.stringify(uiSchema)}
412+
mods={mods}
413+
onChange={(newSchema, newUiSchema) => {
414+
setSchema(JSON.parse(newSchema));
415+
setUiSchema(JSON.parse(newUiSchema));
416+
}}
417+
/>
418+
);
419+
}
420+
```
421+
309422
### Styling
310423

311424
To avoid collisions with existing CSS styles, this app uses [react-jss](https://cssinjs.org/react-jss/?v=v10.5.0) in order to generate class names avoiding overlap with others in the global scope. Using CSS to style FormBuilder and PredefinedGallery components will not work and is not supported. The ability to "skin" the FormBuilder and PredefinedGallery components may be a feature in the future.

src/formBuilder/Add.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import FontAwesomeIcon from './FontAwesomeIcon';
1414
import FBRadioGroup from './radio/FBRadioGroup';
1515
import { getRandomId } from './utils';
1616
import type { Node } from 'react';
17+
import type { ModLabels } from './types';
1718

1819
const useStyles = createUseStyles({
1920
addDetails: {
@@ -38,10 +39,12 @@ export default function Add({
3839
addElem,
3940
hidden,
4041
tooltipDescription,
42+
labels,
4143
}: {
4244
addElem: (choice: string) => void,
4345
hidden?: boolean,
4446
tooltipDescription?: string,
47+
labels?: ModLabels,
4548
}): Node {
4649
const classes = useStyles();
4750
const [popoverOpen, setPopoverOpen] = useState(false);
@@ -76,11 +79,11 @@ export default function Add({
7679
options={[
7780
{
7881
value: 'card',
79-
label: 'Form element',
82+
label: labels?.addElementLabel ?? 'Form element',
8083
},
8184
{
8285
value: 'section',
83-
label: 'Form section',
86+
label: labels?.addSectionLabel ?? 'Form section',
8487
},
8588
]}
8689
onChange={(selection) => {

src/formBuilder/Card.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export default function Card({
113113
allFormInputs,
114114
mods,
115115
showObjectNameInput = true,
116+
addProperties,
116117
}: {
117118
componentProps: {
118119
[string]: string | number | boolean | Array<string | number>,
@@ -132,6 +133,7 @@ export default function Card({
132133
mods?: Mods,
133134
allFormInputs: { [string]: FormInput },
134135
showObjectNameInput?: boolean,
136+
addProperties?: { [string]: any },
135137
}): Node {
136138
const classes = useStyles();
137139
const [modalOpen, setModalOpen] = React.useState(false);
@@ -249,13 +251,12 @@ export default function Card({
249251
TypeSpecificParameters={TypeSpecificParameters}
250252
/>
251253
</Collapse>
252-
{addElem ? (
254+
{mods?.components?.add && mods?.components?.add(addProperties)}
255+
{!mods?.components?.add && addElem && (
253256
<Add
254257
tooltipDescription={((mods || {}).tooltipDescriptions || {}).add}
255258
addElem={(choice: string) => addElem(choice)}
256259
/>
257-
) : (
258-
''
259260
)}
260261
</React.Fragment>
261262
);

src/formBuilder/CardGallery.js

Lines changed: 38 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -71,61 +71,49 @@ export default function CardGallery({
7171
</div>
7272
));
7373

74+
const hideAddButton =
75+
!!definitionSchema && Object.keys(definitionSchema).length !== 0;
76+
77+
const addProperties = {
78+
schema: { properties: definitionSchema },
79+
uischema: definitionUiSchema,
80+
mods: mods,
81+
onChange: (newDefinitions, newDefinitionUis) => {
82+
const oldUi = newDefinitionUis;
83+
const newUi = {};
84+
85+
Object.keys(oldUi).forEach((definedUiSchemaKey) => {
86+
if (!['definitions', 'ui:order'].includes(definedUiSchemaKey))
87+
newUi[definedUiSchemaKey] = oldUi[definedUiSchemaKey];
88+
});
89+
onChange(newDefinitions.properties, newUi);
90+
},
91+
definitionData: definitionSchema,
92+
definitionUi: definitionUiSchema,
93+
categoryHash,
94+
};
95+
7496
return (
7597
<div className='form_gallery'>
7698
{componentArr}
7799
{componentArr.length === 0 && <h5>No components in "definitions"</h5>}
78100
<div className='form_footer'>
79-
<Add
80-
tooltipDescription={((mods || {}).tooltipDescriptions || {}).add}
81-
addElem={(choice: string) => {
82-
if (choice === 'card') {
83-
addCardObj({
84-
schema: { properties: definitionSchema },
85-
uischema: definitionUiSchema,
86-
mods: mods,
87-
onChange: (newDefinitions, newDefinitionUis) => {
88-
const oldUi = newDefinitionUis;
89-
const newUi = {};
90-
91-
Object.keys(oldUi).forEach((definedUiSchemaKey) => {
92-
if (
93-
!['definitions', 'ui:order'].includes(definedUiSchemaKey)
94-
)
95-
newUi[definedUiSchemaKey] = oldUi[definedUiSchemaKey];
96-
});
97-
onChange(newDefinitions.properties, newUi);
98-
},
99-
definitionData: definitionSchema,
100-
definitionUi: definitionUiSchema,
101-
categoryHash,
102-
});
103-
} else if (choice === 'section') {
104-
addSectionObj({
105-
schema: { properties: definitionSchema },
106-
uischema: definitionUiSchema,
107-
onChange: (newDefinitions, newDefinitionUis) => {
108-
const oldUi = newDefinitionUis;
109-
const newUi = {};
110-
111-
Object.keys(oldUi).forEach((definedUiSchemaKey) => {
112-
if (
113-
!['definitions', 'ui:order'].includes(definedUiSchemaKey)
114-
)
115-
newUi[definedUiSchemaKey] = oldUi[definedUiSchemaKey];
116-
});
117-
onChange(newDefinitions.properties, newUi);
118-
},
119-
definitionData: definitionSchema,
120-
definitionUi: definitionUiSchema,
121-
categoryHash,
122-
});
123-
}
124-
}}
125-
hidden={
126-
!!definitionSchema && Object.keys(definitionSchema).length !== 0
127-
}
128-
/>
101+
{!hideAddButton &&
102+
mods?.components?.add &&
103+
mods.components.add(addProperties)}
104+
{!mods?.components?.add && (
105+
<Add
106+
tooltipDescription={((mods || {}).tooltipDescriptions || {}).add}
107+
addElem={(choice: string) => {
108+
if (choice === 'card') {
109+
addCardObj(addProperties);
110+
} else if (choice === 'section') {
111+
addSectionObj(addProperties);
112+
}
113+
}}
114+
hidden={hideAddButton}
115+
/>
116+
)}
129117
</div>
130118
</div>
131119
);

0 commit comments

Comments
 (0)