Alpine.js The easy way to add interactivity to your Umbraco site
A presentation at Umbraco DK Festival in November 2021 in Aarhus, Denmark by Søren Kottal
 
                Alpine.js The easy way to add interactivity to your Umbraco site
 
                Søren Kottal Tech Lead at Ecreo Umbraco MVP since 2018 Maintainer/author of Doc Type Grid Editor, Full Text Search, Matryoshka and others… Tinkerer by heart
 
                Ecreo loves Umbraco Simple things should be simple, complex things should be possible Alan Kay
 
                Building CMS driven websites should be simple Routing Templating Extensibility
 
                A brief history of interactivity on websites Flash DHTML The early frameworks
 
                Then jQuery happened $(“#my-div”).addInteractivity(); // yay
 
                // handles how the fixed navigation reacts var position = 0; $(window).scroll(function (e) { var $element = $(‘.header’); var scrollTop = $(this).scrollTop(); if (scrollTop <= 1) { $element.removeClass(‘is-hidden’).removeClass(‘is-scrolling’).removeClass(‘is-scroll-up’); if ($(‘.hero’).length == 0) { $element.addClass(‘is-scrolling’).addClass(‘is-scroll-up’); } } else if (scrollTop < position) { $element.removeClass(‘is-hidden’); $element.addClass(‘is-scroll-up’); } else if (scrollTop > position) { $element.addClass(‘is-scrolling’); $element.removeClass(‘is-scroll-up’); if (scrollTop + $(window).height() >= $(document).height() - $element.height()) { $element.removeClass(‘is-hidden’); $element.addClass(‘is-scroll-up’); } else if (Math.abs($element.position().top) < $element.height()) { $element.addClass(‘is-hidden’); $(‘.header__basket’).removeClass(‘open’); } } position = scrollTop; });
 
                $(document).ready(function () { if (location.hash != “”) { scrollToElement($(location.hash)); } $(“[data-ga-pageview]”).on(“click”, function () { if (ga) { ga(“send”, “pageview”, $(this).attr(“data-ga-pageview”)); ga(“sub.send”, “pageview”, $(this).attr(“data-ga-pageview”)); } }); $(“.checkout__form”).each(function () { var checkout = $(this); checkout.find(“[name=Type]”).on(“change”, function () { var selected = checkout.find(“[name=Type]:checked”).val(); if (selected === “Privat”) { checkout.find(“[name=Organization]”).closest(“.control-group”).hide(); checkout.find(“[name=Address1]”).attr(“placeholder”, “Din adresse”); } else if (selected === “Skole”) { checkout.find(“[name=Organization]”).closest(“.control-group”).show(); checkout.find(“[name=Address1]”).attr(“placeholder”, “Skolens adresse”); } if (checkout.find(“[name=PaymentMethod][value=Faktura]”).is(“:checked”) && selected === “Skole”) { checkout.find(“#ean-option-message”).show(); } else { checkout.find(“#ean-option-message”).hide(); } }); checkout.find(“[name=PaymentMethod]”).on(“change”, function () { }); }); if (checkout.find(“[name=PaymentMethod][value=Faktura]”).is(“:checked”) && checkout.find(“[name=Type]:checked”).val() === “Skole”) { checkout.find(“#ean-option-message”).show(); } else { checkout.find(“#ean-option-message”).hide(); }
 
                // card__item__info $(‘.cardlist__item’).on(‘click’, ‘.cardlist__item__info__btn’, function () { $(this).parent().find(‘.cardlist__item__info’).toggleClass(‘open’); $(this).find(‘.icon’).toggleClass(‘closed’); $(this).find(‘.close’).toggleClass(‘open’); }); if ($(‘#type-person’).prop(‘checked’)) { $(‘label[for=”type-person”]’).addClass(‘active’); $(‘#ean-option’).hide(); } if ($(‘#type-school’).prop(‘checked’)) { $(‘label[for=”type-school”]’).addClass(‘active’); $(‘#ean-option’).show(); $(‘#ean-option .ean’).show(); } if ($(‘#payment-invoice’).prop(‘checked’)) { $(‘label[for=”payment-invoice”]’).addClass(‘active’); } if ($(‘#payment-ean’).prop(‘checked’)) { $(‘label[for=”payment-ean”]’).addClass(‘active’); } if ($(‘#AcceptTerms’).prop(‘checked’)) { $(‘label[for=”AcceptTerms”]’).addClass(‘active’); } $(‘#type-school’).change(function () { if (this.checked) { $(‘label[for=”type-person”]’).removeClass(‘active’); $(‘label[for=”type-school”]’).addClass(‘active’); $(‘#ean-option’).show(); } }); $(‘#type-person’).change(function () { if (this.checked) { $(‘label[for=”type-person”]’).addClass(‘active’); $(‘label[for=”type-school”]’).removeClass(‘active’); $(‘#ean-option’).hide(); } }); if ($(‘#SameDelivery’).prop(‘checked’)) { $(‘label[for=”SameDelivery”]’).addClass(‘active’); $(‘#delivery-address’).show(); } $(‘#SameDelivery’).change(function () { if (this.checked != true) { $(‘label[for=”SameDelivery”]’).removeClass(‘active’); $(‘#delivery-address’).hide(); } else { $(‘label[for=”SameDelivery”]’).addClass(‘active’); $(‘#delivery-address’).show(); } }); $(‘#payment-invoice’).change(function () { if (this.checked) { $(‘label[for=”payment-ean”]’).removeClass(‘active’); $(‘label[for=”payment-invoice”]’).addClass(‘active’); } }); $(‘#payment-ean’).change(function () { if (this.checked) { $(‘label[for=”payment-ean”]’).addClass(‘active’); $(‘label[for=”payment-invoice”]’).removeClass(‘active’); } }); $(‘#AcceptTerms’).change(function () { if (this.checked) { $(‘label[for=”AcceptTerms”]’).addClass(‘active’); } else { $(‘label[for=”AcceptTerms”]’).removeClass(‘active’); } }); // checkout ean $(‘input[name=”PaymentMethod”]’).on(‘change’, function () { var radio = $(this); if (radio.val() == ‘EAN’) { $(‘#ean-option .ean’).show(); } else { $(‘#ean-option .ean’).hide(); } }); //$.validator.unobtrusive.adapters.addBool(“mustbetrue”, “required”); });
 
                And we moved on to MVC like frameworks AngularJS Vue React
 
                NPM became a thing Grunt Gulp Webpack
 
                “Did you npm install?”
 
                CSS were no longer just CSS Less Sass (SCSS) PostCSS
 
                Difficulties with teams and “build-and-forget”-type projects Coding styles Approaches Documentation
 
                “We are really busy, can you add these features, to this stale site, built like no other sites in our portfolio?”
 
                The revelation Best practices you read about is mostly someone elses practises CSS Utility Classes and “Separation of Concerns”
 
                Utility first CSS TailwindCSS Configure and consume Well documented coding style and approach Optimized build
 
                Back to Javascript We had already ditched jQuery Vue and friends felt too big for simple things
 
                Problems with the big boys Vue single file components is great But you have to build them to use them, and have boilerplate code for attaching Vue to your DOM And they add your markup and styles to your JS bundle
 
                Core Web Vitals Speed Interactivity Visual stability
 
                Core Web Vitals Largest Contentful Paint (LCP) First Input Delay (FID) Cumulative Layout Shift (CLS)
 
                Javascript is hurting your Core Web Vitals Every kB of Javascript must add value Not just “what can each framework do” But “what can each framework do, with the smallest payload”
 
                The weigh in Framework Version Filesize Alpine.js 3.5.0 13.8 kB Vue.js 2.6.14 35.4 kB React + React DOM 16.7.0 38.4 kB AngularJS 1.8.2 55.0 kB jQuery 3.6.0 86.4 kB
 
                Modern frameworks are nice Declarative syntax reduces complexity v-if , v-text , v-bind , v-on etc. Less DOM traversing
 
                What is Alpine.js anyway? “Alpine.js offers you the reactive and declarative nature of big frameworks like Vue or React at a much lower cost. You get to keep your DOM, and sprinkle in behavior as you see fit.” Introducing Alpine.js @ Smashing Magazine
 
                Alpine.js works like other frameworks x-if , x-text , x-bind , x-on etc. Works by adding a script tag Sprinkle your interactivity where you need it Refactor to components when too complex
 
                Alpine.js works like other frameworks x-data initializes your component All in your markup, no querying for a node to attach “the app”
 
                TailwindCSS for Javascript Where TailwindCSS gives us tools to write less CSS, while still being able to make our own bespoke designs, Alpine.js gives us tools to write less Javascript, and makes it easier to sprinkle interactivity where needed. Less code - less bugs Same approach across projects and teams, documentation out of the box
 
                Familiar syntax if you are used to popular frameworks v-if v-bind v-on v-text v-html v-model v-show v-transition v-for v-ref v-cloak
 
                Familiar syntax if you are used to popular frameworks x-if x-bind x-on x-text x-html x-model x-show x-transition x-for x-ref x-cloak
 
                And then some x-init x-effect x-ignore x-intersect = from plugins x-trap x-collapse
 
                Magic properties $el $refs $store $watch $dispatch $nextTick $root $persist = from plugins
 
                Where and when to use CMS driven sites (Umbraco, Wordpress, Drupal etc.) Simple static site generators (11ty, Hugo etc.) When doing proof of concepts in Codepen
 
                How to use <script src=”//unpkg.com/alpinejs” defer></script>
 
                Simple stuff should be simple <div x-data=”{ open: false }”> <button x-on:click=”open = !open”> </div> </button>
 
                Complex stuff should be possible <div x-data=”dropdown”> <button x-on:click=”toggle”> </div> </button> document.addEventListener(“alpine:init”, () => { Alpine.data(“dropdown”, () => ({ open: false, toggle() { this.open = !this.open; }, })); });
 
                A typical vanilla JS component - markup <div class=”hamburger”> <button class=”hamburger__toggle”> <div class=”hamburger__content”> </div> </button> </div>
 
                A typical vanilla JS component - css .hamburger__content { display: none; } .hamburger[aria-expanded] > .hamburger__content { display: block; }
 
                A typical vanilla JS component - script const hamburger = { init: function () { let hamburgerElm = document.querySelector(“.hamburger”); let toggleElm = hamburgerElm?.querySelector(“.hamburger__toggle”); toggleElm?.addEventListener(“click”, () => { hamburgerElm.ariaExpanded = !hamburgerElm.ariaExpanded; if (hamburgerElm.ariaExpanded) { window.addEventListener( “keydown”, (event) => { if (event.code === “Escape”) { hamburgerElm.ariaExpanded = false; } }, { once: true } ); } }); }, }; export default hamburger.init();
 
                A typical vanilla JS component - index.js import “./components/hamburger”;
 
                The same in Alpine.js <div class=”hamburger” x-data=”{ open: false }” x-on:keydown.escape.window=”open = false” x-bind:aria-expanded=”open” > <button class=”hamburger__toggle” x-on:click=”open = !open”> <div class=”hamburger__content” x-show=”open”> </div> </button>
</div> 
                Demo time
 
                Updating values
 
                Using values across components
 
                Persisting values to localStorage Plugin provided - adds just 369 bytes of javascript to your bundle.
 
                Easy transitions Combine x-transition with x-show and get transitions without breaking a sweat
 
                Easy transitions
 
                Triggering animation with x-intersect x-intersect allows you to easily set up Intersection Observers on your elements. Lazy loading images Infinite scrolling Tracking how much content has been seen by the user Triggering animations 398 bytes of javascript
 
                Triggering animation with x-intersect <div x-data=”{ isIn: false }” x-intersect:enter.half=”isIn = true” x-bind:class=”{‘opacity-0 transform translate-x-1/2’ : !isIn }” class=”transition” > … </div>
 
                Triggering animation with x-intersect
 
                Intersection Observer let options = { root: document.querySelector(‘#scrollArea’), rootMargin: ‘0px’, threshold: 1.0 } let callback = (entries, observer) => { entries.forEach(entry => { // Each entry describes an intersection change for one observed // target element: // entry.boundingClientRect // entry.intersectionRatio // entry.intersectionRect // entry.isIntersecting // entry.rootBounds // entry.target // entry.time }); }; let observer = new IntersectionObserver(callback, options); let target = document.querySelector(‘#listItem’); observer.observe(target);
 
                Easy keyboard handling Focus styles are crucial for keyboard users, but often redundant for pointer users AlpineJS makes it easy to detect keyboard users
 
                Easy keyboard handling <div x-data x-on:keydown.tab.window=”$store.usingKeyboard = true” x-on:mousedown.window=”$store.usingKeyboard = false” x-on:touchend.window=”$store.usingKeyboard = false” ></div> … <a x-bind:class=”{ ‘focus:outline-none’ : !$store.usingKeyboard, ‘focus:ring-2’ : $store.usingKeyboard }” >Link</a >
 
                Easy keyboard handling <div x-data x-on:keydown.tab.window=”$store.usingKeyboard = true” x-on:mousedown.window=”$store.usingKeyboard = false” x-on:touchend.window=”$store.usingKeyboard = false” ></div> … <body x-bind:class=”{ ‘not-using-keyboard’ : !$store.usingKeyboard, ‘using-keyboard’ : $store.usingKeyboard }” ></body>
 
                Preventing scroll when modal is open <div x-data=” { open: false, toggle() { this.open = !this.open if (this.open) { document.body.classList.add(‘overflow-y-hidden’) } else { document.body.classList.remove(‘overflow-y-hidden’) } } }”> <button x-on:click=”open != open”> </div>
 
                Preventing scroll when modal is open <body x-data x-class=”{ ‘overflow-y-hidden’ : $store.disableBodyScroll }”> <div x-data=” { open: false, toggle() { this.open = !this.open this.$store.disableBodyScroll = this.open } }”> <button x-on:click=”open != open”> </div>
 
                Preventing scroll when modal is open <body> <div x-data=” { open: false, toggle() { this.open = !this.open } }”> <button x-on:click=”open != open”> <div x-show=”open” x-trap.noscroll> </div>
 
                Initializing JS libraries Cleave.js is a library for formatting <input> content while typing var cleave = new Cleave(“.input-element”, { numeral: true, });
 
                Cleave.js with Alpine <input x-init=”new Cleave($el, { numeral: true })” />
 
                Umbraco and Alpine Razor views and declarative javascript directives
 
                Easy filtering <div x-data=”{ filter: ” }”> <input type=”text” x-model=”filter” /> @foreach (var person in Model.Persons) { <div x-show=”’@person.Name’.indexOf(filter) > -1”>@person.Name</div> } </div>
 
                Easy filtering
 
                Less easy filtering <div x-data=”{ search() { fetch(this.$refs.searchForm.action, { body: new FormData(this.$refs.searchForm), method: this.$refs.searchForm.method }).then(response => { let parser = new DOMParser(); let doc = parser.parseFromString(response.content, ‘text/html’) let persons = doc.querySelector(‘[x-ref=persons]’) this.$refs.persons.innerHTML = persons.innerHTML }); } }”> <form action=”@Model.Url()” method=”get” x-ref=”searchForm” x-on:submit.prevent=”search” > <input type=”text” name=”filter” x-on:keyup.debounce=”$dispatch(‘submit’)” /> </form> <div x-ref=”persons”> @foreach (var person in Model.Persons.Where(x => x.Name.InvariantContains(Request[“filter”]))) { <div>@person.Name</div> } </div> </div>
 
                Making a tabbed interface Ingredients: Nested Content Tab element type containing a title and some content Razor view for rendering the tabbed interface
 
                 
                Making a tabbed interface
 
                Ajax submitting Umbraco Forms Swap @using (Html.BeginUmbracoForm<Umbraco.Forms.Web.Controllers.UmbracoFormsController>(“HandleForm”)) with @{ var htmlAttrs = new Dictionary<string, object>(); htmlAttrs.Add(“x-data”, “asyncUmbracoForm”); // our Alpine component htmlAttrs.Add(“x-on:submit”, “submit”); // the submit method } @using (Html.BeginUmbracoForm<Umbraco.Forms.Web.Controllers.UmbracoFormsController>(“HandleForm”, new { }, htmlAttrs))
 
                Alpine.data(“asyncUmbracoFrom”, () => ({ submitting: false, submit(event) { this.submitting = $(this.$root).valid(); // jQuery validate if (!this.submitting) { event.preventDefault(); } else { event.preventDefault(); fetch(this.$root.action, { body: new FormData(this.$root), method: this.$root.method, }).then((response) => { if (response.redirected) { window.location.href = response.url; } else if (response.headers.get(“UmbracoFormsSubmitted”)) { this.submitting = false; // TODO: Somehow tell the user, the form has submitted } else { alert(“Oh no, it didn’t work”); } }); } }, }));
 
                Why is this nice? We let Forms do what Forms does best Handling fields, conditions, validation and serverside stuff We orchestrate behaviour using Alpine, piggybacking onto existing stuff.
 
                What do I love the most of Alpine.js Transitions Event handling State management
 
                What are the alternatives? The usual suspects - Vue, React, Angular, Svelte HTMX Petite Vue
 
                Petite Vue vs Alpine If you love Vue and don’t want to let it go No transitions No plugins (x-intersect, x-persist etc.) Half the size Great potential
 
                Summing up Easy to get going Sprinkle in interactivity when you need it Stay out of your way when you don’t Mature with a growing community
 
                 
                Questions? Twitter @skttl Catch me later
