A Cat-Centric Guide to Using Web Components in Alpine.js

Written by raymondcamden | Published 2023/06/27
Tech Story Tags: javascript | web-development | web-components | alpine.js | programming | coding | guide | java

TLDRvia the TL;DR App

To be honest, the TLDR for this entire post is, "It just works,” so I'd more than understand if you stop reading, but like most things in my life, I like to see it working to reassure myself of the fact. So with that out of the way, let's consider a simple example.

First Attempt

I began by defining a super simple Alpine application that just has a list of cats:

document.addEventListener('alpine:init', () => {
  Alpine.data('app', () => ({
		cats:[
			{name:"Luna", age:11},
			{name:"Pig", age:9},
			{name:"Elise", age:13},
			{name:"Zelda", age:1},
			{name:"Grace", age:12},
			]
  }))
});

In the HTML, I iterate over each cat and display it with a web component I'll define in a moment:

<div x-data="app">
	<template x-for="cat in cats">
		<p>
		cat: <cat-view :name="cat.name" :age="cat.age"></cat-view>
		</p>
	</template>
</div>

As the component hasn't been defined yet, all I'll see are 5 "cat:" messages:

Alright, let's define our web component:

class CatView extends HTMLElement {

	constructor() {
		super();
		this.name = '';
		this.age = '';
	}
	
	connectedCallback() {
		
		if(this.hasAttribute('name')) this.name = this.getAttribute('name');
		if(this.hasAttribute('age')) this.age = this.getAttribute('age');
		this.render();
	}
	
	render() {
		this.innerHTML = `
<div>
I'm a cat named ${this.name} that is ${this.age} years old.
</div>
`;
	}
	
	
}

if(!customElements.get('cat-view')) customElements.define('cat-view', CatView);

All this component is doing is picking up the name and age attributes and rendering it out in a div. Let's see what this renders:

So what happened? Alpine successfully added the components to the DOM, but the attributes were updated after the connectedCallback event was fired. This was - I think - expected - and luckily is simple enough to fix with observedAttributes and attributeChangedCallback:

class CatView extends HTMLElement {

	constructor() {
		super();
		this.name = '';
		this.age = '';
	}
	
	connectedCallback() {
		
		if(this.hasAttribute('name')) this.name = this.getAttribute('name');
		if(this.hasAttribute('age')) this.age = this.getAttribute('age');
		this.render();
	}
	
	render() {
		this.innerHTML = `
<div>
I'm a cat named ${this.name} that is ${this.age} years old.
</div>
`;
	}
	
	static get observedAttributes() { return ['name', 'age']; }

	attributeChangedCallback(name, oldValue, newValue) {
		this[name] = newValue;
		this.render();
	}
	
	
}

And voila, you can see the result below:

https://codepen.io/cfjedimaster/pen/ZEqgXWo?embedable=true

Small Update

Cool, so that worked, but I wanted to be sure that updating data in Alpine worked, so I added a quick button:

<button @click="addCat">Add Cat</button>

This was tied to this handler:

addCat() {
	let newCat = {
		name:`New cat ${this.cats.length+1}`,
		age: this.cats.length
	};
	this.cats.push(newCat);
}

I'm just giving a name and age based on the number of cats already in the data set. Again, no surprises here, but it works as expected:

You can find this version below. I encourage you to hit that "Add Cat" button multiple times because more cats are always a good thing.

https://codepen.io/cfjedimaster/pen/abReLmX?embedable=true


Also published here.


Written by raymondcamden | Father, husband, web nerd who builds too many cat-based demos. Love Jamstack, serverless, JavaScript, Python.
Published by HackerNoon on 2023/06/27