Editing Data
In the previous sections, you created several user interfaces displaying contact data. This article teaches you to manipulate data by creating, editing, and deleting contacts.
You can continue working with the code you completed in the previous tutorial section. Alternatively, you can download the code from here and carry on.
Preparing the UI
There are several ways to add data editing functionality to an app. In this article, you will use a modal dialog to add and edit contact items.
Button for New Contacts
Let's change the layout of the Contacts
component to add a button above the list (and wrap the button and the list into a VStack
). Later, when you click this button, it will display a dialog for adding a new contact item.
<Component name="Contacts">
<DataSource id="categories" url="/api/categories" />
<VStack>
<HStack horizontalAlignment="end">
<Button icon="plus">New Contact</Button>
</HStack>
<List
<!-- Unchanged -->
</List>
</VStack>
</Component>
The HStack
section adds a button above the list:

Triggers for Edit and Delete
Next, you should add a context menu to each contact item. This menu appears on the right side of a list item with the following markup in the Contacts
component:
<Component name="Contacts">
<DataSource id="categories" url="/api/categories" />
<VStack>
<HStack horizontalAlignment="end">
<Button icon="plus">New Contact</Button>
</HStack>
<List data="{$props.data.map(extendWithSection)}"
defaultGroups="{['Overdue', 'Today', 'Upcoming', 'No Due Date', 'Completed']}"
groupBy="section">
<!-- Unchanged -->
<Card var.categoryName="{getCategoryNameById($item.categoryId)}">
<HStack verticalAlignment="center">
<!-- Unchanged -->
<HStack verticalAlignment="center" horizontalAlignment="end">
<Badge when="{categoryName}" value="{categoryName}" colorMap="{categoriesColorMap}"/>
<Text>{smartFormatDate($item.reviewDueDate)}</Text>
</HStack>
<DropdownMenu>
<property name="triggerTemplate">
<Button icon="dotmenu" variant="ghost" themeColor="secondary"/>
</property>
<MenuItem label="Edit" />
<MenuItem label="Delete" />
</DropdownMenu>
</HStack>
</Card>
</List>
</VStack>
</Component>
The DropdownMenu
definition adds the context menu to the contact items. You can reveal the menu by clicking the three dots to the right of a particular contect item in the list:

Contact Data Dialog
When you add a new contact's details or edit a contact, the app displays a form in a modal dialog.
Create a component, ContactForm
(ContactForm.xmlui
within the components
folder), to display the dialog:
<Component name="ContactForm">
<ModalDialog id="dialog" title="Add a new Contact">
<Text>Contact Form</Text>
</ModalDialog>
<method name="open" value="dialog.open" />
</Component>
This component embeds a ModalDialog
component to display a dialog on demand. This dialog is only displayed if it is explicitly opened. The ModalDialog
component is nested within the component definition, so it cannot be displayed outside of ContactForm
without the component's cooperation. ContactForm
exposes an open
method that can be invoked from outside; it displays the dialog by invoking the dialog.open
method.
Whatever parameters you pass to the exposed open
method, XMLUI conveys to the dialog.open
method when calling it.
Showing the Form
Add a ContactForm
component to Contacts
, and set up click event handler in the New Contact button to display this dialog:
<Component name="Contacts">
<ContactForm id="contactForm" />
<DataSource id="categories" url="/api/categories" />
<VStack>
<HStack horizontalAlignment="end">
<Button icon="plus" onClick="contactForm.open()">New Contact</Button>
</HStack>
<List>
<!-- Unchanged -->
</List>
</VStack>
</Component>
In the code you assign an id (contactForm
) to the dialog component.
The dialog is not displayed unless you explicitly invoke its exposed open
method through its identifier by clicking the "New Contact" button(contactForm.open()
):

