Data and forms

Fetching data from a server

Fetch data, submit a form, delete a record. Normally that turns into fetch, then, HTML assembled by hand and error handling repeated on every screen. In Voodoo it is one attribute.

About the examples on this page

The blocks here do not run live the way they do on the other pages: they would need a real server on the other side. The URLs are made up — swap them for addresses on your own API.

The five verbs

<button v-get="/api/users" v-target="#list">Load</button>

<div id="list"></div>

There are v-get, v-post, v-put, v-patch and v-delete. Each element has a natural trigger: a <form> fires on submit, a field fires on change, and everything else fires on click.

The URL can be fixed or an expression:

<button v-delete="'/api/users/' + u.id"
        v-confirm="Delete user?"
        v-toast-success="User deleted!">Delete</button>

What happens to the response

If the server returns HTML, it goes straight into the target. If it returns JSON, Voodoo turns it into readable HTML: a list of objects becomes a table, an object becomes a definition list, and every value is escaped along the way.

If you would rather control the shape, point at a <template> on the page itself:

<button v-get="/api/users" v-target="#list" v-template="#row">Load</button>

<template id="row">
  <li><strong>{ name }</strong> <small>{ email }</small></li>
</template>

<ul id="list"></ul>

And if you want no HTML at all, only the data in state, v-as writes into the variable and the rest of the page reacts the way it would to any other change:

<div v-data="{ users: [] }">
  <button v-get="/api/users" v-as="users">Load</button>
  <ul>
    <li v-for="u in users" :key="u.id">{ u.name }</li>
  </ul>
</div>

Where the result is placed

v-target is the selector of the element that receives it. Without it, the target is the element itself. v-swap decides how:

<button v-get="/api/comments?page=2" v-target="#comments" v-swap="append">
  Load more
</button>

Accepted modes: innerHTML (the default), outerHTML, replace, textContent, beforebegin, afterbegin, beforeend, afterend, append, prepend, delete and none.

Other triggers

<div v-get="/api/status" v-poll="5s" v-target="#status"></div>
<div v-load-visible="/api/comments"></div>
<input v-search="/api/products" v-param="q" v-target="#results" v-debounce="300">
<div v-get="/api/banner" v-trigger="visible"></div>

Loading, error, and what to do afterwards

<button v-get="/api/report"
        v-target="#output"
        v-loading="#spinner"
        v-disable-loading
        v-on-success="$toast.success('Done!')"
        v-on-error="console.warn($detail)">
  Generate
</button>

<div id="spinner">Generating the report...</div>

While the request is in flight the element gets the class v-loading and the attribute aria-busy="true" — both give CSS and screen readers a hook with no extra code. A new request from the same element cancels the previous one if it is still pending, which settles the repeated click on its own.

v-resource: data, loading and error in one line

For when you want the request state available in the HTML, and not only the result:

<div v-resource="products: /api/products">

  <p v-if="products.loading">Loading...</p>
  <p v-else-if="products.error">Failed: { products.error.message }</p>

  <ul v-else>
    <li v-for="p in products.data" :key="p.id">{ p.name }</li>
  </ul>

  <button @click="products.reload()">Refresh</button>
</div>

The syntax is name: url. The object it creates has data, loading, error, loaded, reload() and set(). It accepts v-method, v-params, v-cache, v-retry, v-timeout, v-json-path, v-poll and v-manual, which holds the first fetch back until you call reload().

Supporting attributes

AttributeWhat it is for
v-bodyThe request body, as an object or an expression
v-paramsQuery parameters
v-headersExtra headers
v-cacheKeeps the response for the time given
v-retryTries again when it fails
v-timeoutGives up after the time given
v-json-pathTakes one piece out of the response, such as data.items
v-redirectNavigates to a URL when it succeeds
v-scroll-toScrolls to an element after the swap
v-loading-classThe class applied during the request
v-on-completeAn expression run at the end, success or not
v-offline-queueHolds the request while there is no network and sends it when the connection is back

The same thing, in JavaScript

const users = await V.http.get('/api/users', { params: { page: 2 }, cache: 60000 });
await V.http.post('/api/users', { name: 'Ann' });
await V.http.upload('/api/files', formData, { onProgress: (p) => console.log(p) });

V.http.setBaseURL('https://api.example.com');
V.http.setToken('my-jwt');
V.http.interceptors.response.use((r) => r);

The full surface — interceptors, cache, cancellation, HttpError and upload with progress — is in the HTTP client reference.

CSRF with nothing to configure

Methods that write carry the token from <meta name="csrf-token"> in the X-CSRF-TOKEN header automatically. If your server uses another header name, adjust it with a request interceptor.

A known detail

v-confirm on the same element as an HTTP verb asks twice. Use v-confirm alongside v-click, or ask for the confirmation inside the expression itself with $confirm(...).