Examples

Walkthrough examples of how to use Vivere

Counter

A simple counter, using an anonymous Vivere component. We define a data property called count, tell our <p> to display the (reactive) value of the count, and hook up listeners on the buttons that mutate the component.

<!-- Initialize a Vivere component -->
<!-- Create a property called count, initialized to 0 -->
<div
v-component
v-data:count="0"
>

<!-- Register click listener to decrement count -->
<button v-event:click="count -= 1"> - </button>

<!-- Display the count -->
<p v-text="count"></p>

<!-- Register click listener to increment count -->
<button v-event:click="count += 1"> + </button>
</div>
// No javascript required!

Fancy Counter

Now, let's upgrade our counter and show what's possible when we name our component, and define it in Javascript. Here we're going to use a computed property to render a string with our count, add more logic to our buttons, and use methods instead of expressions to update our count.

<!-- Initialize a named Vivere component -->
<div v-component="fancy-counter">
<!-- Register click listener to decrement count -->
<!-- Conditionally disable the button -->
<button
v-event:click="decrementCount"
v-disabled="!canDecrement"
>
- </button>

<!-- Display the count via a computed property -->
<!-- Conditionally add a class to the element -->
<p
v-text="countString"
v-class:text-red="countHigh"
>
</p>

<!-- Increment the count inline -->
<button v-event:click="count += 1"> + </button>
</div>
class FancyCounter extends VivereComponent {
// Initialize a data attribute called count
count = 0;

get canDecrement() {
return this.count > 0;
}

// Define a high count as >= 5
get countHigh() {
return this.count >= 5;
}

// Make our display string particularly fun
get countString() {
const { count, countHigh } = this;

if (countHigh)
return `Wow! Your count is ${count}!`;
return `Your count is ${count}`;
}

// Use a method to decrement the count, but
// don't allow it if our count is already 0
decrementCount() {
if (this.canDecrement)
this.count -= 1;
}
};
Vivere.register('FancyCounter', FancyCounter);

Filterable List

We can use a v-if directive to conditionally render elements in a list. Instead of something like Vue's v-for to create lists, we'll rely on rendering the whole list from the server, and use individual directives to control which elements appear. We can also use a v-data directive pass JSON data from the server to the individual list items, and filter them based on that data.

This is a great opportunity to use something like partials from Rails to make our components as re-usable as possible.

Filter:

<!-- Our list component controls whether we're currently `filtering` -->
<div
v-component
v-data:filtering="false"
>

<!-- Buttons that toggle our filter on or off -->
<button v-event:click="filtering = false"> Off </button>
<button v-event:click="filtering = true"> On </button>
<ul>
<!-- A set of duplicate list item components -->
<!-- · Each passed JSON data from the server -->
<!-- · But with identical v-if directives -->
<!-- · And can read the list's `filtering` property -->

<li
v-component="list-item"
v-data:record='{ "id": 1, "title": "Item A" }'
v-pass:filtering
v-if="shouldShow"
v-text="record.title"
>
</li>

. . .

</ul>
</div>
class ListItem extends VivereComponent {
// While not strictly necessary, it can be nice to
// call out that we have a property named record
record = null;

// Inherit the `filtering` property from
// this component's parent
$passed = {
filtering: { default: false },
};

// Always show if we're not filtering, otherwise
// only show records with odd id's
get shouldShow() {
const { filtering, record } = this;
if (filtering) return record.id % 2 !== 0;
return true;
}
};
Vivere.register('ListItem', ListItem);

To Do List

Putting together all of our component magic let's us set up very interactive and involved interface. Here we have a list of to-do's generated from a server, with the ability to check them off, delete them, edit them, or create a new one.

#

<!-- Our list component manages the search bar and dropdown -->
<div v-component="list">

<!-- Our search bar text is sycned to the `filterText` data property -->
<input
v-sync="filterText"
type="text"
placeholder="Search to-dos..."
/>


<!-- Our sorting drop down is synced to the `sortMode` data property -->
<select v-sync="sortMode">
<option value="idAsc">id: 1-9</option>
<option value="idDesc">id: 9-1</option>
<option value="titleAsc">title: A-Z</option>
<option value="titleDesc">title: Z-A</option>
</select>

<!-- v-for will instantly duplicate this element and create separate -->
<!-- components with separate data (see below!) -->
<list-item></list-item>

<!-- The create button reveals the ability to create new to-dos -->
<div>
<button>Create New To-Do</button>
</div>
</div>
import axios from 'axios';

