Coding ยท ยท

Advanced Petite-vue

This assumes you've gone through the interactive guide and tried some things out with petite-vue already.

Reactivity with proxies

Part of the magic sauce behind petite-vue is the use of JavaScript proxies. These enable petite-vue to know when properties of an object have been edited/mutated, and respond to those changes by re-rendering a part of the DOM.

You may have found in the interactive guide that logging or changing variables in the console didn't always do what you expected. Given this code:

<script src="https://unpkg.com/petite-vue" defer init></script>

<div v-scope="{ state }">
  <select v-model="state.selected">
    <option v-for="opt in state.options">{{ opt }}</option>
  </select>
  <p>Selected is {{ state.selected }}</p>
</div>

<script>
let state = {
  options: ['red', 'green', 'blue'],
  selected: 'green'
}
</script>

... as you change the select box to a new value, the word below changes to the selected value too. You can also console.log(state) and you'll see the new value in there too.

But try mutating state with state.selected = 'red' and... nothing happens. Try adding an option with state.options.push('pink'), and again nothing happens.

The reason it doesn't work is because petite-vue wraps your object in a proxy when you pass it to v-scope. But there's no easy way to access that proxy from the console (or elsewhere in our code), unless we set up that proxy ourselves instead:

<script src="https://unpkg.com/petite-vue"></script>

<div v-scope>
  <select v-model="state.selected">
    <option v-for="opt in state.options">{{ opt }}</option>
  </select>
  <p>Selected is {{ state.selected }}</p>
</div>

<script>
let state = PetiteVue.reactive({
  options: ['red', 'green', 'blue'],
  selected: 'green'
})
PetiteVue.createApp().mount()
</script>

There are a few things that have changed here:

And what was the point? Well, now state is a proxy to an object rather than an object itself. We can still use it like a normal object and console.log(state.selected), but now we can also state.selected = 'red' and the value in the <select> box and the text below instantly changes. Want a new option? state.options.push('pink'), which again wouldn't work before.

If you're like me and you like to try things out in the console and access your state anywhere in your code, I highly recommend create your state objects this way instead, by explicitly wrapping them in proxies.

Todo list with accessible proxy objects

Here's the todo list from the interactive guide, this time with proxy objects we can access directly:

<script src="https://unpkg.com/petite-vue"></script>

<div v-scope>
  <form @submit.prevent="state.add">
    <input v-model="state.item">
    <button>Add</button>
  </form>
  <ul v-if="state.todos.length">
    <li v-for="todo, i of state.todos" @click="state.del(i)">{{ todo }}</li>
  </ul>
  <p v-else>No items yet</p>
</div>

<script>
let state = PetiteVue.reactive({
  todos: [],
  item: '',
  add () {
    this.todos.push(this.item)
    this.item = ''
  },
  del (i) {
    this.todos.splice(i, 1)
  }
})
PetiteVue.createApp().mount()
</script>

... and everything is fully editable anywhere in your code, including from the console, eg:

You may decide the add() and del() methods don't need to be wrapped up in the proxy, and could be separated out like this:

<script src="https://unpkg.com/petite-vue"></script>

<div v-scope>
  <form @submit.prevent="add">
    <input v-model="state.item">
    <button>Add</button>
  </form>
  <ul v-if="state.todos.length">
    <li v-for="todo, i of state.todos" @click="del(i)">{{ todo }}</li>
  </ul>
  <p v-else>No items yet</p>
</div>

<script>
let state = PetiteVue.reactive({
  todos: [],
  item: ''
})

function add () {
  state.todos.push(state.item)
  state.item = ''
}

function del (i) {
  state.todos.splice(i, 1)
}

PetiteVue.createApp().mount()
</script>

... now state just holds variables/data rather than functions/methods, and those functions needed to be tweaked slightly to work on state rather than this.

Ultimately, which of these variations you decided to code in is up to your preferred style, and the context in which you're building your web app.

Building a single page app (SPA)

There's nothing to stop you having multiple HTML pages, each with petite-vue sprinkled around the place. But many web apps these days are built as SPAs, meaning you have a single HTML file that behaves as though it's several pages. I'm going to use the style introduced at the end of the last section, with a global state and standalone functions.

<script src="https://unpkg.com/petite-vue"></script>

<div v-scope>
  <ul>
    <li v-for="num of [1, 2, 3, 4]">
      <a href="" @click.prevent="state.page = num">Page {{ num }}</a>
    </li>
  </ul>
  <p>You are on page {{ state.page }}</p>
</div>

<script>
let state = PetiteVue.reactive({ page: 1 })
PetiteVue.createApp().mount()
</script>

Instead of <a href="1.html"> we have <a href="" @click.prevent="state.page = num">. The .prevent ensures the browser doesn't navigate away when clicked, and instead we run some JavaScript to change the value of state.page.

Of course, because state is a proxy, we can also do state.page = 1 anywhere in our code (or in the console).

It's a bit odd to link to the page you're currently on, so that can easily be fixed:

<a v-if="state.page != num" href="#" @click="state.page = num">Page {{ num }}</a>
<span v-else>Page {{ num }}</span>

Similar use of v-if can be used to actually show the correct page's content:

<div v-if="state.page === 1">
  <h1>Page 1 - story</h1>
  <p>Once upon a time...</p>
</div>
<div v-if="state.page === 2">
  <h1>Page 2 - best stuff</h1>
  <p>The best way to...</p>
</div>

But annoyingly, if you refresh the page it always shows page 1 rather than the page you were just on. We can fix that by appending the page number to the URL after a hash #.

We'll make the click handler call a function instead:

@click.prevent="changePage(num)"

Then in that function change the url hash #:

function changePage (num) {
  state.page = num
  location.hash = num
}

And when we first load the page, see if there's any value in the hash and use that for the initial value of state.page:

let state = PetiteVue.reactive({
  page: location.hash.replace('#', '') || 1
})

If you refresh the page, you'll now notice it shows the correct content.

There are better and more modern ways of doing this involving pushState() which allow you to drop the # from the URL, but they're beyond the scope of this guide.

Of course we've used simple numbers for our pages, but there's nothing to stop you using strings for your page name.