You can reuse this dialog when editing data. Modify the markup of ContactRow
:
<Component name="ContactRow">
<DataSource id="categories" url="/api/categories" />
<ContactForm id="contactForm" />
<Card>
<!-- Unchanged -->
<DropdownMenu>
<property name="triggerTemplate">
<Button icon="dotmenu" variant="ghost" themeColor="secondary"/>
</property>
<MenuItem label="Edit" onClick="contactForm.open()"/>
<MenuItem label="Delete" />
</DropdownMenu>
</HStack>
</Card>
</Component>
You use the same dialog in the ContactRow
component here. Clicking the Edit action in the dropdown opens the form imperatively, just as the New Contact button did.
Context Sensitive Dialog
The dialog always displays its title as "Add a new Contact" whether you invoke it with the "Add button" or the "Edit" menu command. Modify the dialog to test the parameter passed to the open
function (you invoke it from the Contacts
component). The $param
value represents the first parameter passed to open
.
<Component name="ContactForm">
<ModalDialog
id="dialog"
title="{$param ? 'Edit a Contact' : 'Add a new Contact'}">
<Text>Contact Form</Text>
</ModalDialog>
<method name="open" value="dialog.open" />
</Component>
When we edit data, the dialog receives a contact item. However, when we add a new contact, the dialog does not receive any parameters.
You can also use the $params
(plural form) value, an array of parameters (open
can be invoked with multiple parameters). In this case, you can use $params[0]
, $params[1]
, and so on to get a particular parameter's value. $param
is the same as $params[0]
.
Modify Contacts
to pass the contact item to edit when opening the dialog:
<Component name="ContactRow">
<DataSource id="categories" url="/api/categories" />
<ContactForm id="contactForm" />
<Card>
<!-- Unchanged -->
<DropdownMenu>
<property name="triggerTemplate">
<Button icon="dotmenu" variant="ghost" themeColor="secondary"/>
</property>
<MenuItem label="Edit" onClick="contactForm.open($item)"/>
<MenuItem label="Delete" />
</DropdownMenu>
</HStack>
</Card>
</Component>
Now, when you display the dialog clicking on the "Edit" menu, it will display the correct title:

Contact Data Form
The current contact dialog has no functionality. To flesh it out, create a form that accepts data entry.
Replace the Text
with the highlighted Form
in ContactForm
:
<Component name="ContactForm">
<ModalDialog
id="dialog"
title="{$param ? 'Edit a Contact' : 'Add a new Contact'}">
<Form
id="contactForm"
data="{$param}"
onSubmit="(toSave) => toast.success(JSON.stringify(toSave))">
<FormItem
label="Full name"
placeholder="Specify the contact name"
bindTo="fullName" autoFocus="true"
maxLength="64"
required="true" />
<FormItem label="Review due date" bindTo="reviewDueDate" type="datePicker" />
<FormItem
label="Comments"
placeholder="Add comments here"
bindTo="comments"
type="textarea"
maxLength="256" />
<DataSource id="categories" method="get" url="/api/categories"/>
<FormItem
placeholder="Select a category"
bindTo="categoryId"
initialValue="{$props.contact.categoryId}"
type="select">
<Items items="{categories.value}">
<Option label="{$item.name}" value="{$item.id}"/>
</Items>
</FormItem>
</Form>
</ModalDialog>
<method name="open" value="dialog.open" />
</Component>
The Form
component is responsible for editing the nested data items. Its data
property describes the data to edit; we pass the first parameter ($param
) that is passed to the open
function to show the dialog. Remember, when we add a new contact, this data is empty. However, the dialog gets the corresponding contact record when editing a contact.
Each FormItem
is an input field associated with an attribute of the original data
via the bindTo
property.
The FormItem
bound to the categoryId
property is more complex.
It displays a selection list and uses a DataSource
(categories
) to fetch this list's content (with IDs and names).
The initialValue
property of this FormItem
ensures that the selected category is displayed when you edit a contact.
In this case, ContactForm
will receive the data to edit in its contact
property.
You will add this functionality soon.
The submit
event handler of the Form
(defined with the onSubmit
attribute) defines the activity to execute when the form is saved. So far, this event handler does not persist the data; it just displays it in JSON.
When you run the app, adding a new contact displays an empty form.

