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:

See the Pen Alpine + WC by Raymond Camden (@cfjedimaster) on CodePen.

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 hint that "Add Cat" button multiple times because more cats is always a good thing.

See the Pen Alpine + WC (2) by Raymond Camden (@cfjedimaster) on CodePen.


Photo by Christopher Alvarenga on Unsplash