How to add Alpine directives to markup that you can't control
by philw_
HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/2.8.0/alpine.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.0.2/tailwind.min.css">
<p>What if you had a block of HTML that you wanted to use Alpine with, but you can't edit the source of the HTML?</p>
<p>Maybe it's coming from a CMS or external service that you don't control, but you still want to avoid the boilerplate of writing event listeners and reactive state handling yourself?</p>
<div class="cms-nav" id="cms-nav">
<ul>
<li><a href="/program-hub/">Program Hub</a><button>▼</button>
<ul class="multilevel-linkul-0">
<li><a href="/program-hub/undergraduate-programs/">Undergraduate Programs</a></li>
<li><a href="/program-hub/postgraduate-programs/">Postgraduate Programs</a></li>
<li><a href="/program-hub/program-search/">Program Search</a></li>
<li><a href="/program-hub/program-search/program-compare/">Program Compare</a></li>
<li><a href="/program-hub/program-catalog-builder/">Program Catalog Builder</a></li>
</ul>
</li>
<li><a href="/life-at-t4u/">Life at TXU</a><button>▼</button>
<ul class="multilevel-linkul-0">
<li><a href="/life-at-txu/campuses/">Campuses</a>
</li>
<li><a href="/life-at-txu/faq/">FAQ</a></li>
<li><a href="/life-at-txu/student-support/">Student Support</a></li>
<li><a href="/life-at-txu/blog/">Blog</a></li>
</ul>
</li>
<li><a href="/research/">Research</a><button>▼</button>
<ul class="multilevel-linkul-0">
<li><a href="/research/staff-directory/">Staff Directory</a></li>
</ul>
</li>
<li><a href="/alumni/">Alumni</a><button>▼</button>
<ul class="multilevel-linkul-0">
<li><a href="/alumni/events/">Events</a>
</li>
<li><a href="/alumni/newsroom/">Newsroom</a>
</li>
</ul>
</li>
<li><a...
SCSS
.cms-nav {
a {
text-decoration: underline;
}
li {
display: block;
}
&> ul {
display: flex;
button {
margin-left: 1em;
}
}
&> ul > li {
padding: 1em;
background-color: #ebebeb;
margin-bottom: 0.5em;
}
ul ul {
margin-top: 1em;
}
}
JavaScript
// 1) This is where we use JavaScript to make an Alpine component dynamically.
const nav = document.querySelector('#cms-nav > ul');
const topLevelLi = document.querySelectorAll('#cms-nav > ul > li');
const topLevelButton = document.querySelectorAll('#cms-nav > ul > li > button');
const topLevelLinks = document.querySelectorAll('#cms-nav > ul > li > a');
const secondLevelUl = document.querySelectorAll('#cms-nav > ul > li > ul');
topLevelLi.forEach((elem, index) => {
elem.setAttribute('x-on:mouseenter', `toggleMenu(${index})`);
elem.setAttribute('x-on:mouseleave', `closeMenu()`);
});
topLevelButton.forEach((elem, index) => {
elem.setAttribute('x-on:click', `toggleMenu(${index})`);
});
topLevelLinks.forEach((elem, index) => {
elem.setAttribute('x-bind:aria-expanded', `expanded(${index})`);
});
secondLevelUl.forEach((elem, index) => {
elem.setAttribute('x-show', `menuOpen === ${index}`);
elem.style.display = 'none';
});
nav.setAttribute('x-on:click.away', 'escape()');
nav.setAttribute('x-on:keydown.escape.window', 'escape()')
// Adding x-data makes it into an Alpine app again thanks to the Alpine.js
// MutationObserver that detects any DOM changes on the page.
nav.setAttribute('x-data', 'subNavigation()');
// 2) This is the previous Alpine code.
function subNavigation() {
return {
timer: false,
menuOpen: false,
dropdownButton: false,
escape(key = null) {
if (key && key !== 'Escape') return;
this.changeMenuState(false);
this.dropdownButton = false;
this.timer = false;
},
focus(index) {
this.dropdownButton = index;
this.changeMenuState(false);
},
toggleMenu(index) {
const state = this.menuOpen === index ? false : index;
this.changeMenuState(state);
},
openMenu(index) {
clearTimeout(this.timer);
this.changeMenuState(index);
},
closeMenu() {
this.changeMenuState(false);
},
expanded(index) {
return this.menuOpen ===...