Decisions are often made against migrating to an SPA
How to apply Progressive Enhancement?
const form = document.querySelector('#taskform') const inputElement = document.querySelector('#taskform input') form.addEventListener('submit', (event) => { event.preventDefault() const input = inputElement.value const isEmpty = !input.length if (isEmpty) { alert("Please Enter a Task") return } createTask(input) initRemoveForDeleteButtons() clearInput() }) function createTask(input) { document.querySelector('#tasks').innerHTML += ` <div class="task"> <span class="taskname"> ${input} </span> <button class="delete"> Remove </button> </div> ` } function initRemoveForDeleteButtons() { const currentTasks = document.querySelectorAll(".delete") Array.from(currentTasks).forEach(task => { task.onclick = function () { this.parentNode.remove() } }) } function clearInput() { inputElement.value = "" }
const form = document.querySelector('#taskform') const inputElement = document.querySelector('#taskform input') form.addEventListener('submit', (event) => { event.preventDefault() const input = inputElement.value const isEmpty = !input.length if (isEmpty) { alert("Please Enter a Task") return } createTask(input) initRemoveForDeleteButtons() clearInput() }) function createTask(input) { document.querySelector('#tasks').innerHTML += ` <div class="task"> <span class="taskname"> ${input} </span> <button class="delete"> Remove </button> </div> ` } function initRemoveForDeleteButtons() { const currentTasks = document.querySelectorAll(".delete") Array.from(currentTasks).forEach(task => { task.onclick = function () { this.parentNode.remove() } }) } function clearInput() { inputElement.value = "" }
<!DOCTYPE html> <html> <head> <script src="main.js" defer></script> </head> <body> <div class="container"> <h1>Todo List</h1> <form id="taskform"> <input type="text" placeholder="Enter Task"> <button type="submit">Add</button> </form> <div id="tasks"></div> </div> </body> </html>
<!DOCTYPE html> <html> <head> <script src="main.js" defer></script> </head> <body> <div class="container"> <h1>Todo List</h1> <form id="taskform"> <input type="text" placeholder="Enter Task"> <button type="submit">Add</button> </form> <div id="tasks"></div> </div> </body> </html>
Don't be like Jason (@JSONTheDev)!
petite-vue
@vue/reactivity
<script lang="ts" setup> import { ref } from 'vue' const count = ref(0) </script> <template> <div> Count: {{ count }} <button @click="count++"> Increment </button> </div> </template>
<script lang="ts" setup> import { ref } from 'vue' const count = ref(0) </script> <template> <div> Count: {{ count }} <button @click="count++"> Increment </button> </div> </template>
Move the template into the HTML file<body> <div> Count: {{ count }} <button @click="count++"> Increment </button> </div> </body>
<body> <div> Count: {{ count }} <button @click="count++"> Increment </button> </div> </body>
Add v-scope directive<body> <div v-scope="{ count: 0 }"> Count: {{ count }} <button @click="count++"> Increment </button> </div> </body>
<body> <div v-scope="{ count: 0 }"> Count: {{ count }} <button @click="count++"> Increment </button> </div> </body>
Import petite-vue<script src="https://unpkg.com/petite-vue" defer init></script>
<script src="https://unpkg.com/petite-vue" defer init></script>
<script src="https://unpkg.com/petite-vue" defer init></script> <div v-scope="{ count: 0 }"> Count: {{ count }} <button @click="count++"> Increment </button> </div>
<script src="https://unpkg.com/petite-vue" defer init></script> <div v-scope="{ count: 0 }"> Count: {{ count }} <button @click="count++"> Increment </button> </div>
petite-vue
, we can set the init
attribute so it will initialize automaticallyv-scope
directive. It will be accessible for all descendant elements<div v-scope="{ count: 0 }"> Count: {{ count }} <button @click="count++"> Increment </button> </div> <script src="https://unpkg.com/petite-vue"></script> <script> PetiteVue .createApp() .mount() </script>
<div v-scope="{ count: 0 }"> Count: {{ count }} <button @click="count++"> Increment </button> </div> <script src="https://unpkg.com/petite-vue"></script> <script> PetiteVue .createApp() .mount() </script>
<div v-scope="{ count: 0 }"> Count: {{ count }} <button @click="count++"> Increment </button> </div> <script type="module"> import { createApp } from 'https://unpkg.com/petite-vue?module' createApp().mount() </script>
<div v-scope="{ count: 0 }"> Count: {{ count }} <button @click="count++"> Increment </button> </div> <script type="module"> import { createApp } from 'https://unpkg.com/petite-vue?module' createApp().mount() </script>
init
, we have to call the createApp
and mount
manually<div> <div> <h1>Welcome to my app!</h1> <h2>{{ count }} won't render here</h2> <h3>Because petite-vue did not mounted here</h3> </div> <div v-scope="{ count: 0 }" id="counter"> Count: {{ count }} <button @click="count++"> Increment </button> </div> </div> <script type="module"> import { createApp } from 'https://unpkg.com/petite-vue?module' createApp().mount('#counter') </script>
<div> <div> <h1>Welcome to my app!</h1> <h2>{{ count }} won't render here</h2> <h3>Because petite-vue did not mounted here</h3> </div> <div v-scope="{ count: 0 }" id="counter"> Count: {{ count }} <button @click="count++"> Increment </button> </div> </div> <script type="module"> import { createApp } from 'https://unpkg.com/petite-vue?module' createApp().mount('#counter') </script>
mount
call petite-vue
createApp
as root scope<div v-scope> Count: {{ count }} </div> <div v-scope> <button @click="increment"> Increment {{ count }} by one </button> </div> <script type="module"> import { createApp } from 'https://unpkg.com/petite-vue?module' createApp({ count: 0, increment() { this.count++ } }).mount() </script>
<div v-scope> Count: {{ count }} </div> <div v-scope> <button @click="increment"> Increment {{ count }} by one </button> </div> <script type="module"> import { createApp } from 'https://unpkg.com/petite-vue?module' createApp({ count: 0, increment() { this.count++ } }).mount() </script>
createApp
functionpetite-vue
petite-vue
<div v-scope> Count: {{ count }} Digit sum {{ digitSum }} <button @click="count++"> Increment </button> </div> <script type="module"> import { createApp } from 'https://unpkg.com/petite-vue?module' createApp({ count: 0, get digitSum() { return String(this.count) .split('') .reduce((x, a) => x + Number(a), 0) } }).mount() </script>
<div v-scope> Count: {{ count }} Digit sum {{ digitSum }} <button @click="count++"> Increment </button> </div> <script type="module"> import { createApp } from 'https://unpkg.com/petite-vue?module' createApp({ count: 0, get digitSum() { return String(this.count) .split('') .reduce((x, a) => x + Number(a), 0) } }).mount() </script>
petite-vue
@vue:mounted
and @vue:unmounted
$el
can be used inline to access the element<div v-scope="{ show: false }"> <button @click="show = !show"> Toggle it! </button> <div v-if="show" @vue:unmounted="console.log('unmounted', $el)" @vue:mounted="console.log('mounted', $el)" > Content is being shown! </div> </div>
<div v-scope="{ show: false }"> <button @click="show = !show"> Toggle it! </button> <div v-if="show" @vue:unmounted="console.log('unmounted', $el)" @vue:mounted="console.log('mounted', $el)" > Content is being shown! </div> </div>
v-effect
v-effect
<div v-scope v-effect="getData()"> <input v-model="input" type="number" min="1" max="10"> <pre>{{user.name}}</pre> </div> <script type="module"> import { createApp } from 'https://unpkg.com/petite-vue?module' createApp({ input: '', user: {}, async getData () { if(!this.input) { return } const url = 'https://jsonplaceholder.typicode.com/users/' + this.input this.user = await fetch(url).then(r => r.json()) } }).mount() </script>
<div v-scope v-effect="getData()"> <input v-model="input" type="number" min="1" max="10"> <pre>{{user.name}}</pre> </div> <script type="module"> import { createApp } from 'https://unpkg.com/petite-vue?module' createApp({ input: '', user: {}, async getData () { if(!this.input) { return } const url = 'https://jsonplaceholder.typicode.com/users/' + this.input this.user = await fetch(url).then(r => r.json()) } }).mount() </script>
petite-vue
Ipetite-vue
provides a way to abstract components too<script type="module"> import { createApp } from 'https://unpkg.com/petite-vue?module' function NumberList(props) { return { numbers: props.numbers, get min() { return Math.min(...this.numbers) }, addNumber() { this.numbers.push(this.numbers.length) } } } createApp({ NumberList }).mount() </script>
<script type="module"> import { createApp } from 'https://unpkg.com/petite-vue?module' function NumberList(props) { return { numbers: props.numbers, get min() { return Math.min(...this.numbers) }, addNumber() { this.numbers.push(this.numbers.length) } } } createApp({ NumberList }).mount() </script>
<div v-scope="NumberList({ numbers: [1, 2, 3] })"> <p>{{ numbers }}</p> <p>Min: {{ min }}</p> <button @click="addNumber">Add Number</button> </div> <div v-scope="NumberList({ numbers: [0] })"> <p>{{ numbers }}</p> <p>Min: {{ min }}</p> <button @click="addNumber">Add Number</button> </div>
<div v-scope="NumberList({ numbers: [1, 2, 3] })"> <p>{{ numbers }}</p> <p>Min: {{ min }}</p> <button @click="addNumber">Add Number</button> </div> <div v-scope="NumberList({ numbers: [0] })"> <p>{{ numbers }}</p> <p>Min: {{ min }}</p> <button @click="addNumber">Add Number</button> </div>
petite-vue
IItemplate
s?<template>
to the rescue!<div v-scope="NumberList({ numbers: [1, 2, 3] })"> <p>{{ numbers }}</p> <p>Min: {{ min }}</p> <button @click="addNumber">Add Number</button> </div> <div v-scope="NumberList({ numbers: [0] })"> <p>{{ numbers }}</p> <p>Min: {{ min }}</p> <button @click="addNumber">Add Number</button> </div>
<div v-scope="NumberList({ numbers: [1, 2, 3] })"> <p>{{ numbers }}</p> <p>Min: {{ min }}</p> <button @click="addNumber">Add Number</button> </div> <div v-scope="NumberList({ numbers: [0] })"> <p>{{ numbers }}</p> <p>Min: {{ min }}</p> <button @click="addNumber">Add Number</button> </div>
<div v-scope="NumberList({ numbers: [1, 2, 3] })"> <div v-scope="NumberList({ numbers: [0] })"> <template id="number-list-template"> <p>{{ numbers }}</p> <p>Min: {{ min }}</p> <button @click="addNumber">Add Number</button> </template> <script type="module"> /* ... */ function NumberList(props) { return { $template: '#number-list-template', /* ... */ } } createApp({ NumberList }).mount() </script>
<div v-scope="NumberList({ numbers: [1, 2, 3] })"> <div v-scope="NumberList({ numbers: [0] })"> <template id="number-list-template"> <p>{{ numbers }}</p> <p>Min: {{ min }}</p> <button @click="addNumber">Add Number</button> </template> <script type="module"> /* ... */ function NumberList(props) { return { $template: '#number-list-template', /* ... */ } } createApp({ NumberList }).mount() </script>
petite-vue
render
functions (due to missing VDOM)ref
, no computed
petite-vue.html<div v-scope="{ count: 0 }"> <button @click="count++">Increment</button> <span>{{ count }}</span> </div>
<div v-scope="{ count: 0 }"> <button @click="count++">Increment</button> <span>{{ count }}</span> </div>
alpine.html<div x-data="{ count: 0 }"> <button x-on:click="count++">Increment</button> <span x-text="count"></span> </div>
<div x-data="{ count: 0 }"> <button x-on:click="count++">Increment</button> <span x-text="count"></span> </div>
petite-vue
@vue/reactivity
under the hoodpetite-vue
on the other hand was inspired by Alpinepetite-vue
petite-vue
and comes with a few more features (like a transition system)petite-vue
was thougth as even more minimal compared to Alpine, but also "vue compatible"When building applications
petite-vue
+ progressive enhancement = perfect match