Using the Setting Form Component
Author:
Christopher Tse
Changed on:
14 Oct 2024
Key Points
- This page will guide you through placing the Setting Form Component on a page and customizing it for use by a business user. Key topics covered include:
- Adding the component to a page
- Using the component within an Accordion component
- Generating a schema for a setting
- Adding titles to individual fields
- Using the component with custom fields
Steps
You will need:
- A setting of any type (Other than LOB)
- A web app page where you want to add the setting
Step 1 - Generating a schema for the setting
To generate a schema for your setting, we recommend using an online tool that converts JSON into a JSON schema. You can retrieve an example here.
Here’s how to use it:
- Copy and paste an example setting into the tool.
- The tool will generate the schema for the setting.
Once you have the schema, store it as its own setting. The setting type should be JSON, and we recommend storing it at the account level to ensure global accessibility.
Example:
Let’s use the RETURN_REASON setting as an example. This setting is made up of label-value pairs, represented in schema terms as an array of objects. Each object contains two strings: one for the label and one for the value. Using a JSON schema converter, you should expect a schema that looks something like this:
1{
2 "$schema": "http://json-schema.org/draft-07/schema#",
3 "title": "Generated schema for Root",
4 "type": "array",
5 "items": {
6 "type": "object",
7 "properties": {
8 "label": {
9 "type": "string"
10 },
11 "value": {
12 "type": "string"
13 }
14 },
15 "required": [
16 "label",
17 "value"
18 ]
19 }
20}
Language: json
Name: Sample JSON schema
Description:
[Warning: empty required content area]As explained above, you can now take the generated schema and create a new setting with the following details:
Name | RETURN_REASON_SCHEMA |
Context | ACCOUNT |
Context ID | 0 |
Type | JSON |
Value | {{schema from above}} |
Step 2 - Adding titles to each field
In the schema, you can assign titles to each field using the
`title`
`title`
`label`
Depending on the JSON schema generator you used, titles may already be present in your schema. In this example, only the top-level title was added initially, but we also need titles for the
`label`
`value`
`object`
1{
2 "title": "Return Reasons",
3 "type": "array",
4 "items": {
5 "type": "object",
6 "properties": {
7 "label": {
8 "title": "Label",
9 "type": "string"
10 },
11 "value": {
12 "title": "Value",
13 "type": "string"
14 }
15 }
16 }
17}
Language: json
Name: Modified Schema
Description:
[Warning: empty required content area](Note: i18n strings are currently not supported)
For schemas that already include titles, you can use this step to update them with more appropriate labels. Ensure that each field has a clear and descriptive title.
We will later demonstrate how these titles are reflected in the settings component when we review how it is laid out on a page.
Step 3 - Adding the Settings Component onto a Page
To add the settings component to a page, you need to open the manifest fragment for that page.
In this example, we will incorporate the return reason setting into a new tab called Settings under the Retailer detail page in OMS.
The retailer page is located in the
`fc.mystique.manifest.oms.fragment.admin`
Once you’ve created the setting, the next step is to edit the Retailer details page to split it into two tabs. The first tab will be named Details and will retain the existing layout currently defined in the manifest. The second tab, called Settings, will host the new component for editing the return reasons.
1{
2 "path": "retailers/:id",
3 "type": "page",
4 "component": "fc.page",
5 "data": {
6 "query": "query ($id: ID!) { retailerById(id: $id) { id ref tradingName createdOn websiteUrlName websiteUrl supportContactName supportEmail primaryEmail supportPhone summary updatedOn }}",
7 "variables": {
8 "id": "{{params.id}}"
9 }
10 },
11 "props": {
12 "title": "Retailer - {{retailerById.ref}}",
13 "backButtons": [
14 {
15 "path": "retailers",
16 "menuLabel": "i18n:fc.admin.retailers.detail.breadcrumb.backToRetailers"
17 }
18 ]
19 },
20 "descendants": [
21 {
22 "component": "fc.tabs",
23 "props": {
24 "layouts": [
25 {
26 "label": "Summary"
27 },
28 {
29 "label": "Settings"
30 }
31 ]
32 },
33 "descendants": [
34 {
35 "component": "fc.page.section",
36 "descendants": [
37 {
38 "component": "fc.card.attribute",
39 "props": {
40 "title": "i18n:fc.admin.retailers.detail.card.summary.title",
41 "width": "half",
42 "dataSource": "retailerById",
43 "attributes": [
44 {
45 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.id.label",
46 "template": "{{id}}"
47 },
48 {
49 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.ref.label",
50 "template": "{{ref}}"
51 },
52 {
53 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.creationDate.label",
54 "template": "{{dateStringFormatter createdOn}} ({{dateRelative createdOn}})"
55 },
56 {
57 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.updatedDate.label",
58 "template": "{{dateStringFormatter updatedOn}} ({{dateRelative updatedOn}})"
59 }
60 ]
61 }
62 },
63 {
64 "component": "fc.card.attribute",
65 "props": {
66 "title": "i18n:fc.admin.retailers.detail.card.details.title",
67 "width": "half",
68 "dataSource": "retailerById",
69 "attributes": [
70 {
71 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.tradingName.label",
72 "template": "{{tradingName}}"
73 },
74 {
75 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.websiteUrl.label",
76 "template": "{{websiteUrl}}",
77 "link_template": "{{websiteUrl}}",
78 "condition": "{{and websiteUrl}}"
79 },
80 {
81 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.supportPhone.label",
82 "template": "{{supportPhone}}"
83 },
84 {
85 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.supportEmail.label",
86 "template": "{{supportEmail}}"
87 }
88 ]
89 }
90 }
91 ]
92 },
93 {
94 "component": "fc.page.section",
95 "descendants": [
96 {
97 "component": "fc.settingForm",
98 "props": {
99 "context": "RETAILER",
100 "contextId": "1",
101 "setting": "RETURN_REASON",
102 "schema": "RETURN_REASON_SCHEMA"
103 }
104 }
105 ]
106 }
107 ]
108 }
109 ]
110}
Language: json
Name: Manifest Snippet
Description:
[Warning: empty required content area]Step 4 - Styling the Page
Now that you have added the settings component to the page, it doesn't look very appealing on its own, as it currently occupies the entire screen while the form only takes up about half of that space. To enhance the appearance, you can place the settings component within an Accordion Component, adjust its width, and use empty components to center the Accordion.
1{
2 "path": "retailers/:id",
3 "type": "page",
4 "component": "fc.page",
5 "data": {
6 "query": "query ($id: ID!) { retailerById(id: $id) { id ref tradingName createdOn websiteUrlName websiteUrl supportContactName supportEmail primaryEmail supportPhone summary updatedOn }}",
7 "variables": {
8 "id": "{{params.id}}"
9 }
10 },
11 "props": {
12 "title": "Retailer - {{retailerById.ref}}",
13 "backButtons": [
14 {
15 "path": "retailers",
16 "menuLabel": "i18n:fc.admin.retailers.detail.breadcrumb.backToRetailers"
17 }
18 ]
19 },
20 "descendants": [
21 {
22 "component": "fc.tabs",
23 "props": {
24 "layouts": [
25 {
26 "label": "Summary"
27 },
28 {
29 "label": "Settings"
30 }
31 ]
32 },
33 "descendants": [
34 {
35 "component": "fc.page.section",
36 "descendants": [
37 {
38 "component": "fc.card.attribute",
39 "props": {
40 "title": "i18n:fc.admin.retailers.detail.card.summary.title",
41 "width": "half",
42 "dataSource": "retailerById",
43 "attributes": [
44 {
45 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.id.label",
46 "template": "{{id}}"
47 },
48 {
49 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.ref.label",
50 "template": "{{ref}}"
51 },
52 {
53 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.creationDate.label",
54 "template": "{{dateStringFormatter createdOn}} ({{dateRelative createdOn}})"
55 },
56 {
57 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.updatedDate.label",
58 "template": "{{dateStringFormatter updatedOn}} ({{dateRelative updatedOn}})"
59 }
60 ]
61 }
62 },
63 {
64 "component": "fc.card.attribute",
65 "props": {
66 "title": "i18n:fc.admin.retailers.detail.card.details.title",
67 "width": "half",
68 "dataSource": "retailerById",
69 "attributes": [
70 {
71 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.tradingName.label",
72 "template": "{{tradingName}}"
73 },
74 {
75 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.websiteUrl.label",
76 "template": "{{websiteUrl}}",
77 "link_template": "{{websiteUrl}}",
78 "condition": "{{and websiteUrl}}"
79 },
80 {
81 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.supportPhone.label",
82 "template": "{{supportPhone}}"
83 },
84 {
85 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.supportEmail.label",
86 "template": "{{supportEmail}}"
87 }
88 ]
89 }
90 }
91 ]
92 },
93 {
94 "component": "fc.page.section",
95 "descendants": [
96 {
97 "component": "fc.page.section.column",
98 "props": {
99 "width": 2
100 }
101 },
102 {
103 "component": "fc.accordion",
104 "props": {
105 "width": 8,
106 "summary": "Return Reasons",
107 "details": {
108 "direction": "column",
109 "components": [
110 {
111 "component": "fc.settingForm",
112 "props": {
113 "context": "RETAILER",
114 "contextId": "1",
115 "setting": "RETURN_REASON",
116 "schema": "RETURN_REASON_SCHEMA"
117 }
118 }
119 ]
120 }
121 }
122 },
123 {
124 "component": "fc.page.section.column",
125 "props": {
126 "width": 2
127 }
128 }
129 ]
130 }
131 ]
132 }
133 ]
134}
Language: json
Name: Manifest Snippet
Description:
[Warning: empty required content area]In the snippet above, we've enclosed the settings component in an Accordion with a width set to 8. Additionally, we've added empty column components on either side of the Accordion. This arrangement allows us to center the Accordion, resulting in a more aesthetically pleasing layout.
Now it looks much better.
Advanced
Advanced - Using a Custom Component
At this point, the page is functional for business users to edit return reasons.
However, there's some room for improvement here. For example, there's the fact that return reasons consist of a label and a value. The setting structure was designed this way so that the dropdown label could be changed without changing the value sent to the backend.
Since our audience are business users, they are primarily concerned with editing the label, which affects what is displayed in the dropdown menu. This raises the question: can we configure the form so that users only need to input the label, and the value auto-populates? The answer is yes, and we can achieve this by using a custom field.
Setting Up the Custom Field:
1. Components SDK
First, ensure you have access to the Components SDK. If you don't already have it, you can download it from the Components SDK page.
2. Create the Custom Field
We will design a field that outputs a JSON object similar to the following:
1{
2 "label": "string",
3 "value": "string"
4}
Language: plain_text
Name: Contract for the Field's Output
Description:
[Warning: empty required content area]You can use the following boilerplate code to get started on creating the new custom field:
1import { FormFieldProps } from 'mystique/registry/FieldRegistry';
2import { FC } from 'react';
3
4
5interface LabelValuePair {
6 label: string
7 value: string
8}
9
10export const LabelValuePairField : FC<FormFieldProps<LabelValuePair>> = ({onChange}) => {
11
12 return <></>;
13};
Language: plain_text
Name: Basic Framework for the field
Description:
[Warning: empty required content area]3. Register the Custom Field
To register this field in the field registry, add the following line to your
`index.tsx`
1FieldRegistry.register(['labelValuePair'], LabelValuePairField);
Language: plain_text
Name: Registering the field in index.html
Description:
[Warning: empty required content area]4. Use the Custom Field in the Schema
Now you can utilize this custom field in your schema. To do this, add the
`fcField`
`name`
1{
2 "$schema": "http://json-schema.org/draft-07/schema#",
3 "title": "Return Reasons",
4 "type": "array",
5 "items": {
6 "title": "Return Reason",
7 "type": "object",
8 "fcField": {
9 "name": "labelValuePair"
10 },
11 "properties": {
12 "label": {
13 "title": "Label",
14 "description": "Put the label you want the business user to see here",
15 "type": "string"
16 },
17 "value": {
18 "title": "Value",
19 "description": "The internal code for a reason. This should stay constant whiel thelabel can change.",
20 "type": "string"
21 }
22 }
23 }
24}
Language: json
Name: RETURN_REASON_SCHEMA
Description:
[Warning: empty required content area]5. Add UI to Your Field
Let's add some UI to the field. You can use the label prop, which gets passed the value of the item's
`title`
1import { css } from '@emotion/react';
2import { Box, TextField } from '@material-ui/core';
3import { FormFieldProps } from 'mystique/registry/FieldRegistry';
4import { FC } from 'react';
5
6interface LabelValuePair {
7 label: string
8 value: string
9}
10
11export const LabelValuePairField : FC<FormFieldProps<LabelValuePair>> = ({label, onChange}) => {
12 const boxCss = css`
13 display: flex;
14 align-items: center;
15 `;
16 const textFieldCss = css`
17 padding-left: 16px;
18 flex-grow: 1;
19 `;
20
21 return <Box css={boxCss}>
22 {label} <TextField css={textFieldCss} />
23 </Box>;
24};
Language: typescript
Name: Field Code
Description:
[Warning: empty required content area]6. Handle Value and Label Logic
Next, you need to pass both the label and value back to the form via the
`onChange`
1import { css } from '@emotion/react';
2import { Box, TextField } from '@material-ui/core';
3import { FormFieldProps } from 'mystique/registry/FieldRegistry';
4import { FC } from 'react';
5
6interface LabelValuePair {
7 label: string
8 value: string
9}
10
11export const LabelValuePairField : FC<FormFieldProps<LabelValuePair>> = ({label, onChange, defaultValue}) => {
12 const boxCss = css`
13 display: flex;
14 align-items: center;
15 `;
16 const textFieldCss = css`
17 padding-left: 16px;
18 flex-grow: 1;
19 `;
20
21 const onTextChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
22 onChange({
23 label: e.target.value,
24 value: ''
25 });
26 };
27
28 return <Box css={boxCss}>
29 {label} <TextField css={textFieldCss} onChange={onTextChange} defaultValue={defaultValue?.label || ''}/>
30 </Box>;
31};
Language: typescript
Name: Code
Description:
[Warning: empty required content area]7. Managing Value State
For the value, we want it to equal the label when creating a new entry, but it shouldn't change if we're editing an existing return reason. To achieve this, we'll use state to track whether the value should be the label or the previous value.
1import { css } from '@emotion/react';
2import { Box, TextField } from '@material-ui/core';
3import { FormFieldProps } from 'mystique/registry/FieldRegistry';
4import { FC, useState } from 'react';
5
6interface LabelValuePair {
7 label: string
8 value: string
9}
10
11export const LabelValuePairField : FC<FormFieldProps<LabelValuePair>> = ({label, onChange, defaultValue}) => {
12 const [value] = useState(defaultValue?.value);
13 const boxCss = css`
14 display: flex;
15 align-items: center;
16 `;
17 const textFieldCss = css`
18 padding-left: 16px;
19 flex-grow: 1;
20 `;
21
22 const onTextChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
23 onChange({
24 label: e.target.value,
25 value: value ? value : e.target.value,
26 });
27 };
28
29 return <Box css={boxCss}>
30 {label} <TextField css={textFieldCss} onChange={onTextChange} defaultValue={defaultValue?.label || ''}/>
31 </Box>;
32};
Language: typescript
Name: Code
Description:
[Warning: empty required content area]Conclusion
By following these steps, you have created a custom field that simplifies the editing process for return reasons, allowing users to focus on changing the label while automatically managing the value. This approach enhances the usability of your form for business users, aligning the technical functionality with user-friendly design.
Feel free to expand or adjust this implementation based on user feedback or additional requirements!
Advanced - Enhancing the Field with Custom Properties
We now have a custom field for return reasons that can be utilized wherever a label-value pair is needed, with the user only able to modify the label.
However, what if we want the value field's functionality to change dynamically based on the label? In some instances, we may want the value to automatically update whenever the label is changed.
To accommodate this, we can introduce an extension to the field that allows toggling this behavior on and off. Here’s how we can implement this.
Extensions can be added through the
`fcField`
`extension`
`extension`
`extension`
Let's modify our field to include an
`alwaysChangeValue`
1import { css } from '@emotion/react';
2import { Box, TextField } from '@material-ui/core';
3import { FormFieldProps } from 'mystique/registry/FieldRegistry';
4import { FC, useState } from 'react';
5
6interface LabelValuePair {
7 label: string
8 value: string
9}
10
11export const LabelValuePairField : FC<FormFieldProps<LabelValuePair>> = ({label, onChange, defaultValue, extensions}) => {
12 const [value] = useState(defaultValue?.value);
13 const boxCss = css`
14 display: flex;
15 align-items: center;
16 `;
17 const textFieldCss = css`
18 padding-left: 16px;
19 flex-grow: 1;
20 `;
21
22 const onTextChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
23 const oldValue = value ? value : e.target.value;
24 const newValue = extensions?.alwaysChangeValue ? e.target.value : oldValue;
25 onChange({
26 label: e.target.value,
27 value: newValue,
28 });
29 };
30
31 return <Box css={boxCss}>
32 {label} <TextField css={textFieldCss} onChange={onTextChange} defaultValue={defaultValue?.label || ''}/>
33 </Box>;
34};
Language: typescript
Name: New Field
Description:
[Warning: empty required content area]And here’s how the schema would look:
1{
2 "title": "Return Reason:",
3 "type": "array",
4 "items": {
5 "title": "Return Reason",
6 "type": "object",
7 "fcField": {
8 "name": "labelvaluepair",
9 "extensions": {
10 "alwaysChangeValue": true
11 }
12 },
13 "properties": {
14 "label": {
15 "title": "Label",
16 "type": "string"
17 },
18 "value": {
19 "title": "Value",
20 "type": "string"
21 }
22 }
23 }
24}
Language: json
Name: Schema
Description:
[Warning: empty required content area]Now we have a field that can adapt its behavior based on the
`alwaysChangeValue`