Learn every core feature of xhtmlx by building a fully functional Task Manager app, one step at a time. Each step below contains live, working code.
The most fundamental thing xhtmlx does is fetch JSON from an API and render it into the page. Add xh-get to any element to tell it which URL to request, and xh-trigger="load" to fire it automatically when the page loads.
Inside the element, place a <template> with xh-each to loop over the array in the response. Each item gets rendered as a repeated element.
<!-- Fetch tasks on page load and render each one -->
<div xh-get="/api/tasks" xh-trigger="load">
<template>
<ul class="task-list">
<li xh-each="tasks" class="task-item">
<span xh-text="title" class="task-title"></span>
</li>
</ul>
</template>
</div>
xh-trigger="load" attribute tells xhtmlx to fire the request as soon as the element is processed. Without it, the default trigger for a <div> is click.
Use xh-text to set an element's text content from a data field. Use xh-attr-* to set any HTML attribute dynamically -- for example, xh-attr-href sets the href on a link.
Here we display each task's title, description, and due date, plus a link to the task detail page.
<div xh-get="/api/tasks" xh-trigger="load">
<template>
<ul class="task-list">
<li xh-each="tasks" class="task-item">
<div>
<a xh-attr-href="link" xh-text="title" class="task-title"></a>
<div xh-text="description" class="task-desc"></div>
</div>
<span xh-text="due" class="task-due"></span>
</li>
</ul>
</template>
</div>
xh-attr-* works with any attribute: xh-attr-src, xh-attr-alt, xh-attr-title, xh-attr-data-id, etc.
Use xh-if to show an element only when a data field is truthy, and xh-unless to show it only when a field is falsy. Here we display a "Done" badge for completed tasks and a "Pending" badge for tasks that are not yet completed.
<div xh-get="/api/tasks" xh-trigger="load">
<template>
<ul class="task-list">
<li xh-each="tasks" class="task-item">
<span xh-if="completed" class="badge badge-done">Done</span>
<span xh-unless="completed" class="badge badge-pending">Pending</span>
<span xh-text="title" class="task-title"></span>
<span xh-text="due" class="task-due"></span>
</li>
</ul>
</template>
</div>
xh-if evaluates to false, the element is completely removed from the DOM -- not just hidden. This is different from xh-show/xh-hide, which toggle CSS visibility.
To send data to the server, use xh-post on a <form>. Form fields are automatically serialized and sent as the request body. The xh-target attribute tells xhtmlx where to put the response, and xh-swap="beforeend" appends the new task to the existing list instead of replacing it.
<!-- Form to add a new task -->
<form xh-post="/api/tasks"
xh-target="#task-list-4"
xh-swap="beforeend">
<template>
<li class="task-item">
<span class="badge badge-pending">Pending</span>
<span xh-text="title" class="task-title"></span>
<span xh-text="due" class="task-due"></span>
</li>
</template>
<input type="text" name="title" placeholder="Task title..." required>
<input type="date" name="due">
<button type="submit" class="btn-small btn-primary-small">Add Task</button>
</form>
<!-- Existing task list loaded on page load -->
<div xh-get="/api/tasks" xh-trigger="load" xh-target="#task-list-4">
<template>
<li xh-each="tasks" class="task-item">
<span xh-if="completed" class="badge badge-done">Done</span>
<span xh-unless="completed" class="badge badge-pending">Pending</span>
<span xh-text="title" class="task-title"></span>
<span xh-text="due" class="task-due"></span>
</li>
</template>
</div>
<ul id="task-list-4" class="task-list"></ul>
<input>, <select>, <textarea>) into the request body. You can also use xh-vals to send additional JSON data alongside form fields.
Use xh-patch to send a PATCH request. URL interpolation with {{id}} inserts the task's ID from the current data context. The xh-vals attribute sends JSON data with the request. Here, clicking a button marks a task as complete.
<div xh-get="/api/tasks" xh-trigger="load">
<template>
<ul class="task-list">
<li xh-each="tasks" class="task-item">
<span xh-if="completed" class="badge badge-done">Done</span>
<span xh-unless="completed" class="badge badge-pending">Pending</span>
<span xh-text="title" class="task-title"></span>
<div class="task-actions">
<button xh-unless="completed"
xh-patch="/api/tasks/{{id}}"
xh-vals='{"completed": true}'
xh-swap="none"
class="btn-small btn-success-small">
Complete
</button>
</div>
</li>
</ul>
</template>
</div>
xh-swap="none" means "fire and forget" -- the request is sent but no DOM swap happens. This is useful for actions where you do not need to replace content (e.g., toggling a flag). Use xh-vals to send any JSON payload.
Use xh-delete to send a DELETE request and xh-swap="delete" to remove the element from the DOM after a successful response. We use xh-target="closest .task-item" to target the parent list item for removal.
<div xh-get="/api/tasks" xh-trigger="load">
<template>
<ul class="task-list">
<li xh-each="tasks" class="task-item">
<span xh-text="title" class="task-title"></span>
<span xh-text="due" class="task-due"></span>
<button xh-delete="/api/tasks/{{id}}"
xh-target="closest .task-item"
xh-swap="delete"
class="btn-small btn-danger-small">
Delete
</button>
</li>
</ul>
</template>
</div>
xh-swap="delete" mode removes the target element entirely. Combined with xh-target="closest .task-item", it walks up the DOM to find the nearest matching ancestor and removes it.
Use xh-trigger="keyup changed delay:300ms" on an input to debounce the request. The changed modifier ensures we only fire when the value actually changes, and delay:300ms waits 300ms after the last keystroke. The input's value is automatically included as a query parameter.
<input type="text"
name="q"
placeholder="Search tasks..."
class="search-box"
xh-get="/api/tasks"
xh-trigger="keyup changed delay:300ms"
xh-target="#search-results-7">
<template>
<ul class="task-list">
<li xh-each="tasks" class="task-item">
<span xh-text="title" class="task-title"></span>
<span xh-text="description" class="task-desc"></span>
</li>
</ul>
</template>
</input>
<div id="search-results-7"></div>
keyup changed delay:300ms fires on keyup, but only if the value changed, and only after a 300ms pause. You can also use throttle:500ms to limit the rate of requests.
Use the xh-indicator attribute to point to an element that should be shown while a request is in flight. xhtmlx will add the xh-request class to the indicator element during loading, and the built-in CSS hides/shows elements with the xh-indicator class based on that.
<button xh-get="/api/tasks"
xh-target="#indicator-results-8"
xh-indicator="#loading-8"
class="btn-small btn-primary-small">
Load Tasks
</button>
<span id="loading-8" class="xh-indicator">
<span class="spinner"></span> Loading...
</span>
<template>
<ul class="task-list">
<li xh-each="tasks" class="task-item">
<span xh-text="title" class="task-title"></span>
</li>
</ul>
</template>
<div id="indicator-results-8"></div>
xh-indicator class is hidden by default and shown when its parent (or the element pointed to by xh-indicator) receives the xh-request class. You can customize the loading animation with CSS.
When an API returns an error status, xhtmlx can render a dedicated error template. Use xh-error-template with an inline <template> to define what to show on failure. The error data context includes status, statusText, and body.
You can also wrap multiple widgets in an xh-error-boundary to catch errors from any child that does not have its own error template.
<!-- This request hits an endpoint that returns 500 -->
<button xh-get="/api/error/500"
xh-target="#error-output-9"
xh-error-target="#error-output-9"
class="btn-small btn-danger-small">
Trigger Server Error
<template>
<div>This won't show because the request fails.</div>
</template>
<template xh-error-template>
<div class="error-box">
<strong>Error <span xh-text="status"></span></strong>
— <span xh-text="body.message"></span>
</div>
</template>
</button>
<div id="error-output-9"></div>
xh-error-template-404 or xh-error-template-4xx for fine-grained control. Error boundaries (xh-error-boundary) catch errors from child widgets that do not have their own error template.