Build a Task Manager with xhtmlx

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.

Step 1

Your First Request (GET)

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.

Live Demo
<!-- 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>
Tip: The 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.
Step 2

Data Binding

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.

Live Demo
<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>
Tip: xh-attr-* works with any attribute: xh-attr-src, xh-attr-alt, xh-attr-title, xh-attr-data-id, etc.
Step 3

Conditionals

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.

Live Demo
<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>
Tip: When 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.
Step 4

Creating Tasks (POST)

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.

Live Demo
<!-- 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>
Tip: xhtmlx automatically serializes all form fields (<input>, <select>, <textarea>) into the request body. You can also use xh-vals to send additional JSON data alongside form fields.
Step 5

Completing Tasks (PATCH)

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.

Live Demo
<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>
Tip: 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.
Step 6

Deleting Tasks (DELETE)

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.

Live Demo
<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>
Tip: The 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.
Step 7

Search with Debounce

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.

Live Demo
<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>
Tip: Trigger modifiers can be combined. 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.
Step 8

Loading Indicators

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.

Live Demo
<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>
Tip: Any element with the 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.
Step 9

Error Handling

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.

Live Demo
<!-- 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>
Tip: You can use status-specific error templates like 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.