This post is part of a series comparing different ways to implement asynchronous requests on the client, which is colloquially known as AJAX. I dedicated the previous post to Vue.js; I'll dedicate this one to Alpine.js - not to be confused with Alpine Linux. I'll follow the same structure as previously. Laying out the work Here's the setup, server- and client-side. Server-side Here is how I integrate Thymeleaf and Alpine.js in the POM: <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <!--1--> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <!--1--> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator</artifactId> <!--1--> <version>0.52</version> </dependency> <dependency> <groupId>org.webjars.npm</groupId> <artifactId>alpinejs</artifactId> <!--2--> <version>3.14.1</version> </dependency> <dependency> <groupId>org.webjars.npm</groupId> <artifactId>axios</artifactId> <!--1--> <version>1.7.3</version> </dependency> </dependencies> Same as last week with Vue Alpine instead of Vue It's similar to Vue's setup. Client-side Here's the code on the HTML side: <script th:src="@{/webjars/axios/dist/axios.js}" src="https://cdn.jsdelivr.net/npm/axios@1.7/dist/axios.min.js"></script> <!--1--> <script th:src="@{/webjars/alpinejs/dist/cdn.js}" src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js" defer></script> <!--2--> <script th:src="@{/alpine.js}" src="../static/alpine.js"></script> <!--3--> <script th:inline="javascript"> /*<![CDATA[*/ window.alpineData = { <!--4--> title: /*[[${ title }]]*/ 'A Title', todos: /*[[${ todos }]]*/ [{ 'id': 1, 'label': 'Take out the trash', 'completed': false }] } /*]]>*/ </script> Axios helps making HTTP requests Alpine itself Our client-side code Set the data As for the POM, it's the same code for Alpine as for Vue. The Alpine code We want to implement the same features as for Vue. Our first steps into Alpine The first step is to bootstrap the framework. We already added the link to our custom alpine.js file above. document.addEventListener('alpine:init', () => { //1 Alpine.data('app', () => ({ //2 // The next JavaScript code snippets will be inside the block })) }) Run the block when the alpine:init event is triggered; the triggering event is specific to Alpine. Bootstrap Alpine and configure it to manage the HTML fragment identified by app We now set the app id on the HTML side. <div id="app"> </div> Until now, it's very similar to Vue.js, a straight one-to-one mapping. Unlike Vue.js, Alpine doesn't seem to have templates. The official UI components are not free. I found an Open Source approach, but it's unavailable on WebJars. Basic interactions Let's implement the check of the complete checkbox. Here's the HTML code: <input type="checkbox" :checked="todo.completed" @click="check(todo.id)"> <!--1--> <input type="checkbox" :checked="todo.completed" @click="check" /> <!--2--> Alpine code Vue code The code is very similar, with the difference that Alpine allows passing parameters. On the Javascript side, we must define the function, and that's all: Alpine.data('app', () => ({ check(id) { axios.patch(`/api/todo/${id}`, {checked: event.target.checked}) } })) Client-side model You might wonder where the todo above comes from. The answer is: from the local model. We initialize it in the app or to be more precise, we initialize the list: Alpine.data('app', () => ({ title: window.alpineData.title, //1 todos: window.alpineData.todos, //2 })) Initialize the title even if it's read-only Initialize the todos list; at this point, it's read-only but we are going to update it the next section Updating the model In this section, we will implement adding a new Todo. Here's the HTML snippet: <form> <div class="form-group row"> <label for="new-todo-label" class="col-auto col-form-label">New task</label> <div class="col-10"> <input type="text" id="new-todo-label" placeholder="Label" class="form-control" x-model="label" /> <!--1--> </div> <div class="col-auto"> <button type="button" class="btn btn-success" @click="create()">Add</button> <!--2--> </div> </div> </form> The x-model defines a model and binds the label property defined in app Define the behavior of the button, as in the previous section The related code is the following: Alpine.data('app', () => ({ label: '', //1 create() { axios.post('/api/todo', {label: this.label}).then(response => { //2 this.todos.push(response.data) //3 }).then(() => { this.label = '' //4 }) } })) Define a new label property Send a POST request with the label value as the JSON payload Get the response payload and add it to the local model of Todo Reset the label value Conclusion Alpine is very similar to Vue, with the notable difference of the lack of templating; components are only available via a price. All other features have an equivalent. I may need to be corrected because the documentation is less extensive. Also, Vue is much more popular than Alpine. The complete source code for this post can be found on GitHub: https://github.com/ajavageek/compare-frontends?embedable=true To go further: Alpine.js This post is part of a series comparing different ways to implement asynchronous requests on the client, which is colloquially known as AJAX. I dedicated the previous post to Vue.js; I'll dedicate this one to Alpine.js - not to be confused with Alpine Linux. previous post Alpine.js I'll follow the same structure as previously. Laying out the work Here's the setup, server- and client-side. Server-side Here is how I integrate Thymeleaf and Alpine.js in the POM: <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <!--1--> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <!--1--> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator</artifactId> <!--1--> <version>0.52</version> </dependency> <dependency> <groupId>org.webjars.npm</groupId> <artifactId>alpinejs</artifactId> <!--2--> <version>3.14.1</version> </dependency> <dependency> <groupId>org.webjars.npm</groupId> <artifactId>axios</artifactId> <!--1--> <version>1.7.3</version> </dependency> </dependencies> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <!--1--> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <!--1--> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator</artifactId> <!--1--> <version>0.52</version> </dependency> <dependency> <groupId>org.webjars.npm</groupId> <artifactId>alpinejs</artifactId> <!--2--> <version>3.14.1</version> </dependency> <dependency> <groupId>org.webjars.npm</groupId> <artifactId>axios</artifactId> <!--1--> <version>1.7.3</version> </dependency> </dependencies> Same as last week with Vue Alpine instead of Vue Same as last week with Vue Alpine instead of Vue It's similar to Vue's setup. Client-side Here's the code on the HTML side: <script th:src="@{/webjars/axios/dist/axios.js}" src="https://cdn.jsdelivr.net/npm/axios@1.7/dist/axios.min.js"></script> <!--1--> <script th:src="@{/webjars/alpinejs/dist/cdn.js}" src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js" defer></script> <!--2--> <script th:src="@{/alpine.js}" src="../static/alpine.js"></script> <!--3--> <script th:inline="javascript"> /*<![CDATA[*/ window.alpineData = { <!--4--> title: /*[[${ title }]]*/ 'A Title', todos: /*[[${ todos }]]*/ [{ 'id': 1, 'label': 'Take out the trash', 'completed': false }] } /*]]>*/ </script> <script th:src="@{/webjars/axios/dist/axios.js}" src="https://cdn.jsdelivr.net/npm/axios@1.7/dist/axios.min.js"></script> <!--1--> <script th:src="@{/webjars/alpinejs/dist/cdn.js}" src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js" defer></script> <!--2--> <script th:src="@{/alpine.js}" src="../static/alpine.js"></script> <!--3--> <script th:inline="javascript"> /*<![CDATA[*/ window.alpineData = { <!--4--> title: /*[[${ title }]]*/ 'A Title', todos: /*[[${ todos }]]*/ [{ 'id': 1, 'label': 'Take out the trash', 'completed': false }] } /*]]>*/ </script> Axios helps making HTTP requests Alpine itself Our client-side code Set the data Axios helps making HTTP requests Axios Alpine itself Our client-side code Set the data As for the POM, it's the same code for Alpine as for Vue. The Alpine code We want to implement the same features as for Vue. Our first steps into Alpine The first step is to bootstrap the framework. We already added the link to our custom alpine.js file above. alpine.js document.addEventListener('alpine:init', () => { //1 Alpine.data('app', () => ({ //2 // The next JavaScript code snippets will be inside the block })) }) document.addEventListener('alpine:init', () => { //1 Alpine.data('app', () => ({ //2 // The next JavaScript code snippets will be inside the block })) }) Run the block when the alpine:init event is triggered; the triggering event is specific to Alpine. Bootstrap Alpine and configure it to manage the HTML fragment identified by app Run the block when the alpine:init event is triggered; the triggering event is specific to Alpine. alpine:init Bootstrap Alpine and configure it to manage the HTML fragment identified by app app We now set the app id on the HTML side. app <div id="app"> </div> <div id="app"> </div> Until now, it's very similar to Vue.js, a straight one-to-one mapping. Unlike Vue.js, Alpine doesn't seem to have templates . The official UI components are not free. I found an Open Source approach , but it's unavailable on WebJars. templates UI components Open Source approach Basic interactions Let's implement the check of the complete checkbox. Here's the HTML code: <input type="checkbox" :checked="todo.completed" @click="check(todo.id)"> <!--1--> <input type="checkbox" :checked="todo.completed" @click="check" /> <!--2--> <input type="checkbox" :checked="todo.completed" @click="check(todo.id)"> <!--1--> <input type="checkbox" :checked="todo.completed" @click="check" /> <!--2--> Alpine code Vue code Alpine code Vue code The code is very similar, with the difference that Alpine allows passing parameters. On the Javascript side, we must define the function, and that's all: Alpine.data('app', () => ({ check(id) { axios.patch(`/api/todo/${id}`, {checked: event.target.checked}) } })) Alpine.data('app', () => ({ check(id) { axios.patch(`/api/todo/${id}`, {checked: event.target.checked}) } })) Client-side model You might wonder where the todo above comes from. The answer is: from the local model. todo We initialize it in the app or to be more precise, we initialize the list: app Alpine.data('app', () => ({ title: window.alpineData.title, //1 todos: window.alpineData.todos, //2 })) Alpine.data('app', () => ({ title: window.alpineData.title, //1 todos: window.alpineData.todos, //2 })) Initialize the title even if it's read-only Initialize the todos list; at this point, it's read-only but we are going to update it the next section Initialize the title even if it's read-only title Initialize the todos list; at this point, it's read-only but we are going to update it the next section todos Updating the model In this section, we will implement adding a new Todo . Todo Here's the HTML snippet: <form> <div class="form-group row"> <label for="new-todo-label" class="col-auto col-form-label">New task</label> <div class="col-10"> <input type="text" id="new-todo-label" placeholder="Label" class="form-control" x-model="label" /> <!--1--> </div> <div class="col-auto"> <button type="button" class="btn btn-success" @click="create()">Add</button> <!--2--> </div> </div> </form> <form> <div class="form-group row"> <label for="new-todo-label" class="col-auto col-form-label">New task</label> <div class="col-10"> <input type="text" id="new-todo-label" placeholder="Label" class="form-control" x-model="label" /> <!--1--> </div> <div class="col-auto"> <button type="button" class="btn btn-success" @click="create()">Add</button> <!--2--> </div> </div> </form> The x-model defines a model and binds the label property defined in app Define the behavior of the button, as in the previous section The x-model defines a model and binds the label property defined in app x-model label app Define the behavior of the button, as in the previous section The related code is the following: Alpine.data('app', () => ({ label: '', //1 create() { axios.post('/api/todo', {label: this.label}).then(response => { //2 this.todos.push(response.data) //3 }).then(() => { this.label = '' //4 }) } })) Alpine.data('app', () => ({ label: '', //1 create() { axios.post('/api/todo', {label: this.label}).then(response => { //2 this.todos.push(response.data) //3 }).then(() => { this.label = '' //4 }) } })) Define a new label property Send a POST request with the label value as the JSON payload Get the response payload and add it to the local model of Todo Reset the label value Define a new label property label Send a POST request with the label value as the JSON payload POST label Get the response payload and add it to the local model of Todo Todo Reset the label value label Conclusion Alpine is very similar to Vue, with the notable difference of the lack of templating; components are only available via a price. All other features have an equivalent. I may need to be corrected because the documentation is less extensive. Also, Vue is much more popular than Alpine. The complete source code for this post can be found on GitHub: https://github.com/ajavageek/compare-frontends?embedable=true https://github.com/ajavageek/compare-frontends?embedable=true To go further: To go further: Alpine.js Alpine.js Alpine.js