paint-brush
A Cat-Centric Guide to Using Web Components in Alpine.jsby@raymondcamden
495 reads
495 reads

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

by Raymond CamdenJune 27th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

A quick look at using web components with Alpine.js
featured image - A Cat-Centric Guide to Using Web Components in Alpine.js
Raymond Camden HackerNoon profile picture


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:


HTML rendered list of cats


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:


Cats render, but with no name or age displayed


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:

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:


Cats listed out with a few new ones


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.


Also published here.