Refactoring XMLUI: Extract Modal Components
When working with XMLUI applications, you may find yourself with complex components that handle multiple modals and interactions. This guide demonstrates how to factor out XMLUI code into reusable user-defined components that contain modals, improving code organization and maintainability.
The Pattern: Modal Components with Event Communication
The key pattern involves:
- Creating user-defined components that encapsulate modal logic
- Using props to pass data from parent to child components
- Using custom events to communicate back from child to parent
- Conditional rendering to control when components are displayed
Example: Extracting a User Modal Component
Before: Monolithic Component
<App var.modalOpenForUserId="{null}">
<DataSource id="users" url="/api/users"/>
<!-- Large table with inline modal logic -->
<ModalDialog id="all_users" title="All users">
<Table data="{users.value}">
<Column bindTo="name" header="name"/>
<Column header="Edit">
<Button icon="pen">
<event name="click">
modalOpenForUserId = $item.id
</event>
</Button>
</Column>
</Table>
</ModalDialog>
<!-- User editing modal mixed in with main component -->
<ModalDialog
when="{modalOpenForUserId}"
title="User {modalOpenForUserId}"
onClose="{modalOpenForUserId = null}"
>
<DataSource id="user" url="/api/users/{modalOpenForUserId}"/>
{JSON.stringify(user.value)}
</ModalDialog>
<Button onClick="{all_users.open()}" label="Show all users" />
</App>
After: Refactored with Component Extraction
Main.xmlui (Parent Component):
<App var.modalOpenForUserId="{null}">
<DataSource id="users" url="/api/users"/>
<ModalDialog id="all_users" title="All users">
<Table data="{users.value}">
<Column bindTo="name" header="name"/>
<Column header="Edit">
<Button icon="pen">
<event name="click">
modalOpenForUserId = $item.id
</event>
</Button>
</Column>
</Table>
</ModalDialog>
<Button onClick="{all_users.open()}" label="Show all users" />
<!-- Clean separation: User component handles its own modal -->
<User
when="{modalOpenForUserId}"
userId="{modalOpenForUserId}"
onClose="{modalOpenForUserId=null}"
/>
</App>
components/User.xmlui (Extracted Component):
<Component name="User">
<DataSource id="user" url="/api/users/{$props.userId}"/>
<ModalDialog
when="{user.loaded}"
title="User {$props.userId}"
onClose="{emitEvent('close')}"
>
{JSON.stringify(user.value)}
</ModalDialog>
</Component>
Key Concepts Explained
1. Props for Data Flow (Parent → Child)
<!-- Parent passes data via attributes -->
<User userId="{selectedUserId}" userRole="{currentRole}" />
<!-- Child accesses via $props -->
<Component name="User">
<DataSource id="user" url="/api/users/{$props.userId}"/>
<Text>Role: {$props.userRole}</Text>
</Component>
2. Custom Events for Communication (Child → Parent)
<!-- Parent listens for custom events -->
<User onClose="{modalOpenForUserId=null}" onSave="{refreshUserList()}" />
<!-- Child emits custom events -->
<Component name="User">
<Button onClick="{emitEvent('save')}" label="Save" />
<Button onClick="{emitEvent('close')}" label="Cancel" />
</Component>
Important: Event handlers like
onClose
are not callback props. They are event listeners that respond to custom events emitted by the child component using emitEvent()
.3. Conditional Rendering
<!-- Only render when needed -->
<User when="{modalOpenForUserId}" userId="{modalOpenForUserId}" />
<!-- Wait for data before showing modal -->
<ModalDialog when="{user.loaded}" title="User Details">
<!-- Modal content -->
</ModalDialog>
Advanced Example: Edit/Add Modal Component
This pattern works well for components that handle both adding and editing:
ProductModal.xmlui:
<Component name="ProductModal">
<!-- Fetch complete product data when editing -->
<DataSource
id="productDetails"
url="/api/products/{$props.productId}"
when="{$props.mode === 'edit' && $props.productId}"
/>
<ModalDialog
title="{$props.mode === 'edit' ? 'Edit Product' : 'Add Product'}"
when="{$props.mode === 'add' || productDetails.loaded}"
onClose="{emitEvent('close')}"
>
<Form
data="{$props.mode === 'edit' ? productDetails.value : $props.initialData}"
submitUrl="{$props.mode === 'edit' ? '/api/products/' + $props.productId : '/api/products'}"
submitMethod="{$props.mode === 'edit' ? 'put' : 'post'}"
onSuccess="{emitEvent('saved', $result)}"
>
<FormItem bindTo="name" label="Product Name" required="true" />
<FormItem bindTo="price" label="Price" type="number" required="true" />
<FormItem bindTo="description" label="Description" type="textarea" />
<FormItem bindTo="category" label="Category" />
<FormItem bindTo="inStock" label="In Stock" type="checkbox" />
</Form>
</ModalDialog>
</Component>
Usage:
<!-- Add mode -->
<ProductModal
when="{showAddModal}"
mode="add"
initialData="{{}}"
onClose="{showAddModal=false}"
onSaved="{handleProductSaved}"
/>
<!-- Edit mode - pass only the product ID, let the modal fetch complete data -->
<ProductModal
when="{editingProductId}"
mode="edit"
productId="{editingProductId}"
onClose="{editingProductId=null}"
onSaved="{handleProductSaved}"
/>
Parent component example:
<App var.editingProductId="{null}" var.showAddModal="{false}">
<!-- Minimal product listing - only shows essential fields -->
<DataSource id="products" url="/api/products" />
<Table data="{products.value}">
<Column bindTo="name" header="Product Name" />
<Column bindTo="price" header="Price" />
<Column header="Actions">
<Button
label="Edit"
onClick="{editingProductId = $item.id}"
/>
</Column>
</Table>
<Button
label="Add Product"
onClick="{showAddModal = true}"
/>
<!-- Modal components handle their own data fetching -->
<ProductModal
when="{showAddModal}"
mode="add"
initialData="{{}}"
onClose="{showAddModal=false}"
onSaved="{products.refresh()}"
/>
<ProductModal
when="{editingProductId}"
mode="edit"
productId="{editingProductId}"
onClose="{editingProductId=null}"
onSaved="{products.refresh()}"
/>
</App>
Benefits of This Pattern
- Separation of Concerns: Each component has a single responsibility
- Reusability: Modal components can be used in multiple places
- Maintainability: Easier to modify and test individual components
- Readability: Parent component focuses on coordination, not implementation details
- Data Flow Clarity: Props flow down, events flow up
When to Extract Modal Components
Consider extracting when:
- Modal logic becomes complex (validation, multiple steps, etc.)
- The same modal is used in multiple places
- Parent component becomes difficult to read
- Modal needs its own state management
- You want to unit test modal behavior separately
Common Pitfalls
- Don't forget
when
conditions: Always use conditional rendering to avoid unnecessary data fetching - Remember
emitEvent()
for communication: Child components cannot directly modify parent variables - Use
$props
prefix: Access passed data with$props.propertyName
- Handle loading states: Wait for data to load before showing modals (
when="{data.loaded}"
)
This refactoring pattern helps create maintainable, reusable XMLUI applications with clear data flow and component boundaries.