class List extends VivereComponent {
// This will track what's been typed into
// our text `input` search field
filterText = null;

// This will track whichever sort value
// we've selected in our `select`
sortMode = null;

// Tracks whether we're trying to create
// a new to do
creating = false;

// Tracks the title of the new to do
// from our text `input`
title = null;

// A computed property that will be
// passed to v-sort to control how
// we will be sorting
get orderBy() {
const { sortMode } = this;

switch (sortMode) {
case 'idAsc':
return ['id', true];
case 'idDesc':
return ['id', false];
case 'titleAsc':
return ['title', true];
case 'titleDesc':
return ['title', false];
default:
return null;
}
}

// Computed property that controls
// which items we want to render and
// in what order
get renderedItems() {
const { items, orderBy, filterText } = this;
const [key, ascending] = orderBy;

return items
.filter((i) => i.title.includes(filterText))
.toSorted((a,b) => a[key] > b[key] ? (ascending ? 1 : -1) : (ascending ? -1 : 1))
}

get validTitle() {
const { title } = this;
return !!title?.trim();
}

// This watcher will automatically trigger
// when `creating` is toggled
onCreatingChanged() {
if (this.creating) {
// Reset the title, so any past
// unsaved edits, are ignored
this.title = null;

// We want to focus on the title input automatically,
// but we need to wait until `$nextRender` to ensure
// that it has been added to the DOM
this.$nextRender(() => { this.$refs.title.focus(); });
}
}

create() {
const { title, validTitle } = this;
if (!validTitle) return;

// What we'd probably do here is post to the server
// and receive the new item as JSON
const item = axios.post('/to-dos', { title });

// We want to add our new item to the list to be rendered,
// v-for will take care of that
this.items.push(item);

this.creating = false;
}

// Method for removing an item from our list
deleteItem(id) {
this.items = this.items
.filter((i) => i.id === id)
}
};
Vivere.register('List', List);
<!-- Our individual list item (hopefully a reusable partial) -->
<!-- that manages the state of a to-do item -->
<div
v-for="toDo of renderedItems"
v-component="list-item"
v-bind:delete="deleteItem"
>


<!-- Each state has it's own "view" -->
<!-- · The show state reveals the title and completion status -->
<div v-if="state === 'show'">
<input
type="checkbox"
v-sync="toDo.complete"
>

<p>#<span v-text="toDo.id"></span></p>
<!-- We use v-text here instead of directly rendering -->
<!-- the tile from the server since the title can change -->
<p
v-text="toDo.title"
v-class:line-through.italic.text-gray="toDo.complete"
>
</p>
<!-- Switch to edit mode (not allowed once we've completed the to-do) -->
<button
v-event:click="state = 'edit'"
v-disabled="toDo.complete"
>
Edit...</button>
<!-- Switch to delete mode (not allowed once we've completed the to-do) -->
<button
v-event:click="state = 'delete'"
v-disabled="toDo.complete"
>
Delete...</button>
</div>

<!-- · The edit state allows us to update the title -->
<div v-if="state === 'edit'">
<input
v-sync="title"
v-ref="title"
v-event:keydown.enter="saveTitle"
v-event:keydown.escape="state = 'show'"
type="text"
placeholder="Choose a title..."
/>

<button
v-event:click="saveTitle"
v-disabled="!validTitle"
>
Save</button>
<button v-event:click="state = 'show'">Cancel</button>
</div>

<!-- · The delete state allows us to delete the to-do -->
<div v-if="state === 'delete'">
<p>Are you sure you want to delete this to-do?</p>
<button v-event:click="delete">Delete</button>
<button v-event:click="state = 'show'">Cancel</button>
</div>
</div>
class ListItem extends VivereComponent {
// While not strictly necessary, it can be nice to
// call out that we have a property named toDo
toDo = {};

// `title` is synced to our `input` while editing
title = null;

// `state` controls which view we see
state = 'show';

// We don't want to allow saving a blank
// title when editing the toDo
get validTitle() {
const { title } = this;
return !!title?.trim();
}

// This watcher will automatically trigger
// when the state changes
onStateChanged() {
if (this.state === 'edit') {
// Reset the title, so any past
// unsaved edits, are ignored
this.title = this.toDo.title;

// We want to focus on the title input automatically,
// but we need to wait until `$nextRender` to ensure
// that it has been added to the DOM
this.$nextRender(() => { this.$refs.title.focus(); })
}
}

saveTitle() {
if (this.validTitle) {
this.toDo.title = this.title;
this.state = 'show';
}
}

delete() {
// Tell our parent to remove this item
// from the list by emitting an event
this.$emit('delete', this.toDo.id);
}
};
Vivere.register('ListItem', ListItem);