Fill out the form and click the submit button. The current submit event handler displays the JSON representation of the data to save (it does not persist yet):

When you click the "Edit" context menu of a particular contact, the form displays the data of the record:

If you modify and save the contact data, you will see a message with the modified contact data that is similar to adding a new one.
Saving Data
You can edit the form data, but it must still be saved to the backend. You will use methods the backend provides for adding and editing a contact record:
- The
POST /api/contacts
method inserts a new data record - The
PUT /api/contacts/{contactId}
method updates the contact record with IDcontactId
- The
DELETE /api/contacts/{contactId}
method removes the contact record with IDcontactId
The first two methods accept the contact details in their request bodies; the DELETE method does not need a body.
Add and Edit a Contact Record
XMLUI provides an onSubmit
event handler to declare the submit action. Within this handler, you can invoke the appropriate API endpoint and do other activities related to saving the form's data. The Contacts
component uses this method to display the data to save.
Most of the time, submitting a form invokes a particular (REST) API endpoint with a POST or a PUT verb, and the data to save is in the request body. XMLUI offers two properties that help you apply this pattern quickly: ' submitUrl' and submitMethod
.
Remove the form's onSubmit
event handler and add the highlighted code to the form:
<Component name="ContactForm">
<ModalDialog
id="dialog"
title="{$param ? 'Edit a Contact' : 'Add a new Contact'}">
<Form
id="contactForm"
data="{$param}"
submitUrl="/api/contacts/{$param.id ?? ''}"
submitMethod="{$param ? 'put' : 'post'}">
<!-- Unchanged -->
</Component>
When you add a new item, the $param
value is undefined; according to the submiUrl and
submit methodproperties, the form will use the
/api/contacts/URL with the
POST` action.
However, when $param
holds a contact item, the same properties will drive the form to use /api/contacts/[id]
, where "[id]" is the contact identifier.
The form puts the edited data into the request body when submitting the request.
Refresh the app in the browser and go to the Today's Reviews page. Add a new contact with its review due date set to today. After saving it, the contact appears in the list:

Similarly, you can modify a contact. After the changes are saved, the modified contact data is refreshed in the list:

Delete a Contact Record
To delete a record, invoke the API endpoint directly from the context menu defined in Contacts
.
<Component name="Contacts">
<ContactForm id="contactForm" />
<DataSource id="categories" url="/api/categories" />
<!-- Unchanged -->
<DropdownMenu>
<property name="triggerTemplate">
<Button icon="dotmenu" variant="ghost" themeColor="secondary"/>
</property>
<MenuItem label="Edit" onClick="contactForm.open($item)"/>
<MenuItem label="Delete">
<event name="click">
<APICall
url="/api/contacts/{$item.id}"
method="delete"
confirmTitle="Delete Contact {$item.fullName}"
confirmMessage="Are you sure you want to delete this contact?"
confirmButtonLabel="Delete" />
</event>
</MenuItem>
</DropdownMenu>
</HStack>
</Card>
</List>
</VStack>
</Component>
In this code snippet, the event handler of the Delete command does not use a script or expression; it declares an APICall
component, which acts as an event handler by invoking the specified endpoint. According to the definition, the component will invoke a DELETE request using the /api/contacts/[id]
endpoint to remove the contact with the specified identifier.
It is good practice to ask the user for confirmation before deleting a particular record item. APICall
makes this confirmation as easy as setting a few properties, such as confirmTitle
, confirmMessage
, and ConfirmButtonLabel
. Before invoking the specified API endpoint, APICall
will display the confirmation dialog. If the user proceeds, the engine invokes the action; otherwise, it cancels it:

Now that you have implemented the core functionality of the contact management system, it is time to add a dashboard to the app.