The Mutation component is essentially a <form>
element used to mutate data on the server. Here is the configuration it expects:
export type MutationProps = ConditionalProps & MachineProps & {
args: Variables
buttonText?: string
enabledEvents?: Array<FormTransitions>
id: string
isReadOnly?: boolean
label?: string
machineConfig?: CreateMachineParamsConfig
mutation?: string
name: string
elements: Array<ComponentConfig>
url: string
mutableVariableNames?: Array<string>
}
Let's take this configuration one property at a time and see what each does.
The args
property is of type Variables
and provides read-only variables to the mutation. If you are using the actions
response from the GraphQL query, these are provided by the suppliedArguments
property of GraphQL type TransitionActionSuppliedArguments
:
type TransitionActionSuppliedArgument {
name: String!
value: JSON!
}
For example, the userId
might be a supplied argument. That would then be injected into the Mutation props like this:
{
args: {
userId,
}
}
The mutable arguments from state should be merged into this object and then sent with the mutation as varaibles
(more on this below).
This is the label for the button associated with this form, e.g., "Submit update". Optional, defaults to "Update".
This is an array of configuration objects for the fields and other content in the Mutation form. It's optional, defaulting to an empty array. This is because the button is controlled by the Mutation, so you could have a Mutation that simply runs a GraphQL mutation when the button is clicked. It doesn't have to be a form.
This looks like any other elements
array, but remember that to connect the fields, buttons, etc. to the Mutation, pass the id
of the Mutation as the mutationId
of the field, e.g.:
{
datatype: "STRING_DATATYPE", // Creates a StringField component
label: "Display name",
id: generateShortId(), // Base58 v4 UUID
initialValue: displayName,
isRequired: true,
mutationId, // Connects displayName StringField to the Mutation for which this is an `element`
name: "displayName", // This will be the field name (key) in the Mutation context (state)
}
This is a set of transitions for which the mutation's state machine should publish events. The default state machine is a Form Machine, hence the possible enabledEvents
are:
export type FormTransitions =
| "FORM_DATA"
| "FORM_FAILURE"
| "FORM_INITIALIZE"
| "FORM_RESET"
| "FORM_SUBMIT"
| "FORM_SUCCESS"
| "FORM_UPDATE"
These are the events published by the mutation, not the events to which it listens, and they are published by the state machine. Simply pass the enabledEvents
to the state machine in its configuration (machineConfig
), and it will publish on those events. You may not need them.
The default form state machine looks like this:
You can see the seven possible transitions and that each of them calls publishFormEvent
. That code looks like this:
publishFormEvent: (context: FormMachineContext, event: FormMachineEvent) => {
const { enabledEvents = [], topic, ...rest } = context
if ((enabledEvents as Array<FormTransitions>).includes(event.type)) {
publish({ eventName: event.type, data: { ...rest } }, { topic: topic as string })
}
}
The topic is currently set to the Mutation ID in the Mutation component. The rest
is all the rest of the Form Machine's context
. Here's what that currently looks like:
export default function Mutation(props: MutationProps): JSX.Element {
const {
args,
buttonText = "Update",
elements,
enabledEvents,
id,
isReadOnly,
label,
machineConfig,
mutableVariableNames,
mutation,
name,
url = "http://example.com/",
} = props
const config: CreateMachineParamsConfig = useMemo(
() => (machineConfig as CreateMachineParamsConfig ?? makeMutationConfiguration({
enabledEvents,
isReadOnly,
label,
mutationId: props.id,
name,
})),
[],
)
// ...
}
export type MutationProps = ConditionalProps & MachineProps & {
args: Variables
buttonText?: string
elements: Array<ComponentConfig>
enabledEvents?: Array<FormTransitions>
id: string
isReadOnly?: boolean
label?: string
mutableVariableNames?: Array<string>
mutation?: string
name: string
url: string
}
These are passed to the default machine configuration:
export default function makeMutationMachineConfiguration({
enabledEvents = ["FORM_FAILURE", "FORM_INITIALIZE", "FORM_RESET", "FORM_SUBMIT", "FORM_SUCCESS", "FORM_UPDATE"],
initial = "enabled",
injectInto = "enabled",
isReadOnly,
label,
mutationId,
name,
}: Props, type: MachineType = "DEFAULT"): CreateMachineParamsConfig {
// ...
}
See also How to update form state from field events below.
The id
is the ID of the mutation/form. It is passed to the form fields as the mutationId
. This is used in the HTML elements as the form
attribute (and in the <form>
element as the id
attribute), which ties them to the form even if they are not nested in the <form>
tags (which here they are not—the form tag sits alone).
This ID is also used as the topic
for all published events from form fields, buttons, etc. This allows the Mutation
to listen for events with topic equal to its id
and to update its own state accordingly.
For example, a string field might publish an "INPUT_UPDATE" event with topic equal to the mutationId
of a Mutation. The Mutation would then subscribe to events with topic its ID, receive the INPUT_UPDATE event, and merge the data
from that event into its own context.fields
property. In this way changes to field state are updated in form state.
When the form is submitted, it can then submit the current field state.
This sets the form to read only mode, which hides the submit button. If all fields are read-only but the form (Mutation) isn't, then the button will be displayed but disabled as it is disabled when none of the fields are dirty (and if all fields are read-only, then none can be dirty).
Mutations can optionally be labelled. This label appears at the top of the form as an <h3>
heading:
return label
? (
<section className={css.mutation}>
<h3>{label}</h3>
{form}
</section>
)
: (
<div className={css.mutation}>
{form}
</div>
)
Note also that labeling the form makes it a <section>
—HTML sectioning content—which means that it appears in the HTML Document Outline with that heading. This is important for accessibility with screen readers.
This prop allows the Site Configuration object (from the mapQueryResponseToSiteConfig function) to specifiy any state machine configuration, which allows configuration from the back end in a data-driven UI situation.
The mutableVariableNames
is an array of string names of those form fields that will be included in the mutation's variables
object.
For example, if the props to the Mutation included:
{
args: { id: "some-id" },
mutableVariableNames: ["email", "name"],
mutation: `mutation MyMutation($id: String!, $name: String, $email: String) {
updateNameAndAddress(input: {
id: $id
name: $name
email: $email
}) {
id
}
}`,
name: "MyMutation",
}
Then when the mutation was submitted, the args
object would be merged with the values from the mutable fields to create a variables
object:
const {
fields: {
email: {
value: email,
},
name: {
value: name,
},
}
} = context
const variables = {
...args,
email,
name,
}
Then this would be supplied to the mutate
function returned from the useGraphQL
hook:
const { mutate } = useGraphQL(url)
const response = await mutate(name, mutation, variables) // name is the operationName
For the above, that would send something like this in a POST as a JSON body to the url
:
{
"operationName":"MyMutation",
"query":"mutation MyMutation($id: String!, $name: String, $address: String) {\n updateNameAndAddress(input: {\n id: $id\n name: $name\n address: $address\n }) {\n id\n }\n}",
"variables":{
"id":"some-id",
"email":"[email protected]",
"name":"Bob Dobbs"
}
}
The mutation
is the GraphQL mutation as a string. Typically it is stored in as a named export, e.g., MY_MUTATION, in a constants.ts
file as close to where it is used as possible. It is then imported and used by the Mutation. However, this can be overridden using the mutation
prop.
Care should be taken to ensure that the name
props matches the name of the mutation.
The name of the mutation, passed as the operationName
in the POST request to the GraphQL service. Must match the name of the mutation
(see above example).
The URL of the GraphQL service. This is passed to the useGraphQL
hook to configure the mutate
(or query
) function used to communicate with the GraphQL service. It is set by the Anti-Corruption Layer.