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.
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:
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.
Also published here.