├── .editorconfig ├── .github └── workflows │ ├── docs.yml │ └── tests.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── builds ├── cdn.js ├── module.js └── server.js ├── cypress.config.js ├── docs ├── _includes │ ├── demo.njk │ ├── example.njk │ ├── googlec8986a0731969a6e.html │ ├── jukebox.svg │ ├── layout.njk │ ├── logo.svg │ ├── page.njk │ ├── phone.svg │ ├── remote.svg │ ├── sample.njk │ ├── server.svg │ └── topper.svg ├── changelog.md ├── comparisons.md ├── css │ ├── lite-yt-embed.css │ ├── main.css │ └── prism-a11y-dark.css ├── eleventy.config.js ├── examples.njk ├── examples │ ├── bulk-update.md │ ├── delete-row.md │ ├── dialog-form.md │ ├── dialog.md │ ├── edit-row.md │ ├── examples.json │ ├── filterable-content.md │ ├── infinite-scroll.md │ ├── inline-edit.md │ ├── inline-validation.md │ ├── instant-search.md │ ├── lazy-load.md │ ├── loading.md │ ├── notifications.md │ ├── progress-bar.md │ ├── server-events.md │ └── toggle-button.md ├── fonts │ ├── helsinki.woff │ ├── helsinki.woff2 │ ├── ibm-plex-mono-400.woff │ └── ibm-plex-mono-400.woff2 ├── github.md ├── img │ ├── bg-texture.png │ ├── favicon.svg │ ├── share.png │ └── sponsors │ │ └── moonbase-labs.svg ├── index.njk ├── js │ ├── lite-yt-embed.js │ └── main.js ├── postcss.config.js ├── reference.njk ├── reference │ ├── ajax.md │ ├── configuration.md │ ├── creating-demos.md │ ├── events.md │ ├── installation.md │ ├── loading-states.md │ ├── navigation.md │ ├── reference.json │ ├── usage.md │ ├── x-focus.md │ ├── x-headers.md │ ├── x-merge.md │ ├── x-sync.md │ └── x-target.md └── tailwind.config.js ├── package-lock.json ├── package.json ├── scripts └── build.js ├── src ├── index.js └── server.js └── tests ├── ajax.cy.js ├── cache.cy.js ├── cdn-late.html ├── cdn.cy.js ├── cdn.html ├── configure.cy.js ├── dynamic.cy.js ├── events.cy.js ├── exceptions.cy.js ├── focus.cy.js ├── form.cy.js ├── history.cy.js ├── index.html ├── link.cy.js ├── load.cy.js ├── map.cy.js ├── merge.cy.js ├── queue.cy.js ├── status.cy.js ├── sync.cy.js └── utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-22.04 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup Node 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '18.x' 19 | 20 | - name: Install dependencies & build 21 | run: | 22 | npm install 23 | npm run build 24 | npm run build:docs 25 | 26 | - name: Deploy 27 | uses: peaceiris/actions-gh-pages@v3 28 | with: 29 | publish_dir: ./docs/_site 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | cname: alpine-ajax.js.org 32 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Run Tests 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | tests: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v3 18 | 19 | - name: Run tests 20 | uses: cypress-io/github-action@v5 21 | with: 22 | build: npm run build 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /docs/_site 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Christian Taylor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alpine AJAX 2 | 3 | A set of AlpineJS directives that enable forms and links to make asynchronous HTTP requests and render the response to the page. 4 | 5 | Learn more at [alpine-ajax.js.org](https://alpine-ajax.js.org). 6 | 7 | ## Contributing 8 | 9 | Clone this repo and run `npm install` to get started. 10 | 11 | `npm run build` will build a fresh version of the library in `/dist`. 12 | 13 | `npm run watch` will watch for file changes and rebuild the library. 14 | 15 | ## Documentation 16 | 17 | The documentation site is hosted at [https://alpine-ajax.js.org](https://alpine-ajax.js.org), the source files are located in `/docs`. 18 | 19 | `npm run start` will locally serve the documentation site built with [Eleventy](https://www.11ty.dev/). The site automatically bundles the latest Alpine AJAX build in `/docs/js/main.js`. 20 | 21 | ## Testing 22 | 23 | Tests are located in `/tests`. 24 | 25 | `npm run test` will run the test suite in the [Cypress](https://www.cypress.io/) CLI. 26 | 27 | `npm run cypress` will open the Cypress browser UI. 28 | 29 | ## Sponsors 30 | 31 | 32 | Moonbase Labs 33 | 34 | -------------------------------------------------------------------------------- /builds/cdn.js: -------------------------------------------------------------------------------- 1 | import ajax from '../src/index' 2 | 3 | document.addEventListener('alpine:initializing', () => { 4 | ajax.configure(window.alpineAJAX || {}) 5 | ajax(window.Alpine) 6 | }) 7 | -------------------------------------------------------------------------------- /builds/module.js: -------------------------------------------------------------------------------- 1 | import ajax from '../src/index.js' 2 | 3 | export default ajax 4 | -------------------------------------------------------------------------------- /builds/server.js: -------------------------------------------------------------------------------- 1 | import '../src/server.js' 2 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require("cypress") 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | retries: 2, // This is a workaround for flaky network tests 6 | specPattern: 'tests/*.cy.js', 7 | supportFile: false, 8 | }, 9 | downloadsFolder: 'tests/downloads', 10 | fixturesFolder: false, 11 | video: false, 12 | screenshotOnRunFailure: false, 13 | }) 14 | -------------------------------------------------------------------------------- /docs/_includes/demo.njk: -------------------------------------------------------------------------------- 1 | 6 |
7 |
23 | 28 |
29 |
30 |
    31 | 36 |
37 |
38 |
39 | 46 |
47 |
48 |
49 | 50 | {% css %} 51 | #footer { 52 | padding-bottom: calc(40vh + 4rem); 53 | } 54 | @media (min-width: 1024px) { 55 | #main { 56 | padding-bottom: calc(40vh + 4rem); 57 | } 58 | } 59 | {% endcss %} 60 | -------------------------------------------------------------------------------- /docs/_includes/example.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page.njk 3 | --- 4 | 5 | 6 |

{{ title }}

7 | {{ content | safe }} 8 |

Demo

9 |
10 | {% include 'demo.njk' %} 11 |
12 | -------------------------------------------------------------------------------- /docs/_includes/googlec8986a0731969a6e.html: -------------------------------------------------------------------------------- 1 | google-site-verification: googlec8986a0731969a6e.html -------------------------------------------------------------------------------- /docs/_includes/layout.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% if title != 'Alpine AJAX' %} 9 | {% set title = title + ' | Alpine AJAX' %} 10 | {% endif %} 11 | {{ title }} 12 | 13 | 30 | {% css %}{% include "css/main.css" %}{% endcss %} 31 | {% css %}{% include "css/prism-a11y-dark.css" %}{% endcss %} 32 | 33 | 34 | {% for dependency in dependencies %} 35 | 36 | {% endfor %} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Skip to content 53 |
54 | 55 | {% set attrs = { width: 96, height: 85 } %} 56 | {% set prefix = 'head_' %} 57 | {% include 'logo.svg' %} 58 | Alpine AJAX 59 | 60 | Menu 61 |
62 |
63 |
64 | {{ content | safe }} 65 |
66 | 97 |
98 | 99 | 100 | -------------------------------------------------------------------------------- /docs/_includes/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/_includes/page.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layout.njk 3 | --- 4 | 5 |
6 | {{ content | safe }} 7 |
8 | -------------------------------------------------------------------------------- /docs/_includes/sample.njk: -------------------------------------------------------------------------------- 1 |
<form x-target="songs" action="/songs">
<input name="search" @input.debounce="$el.form.requestSubmit()">
</form>

<ul id="songs"></ul>
2 | -------------------------------------------------------------------------------- /docs/_includes/topper.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | --- 2 | eleventyNavigation: 3 | key: Changelog 4 | url: https://github.com/imacrayon/alpine-ajax/releases 5 | order: 4 6 | permalink: false 7 | --- 8 | -------------------------------------------------------------------------------- /docs/comparisons.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page.njk 3 | title: Comparisons 4 | description: Compare Alpine AJAX to similar frameworks 5 | eleventyNavigation: 6 | key: Comparisons 7 | order: 3 8 | --- 9 | 10 | # Comparisons 11 | 12 | What follows is a general guide that lays out some comparisons between Alpine AJAX and other similar libraries. The intention here is to give you some context around how Alpine AJAX sets itself apart and when it may or may not makes sense to use. All of the following libraries are awesome in their own right, and each one served as inspiration for Alpine AJAX. 13 | 14 | * [HTMX](#htmx) 15 | * [Hotwired Turbo](#hotwired-turbo) 16 | * [Unpoly](#unpoly) 17 | * [Laravel Livewire](#laravel-livewire) 18 | 19 | ## HTMX 20 | 21 | [https://htmx.org](https://htmx.org) 22 | 23 | Both HTMX and Alpine AJAX are server-agnostic, they'll integrate nicely with almost any server-side language and architecture. Even more, both Alpine AJAX and HTMX work with Alpine.js. In general the HTMX community encourages developers to use [_hyperscript](https://hyperscript.org/) in place of Alpine.js, but Alpine.js is still considered a good option for adding client-side interaction. Since Alpine AJAX is designed as an Alpine.js plugin, it follows Alpine.js conventions; so if you're already building apps with Alpine.js, Alpine AJAX will feel more familiar. 24 | 25 | HTMX favors flexibility where as Alpine AJAX prefers [convention over configuration](https://en.wikipedia.org/wiki/Convention_over_configuration). Beyond the low-level tooling that HTMX provides, the library isn't very prescriptive about how it should be used. The [HTMX documentation for updating content](https://htmx.org/examples/update-other-content/) is one example of the library's lack of opinion: It presents you with four different solutions and leaves it up to you to consider the trade-offs for each. In contrast, Alpine AJAX tries to provide you with more guidance so you can become productive faster without stumbling into common accessibility and progressive enhancement pitfalls. 26 | 27 | HTMX weighs in at 13kB of JavaScript compared to only 3kB for Alpine AJAX. 28 | 29 | ## Hotwired Turbo 30 | 31 | [https://turbo.hotwired.dev](https://turbo.hotwired.dev) 32 | 33 | Turbo can be paired with almost any server-side language, however it holds strict opinions around response status codes, headers, and content; so it does require some back-end configuration to get started. In contrast, Alpine AJAX only requires that the server respond with HTML so you can be up and running more quickly. It's worth noting that Turbo is designed to work with the [Ruby on Rails](https://rubyonrails.org/) framework out of the box, so installation should be easy if you're building a Rails app. 34 | 35 | For client-side interactions, Turbo works well with Alpine.js, however the Turbo community generally encourages developers to use [Stimulus](https://stimulus.hotwired.dev/). 36 | 37 | Alpine AJAX enables functionality very similar to Turbo's `` Custom Element. However, the fact that Turbo uses Custom Elements creates some serious incompatibilities with HTML: [Updating table content is broken](https://github.com/hotwired/turbo/issues/48), so broken in fact that the Rails team [removed tables](https://github.com/hotwired/turbo/issues/48#issuecomment-1014695187) [from their templates](https://discuss.rubyonrails.org/t/back-again-after-a-long-time-rails-7-scaffolds-table-view/80967/2). The lack of table support is especially unfortunate because many CRUD-based apps make significant use of tables. Besides tables, Turbo Frames can make [other common integrations cumbersome as well](https://github.com/hotwired/turbo/pull/131#discussion_r731924782). Turbo's integration problems are a non-issue in Alpine AJAX because AJAX behavior is defined using HTML attributes instead of Custom Elements, these attributes can be safely applied to any HTML element. 38 | 39 | Turbo communicates updates from the server to the client via Turbo Streams. Turbo Streams require that your server responds with different HTML content based on whether the client is making a "Turbo Request" or a regular HTTP request, this means you end up having to maintain two different sets of HTML templates for these two types of requests. The Rails community recommends using template partials to ease the burden of juggling multiple variants of the same page, but even with partials, using Turbo Streams can still feel like you're maintaining two versions of the same app. In comparison, Alpine AJAX requires no distinction between an AJAX request and regular HTTP request, and state changes are communicated to the frontend via [custom JavaScript events](/reference/#server-events) that can be mixed in with any standard HTML response. 40 | 41 | One notable advantage to using the Hotwire framework is that it provides a workflow for transforming your website into a native mobile application, so if you intend to launch your website on Android and iOS platforms, Hotwired Turbo might be worth considering. 42 | 43 | Turbo weighs in at 22kB of JavaScript compared to only 3kB for Alpine AJAX. 44 | 45 | ## Unpoly 46 | 47 | [https://unpoly.com](https://unpoly.com) 48 | 49 | Like Alpine AJAX, Unpoly is server-agnostic, but it also offers an optional server protocol for developers that want more server-side direction. Similarly, Unpoly encourages UI patterns which support progressive enhancement and accessibility. 50 | 51 | Unpoly is a very comprehensive frontend framework; it has strong conventions and also comes with a few elements like loaders, modals, and popovers baked-in. Because Unpoly has such broad concerns, it requires more upfront commitment to get familiar with it's novel concepts like fragments and layers. It feels more akin to frameworks like Laravel Livewire and Phoenix LiveView than "drop-in" libraries like Alpine AJAX, HTMX. Unpoly has it's own APIs for event delegation and animations so you can make due without using a frontend library like Alpine.js, however [Unpoly's imperative API](https://unpoly.com/up.element) is arguably not as expressive as Alpine.js's terse, declarative syntax. 52 | 53 | The core Unpoly library weighs in at 43kB of JavaScript plus 1kB of required CSS compared to only 18kB for Alpine.js and Alpine AJAX combined. 54 | 55 | ## Laravel Livewire 56 | 57 | [https://laravel-livewire.com](https://laravel-livewire.com) 58 | 59 | If you're building a Laravel app, Livewire provides a lot of convenience and a great developer experience. Livewire has invested a lot of work into making the experience feel first class, it's easy to get up and running, however its component-based architecture is a departure from standard Laravel conventions and will probably require some getting used to. In comparison, Alpine AJAX is server-agnostic, so you can start using it without any changes to your Laravel app; in fact you're encouraged to build your Laravel app first **without** Alpine AJAX, then sprinkle in Alpine AJAX at the end to enhance the user experience. 60 | 61 | The lack of progressive enhancement in Livewire is another reason you might choose Alpine AJAX over Livewire. [When JavaScript is not available](https://www.kryogenix.org/code/browser/everyonehasjs.html) a Livewire app becomes completely unresponsive. In contrast, Alpine AJAX gracefully degrades, so your links and forms can continue to function just like any other server-rendered website. 62 | 63 | Alpine.js was originally created as a companion to Laravel Livewire, so of course Alpine.js and Livewire pair flawlessly together for handling client-side interactions. 64 | 65 | Livewire's JavaScript bundle weighs in at 43kB compared to only 3kB for Alpine AJAX. 66 | -------------------------------------------------------------------------------- /docs/css/lite-yt-embed.css: -------------------------------------------------------------------------------- 1 | lite-youtube { 2 | background-color: #000; 3 | position: relative; 4 | display: block; 5 | contain: content; 6 | background-position: center center; 7 | background-size: cover; 8 | cursor: pointer; 9 | max-width: 720px; 10 | } 11 | 12 | /* gradient */ 13 | lite-youtube::before { 14 | content: attr(data-title); 15 | display: block; 16 | position: absolute; 17 | top: 0; 18 | /* Pixel-perfect port of YT's gradient PNG, using https://github.com/bluesmoon/pngtocss plus optimizations */ 19 | background-image: linear-gradient(180deg, rgb(0 0 0 / 67%) 0%, rgb(0 0 0 / 54%) 14%, rgb(0 0 0 / 15%) 54%, rgb(0 0 0 / 5%) 72%, rgb(0 0 0 / 0%) 94%); 20 | height: 99px; 21 | width: 100%; 22 | font-family: "YouTube Noto", Roboto, Arial, Helvetica, sans-serif; 23 | color: hsl(0deg 0% 93.33%); 24 | text-shadow: 0 0 2px rgba(0, 0, 0, .5); 25 | font-size: 18px; 26 | padding: 25px 20px; 27 | overflow: hidden; 28 | white-space: nowrap; 29 | text-overflow: ellipsis; 30 | box-sizing: border-box; 31 | } 32 | 33 | lite-youtube:hover::before { 34 | color: white; 35 | } 36 | 37 | /* responsive iframe with a 16:9 aspect ratio 38 | thanks https://css-tricks.com/responsive-iframes/ 39 | */ 40 | lite-youtube::after { 41 | content: ""; 42 | display: block; 43 | padding-bottom: calc(100% / (16 / 9)); 44 | } 45 | 46 | lite-youtube>iframe { 47 | width: 100%; 48 | height: 100%; 49 | position: absolute; 50 | top: 0; 51 | left: 0; 52 | border: 0; 53 | } 54 | 55 | /* play button */ 56 | lite-youtube>.lty-playbtn { 57 | display: block; 58 | /* Make the button element cover the whole area for a large hover/click target… */ 59 | width: 100%; 60 | height: 100%; 61 | /* …but visually it's still the same size */ 62 | background: no-repeat center/68px 48px; 63 | /* YT's actual play button svg */ 64 | background-image: url('data:image/svg+xml;utf8,'); 65 | position: absolute; 66 | cursor: pointer; 67 | z-index: 1; 68 | filter: grayscale(100%); 69 | transition: filter .1s cubic-bezier(0, 0, 0.2, 1); 70 | border: 0; 71 | } 72 | 73 | lite-youtube:hover>.lty-playbtn, 74 | lite-youtube .lty-playbtn:focus { 75 | filter: none; 76 | } 77 | 78 | /* Post-click styles */ 79 | lite-youtube.lyt-activated { 80 | cursor: unset; 81 | } 82 | 83 | lite-youtube.lyt-activated::before, 84 | lite-youtube.lyt-activated>.lty-playbtn { 85 | opacity: 0; 86 | pointer-events: none; 87 | } 88 | 89 | .lyt-visually-hidden { 90 | clip: rect(0 0 0 0); 91 | clip-path: inset(50%); 92 | height: 1px; 93 | overflow: hidden; 94 | position: absolute; 95 | white-space: nowrap; 96 | width: 1px; 97 | } 98 | -------------------------------------------------------------------------------- /docs/css/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | pre { 6 | line-height: 2; 7 | } 8 | 9 | nav { 10 | --details-force-closed: (max-width: 1023px); 11 | } 12 | 13 | @media (min-width: 1024px) { 14 | header>nav details>summary { 15 | display: none; 16 | } 17 | } 18 | 19 | nav details>summary { 20 | display: none; 21 | } 22 | 23 | .direct-link { 24 | font-family: sans-serif; 25 | text-decoration: none; 26 | font-style: normal; 27 | margin-left: .1em; 28 | } 29 | 30 | a[href].direct-link, 31 | a[href].direct-link:visited { 32 | color: transparent; 33 | } 34 | 35 | a[href].direct-link:focus, 36 | a[href].direct-link:focus:visited, 37 | :hover>a[href].direct-link, 38 | :hover>a[href].direct-link:visited { 39 | color: theme('colors.almond.600') 40 | } 41 | 42 | .animate-twinkle { 43 | position: absolute; 44 | animation: twinkle 10s infinite ease-out; 45 | } 46 | 47 | @keyframes twinkle { 48 | 0% { 49 | opacity: 1; 50 | } 51 | 52 | 5% { 53 | opacity: 0; 54 | } 55 | 56 | 10% { 57 | opacity: 1; 58 | } 59 | 60 | 50% { 61 | opacity: 1; 62 | } 63 | 64 | 55% { 65 | opacity: 0; 66 | } 67 | 68 | 60% { 69 | opacity: 1; 70 | } 71 | 72 | 100% { 73 | opacity: 0; 74 | } 75 | } 76 | 77 | .animate-glow { 78 | transition: box-shadow; 79 | animation: glow 4s infinite ease-in-out; 80 | } 81 | 82 | @keyframes glow { 83 | 0% { 84 | box-shadow: 0 0 4px theme('colors.red.600'); 85 | } 86 | 87 | 25% { 88 | box-shadow: 0 0 8px theme('colors.red.600'); 89 | } 90 | 91 | 50% { 92 | box-shadow: 0 0 6px theme('colors.red.600'); 93 | } 94 | 95 | 75% { 96 | box-shadow: 0 0 8px theme('colors.red.600'); 97 | } 98 | 99 | 100% { 100 | box-shadow: 0 0 4px theme('colors.red.600'); 101 | } 102 | } 103 | 104 | #demo_frame { 105 | border: 1px solid #000; 106 | border-radius: 0.3em; 107 | overflow: auto; 108 | padding: .5rem; 109 | font-family: theme('fontFamily.sans'); 110 | background: #fff; 111 | } 112 | 113 | #demo_frame #demo :where(h1, h2, h3, h4, h5, h6) { 114 | margin-top: 0; 115 | } 116 | 117 | #demo_frame #demo button { 118 | padding: .5rem .75rem; 119 | font-weight: 600; 120 | background: theme('colors.blue.800'); 121 | line-height: 1.25; 122 | font-size: theme('fontSize.sm'); 123 | color: #fff; 124 | } 125 | 126 | #demo_frame #demo table { 127 | width: 100%; 128 | text-align: left; 129 | } 130 | 131 | #demo_frame #demo td { 132 | border-color: theme('colors.gray.200'); 133 | } 134 | 135 | #demo_frame #demo input, 136 | #demo_frame #demo select, 137 | #demo_frame #demo textarea { 138 | font-family: theme('fontFamily.sans'); 139 | margin-bottom: 1rem; 140 | padding: .25rem .5rem; 141 | border-color: #000; 142 | } 143 | 144 | #demo_frame #demo td>input, 145 | #demo_frame #demo td>select, 146 | #demo_frame #demo td>textarea { 147 | margin-bottom: 0; 148 | } 149 | 150 | #demo_frame #demo dialog { 151 | border: 1px solid #000; 152 | border-radius: 0; 153 | padding: 1rem; 154 | max-width: 56ch; 155 | position: fixed; 156 | top: 50vh; 157 | margin-left: auto; 158 | margin-right: auto; 159 | transform: translate(0, -50%); 160 | } 161 | -------------------------------------------------------------------------------- /docs/css/prism-a11y-dark.css: -------------------------------------------------------------------------------- 1 | /** 2 | * a11y-dark theme for JavaScript, CSS, and HTML 3 | * Based on the okaidia theme: https://github.com/PrismJS/prism/blob/gh-pages/themes/prism-okaidia.css 4 | * @author ericwbailey 5 | */ 6 | 7 | code[class*="language-"], 8 | pre[class*="language-"] { 9 | color: #f8f8f2; 10 | background: none; 11 | text-align: left; 12 | white-space: pre; 13 | word-spacing: normal; 14 | word-break: normal; 15 | word-wrap: normal; 16 | line-height: 2; 17 | 18 | -moz-tab-size: 2; 19 | -o-tab-size: 2; 20 | tab-size: 2; 21 | 22 | -webkit-hyphens: none; 23 | -moz-hyphens: none; 24 | -ms-hyphens: none; 25 | hyphens: none; 26 | } 27 | 28 | /* Code blocks */ 29 | pre[class*="language-"] { 30 | padding: 1em; 31 | margin: 0.5em 0; 32 | overflow: auto; 33 | border-radius: 0.3em; 34 | } 35 | 36 | :not(pre)>code[class*="language-"], 37 | pre[class*="language-"] { 38 | background: #2b2b2b; 39 | } 40 | 41 | /* Inline code */ 42 | :not(pre)>code[class*="language-"] { 43 | padding: 0.1em; 44 | border-radius: 0.3em; 45 | white-space: normal; 46 | } 47 | 48 | .token.comment, 49 | .token.prolog, 50 | .token.doctype, 51 | .token.cdata { 52 | color: #d4d0ab; 53 | } 54 | 55 | .token.punctuation { 56 | color: #fefefe; 57 | } 58 | 59 | .token.property, 60 | .token.tag, 61 | .token.constant, 62 | .token.symbol, 63 | .token.deleted { 64 | color: #ffa07a; 65 | } 66 | 67 | .token.boolean, 68 | .token.number { 69 | color: #00e0e0; 70 | } 71 | 72 | .token.selector, 73 | .token.attr-name, 74 | .token.string, 75 | .token.char, 76 | .token.builtin, 77 | .token.inserted { 78 | color: #abe338; 79 | } 80 | 81 | .token.operator, 82 | .token.entity, 83 | .token.url, 84 | .language-css .token.string, 85 | .style .token.string, 86 | .token.variable { 87 | color: #00e0e0; 88 | } 89 | 90 | .token.atrule, 91 | .token.attr-value, 92 | .token.function { 93 | color: #ffd700; 94 | } 95 | 96 | .token.keyword { 97 | color: #00e0e0; 98 | } 99 | 100 | .token.regex, 101 | .token.important { 102 | color: #ffd700; 103 | } 104 | 105 | .token.important, 106 | .token.bold { 107 | font-weight: bold; 108 | } 109 | 110 | .token.italic { 111 | font-style: italic; 112 | } 113 | 114 | .token.entity { 115 | cursor: help; 116 | } 117 | 118 | @media screen and (-ms-high-contrast: active) { 119 | 120 | code[class*="language-"], 121 | pre[class*="language-"] { 122 | color: windowText; 123 | background: window; 124 | } 125 | 126 | :not(pre)>code[class*="language-"], 127 | pre[class*="language-"] { 128 | background: window; 129 | } 130 | 131 | .token.important { 132 | background: highlight; 133 | color: window; 134 | font-weight: normal; 135 | } 136 | 137 | .token.atrule, 138 | .token.attr-value, 139 | .token.function, 140 | .token.keyword, 141 | .token.operator, 142 | .token.selector { 143 | font-weight: bold; 144 | } 145 | 146 | .token.attr-value, 147 | .token.comment, 148 | .token.doctype, 149 | .token.function, 150 | .token.keyword, 151 | .token.operator, 152 | .token.property, 153 | .token.string { 154 | color: highlight; 155 | } 156 | 157 | .token.attr-value, 158 | .token.url { 159 | font-weight: normal; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /docs/eleventy.config.js: -------------------------------------------------------------------------------- 1 | const markdownIt = require('markdown-it') 2 | const markdownItAnchor = require('markdown-it-anchor') 3 | const { EleventyHtmlBasePlugin, EleventyRenderPlugin } = require('@11ty/eleventy') 4 | const pluginSyntaxHighlight = require('@11ty/eleventy-plugin-syntaxhighlight') 5 | const pluginBundle = require("@11ty/eleventy-plugin-bundle") 6 | const pluginNavigation = require("@11ty/eleventy-navigation") 7 | const esbuild = require('esbuild') 8 | const postcss = require('postcss') 9 | const lockFile = require('../package-lock.json') 10 | 11 | module.exports = function (eleventyConfig) { 12 | eleventyConfig.addPassthroughCopy('css') 13 | eleventyConfig.addPassthroughCopy('fonts') 14 | eleventyConfig.addPassthroughCopy('img') 15 | eleventyConfig.addPassthroughCopy({ 16 | '_includes/googlec8986a0731969a6e.html': 'googlec8986a0731969a6e.html' 17 | }) 18 | 19 | eleventyConfig.addPlugin(EleventyHtmlBasePlugin) 20 | eleventyConfig.addPlugin(pluginNavigation) 21 | eleventyConfig.addPlugin(EleventyRenderPlugin) 22 | eleventyConfig.addPlugin(pluginSyntaxHighlight) 23 | eleventyConfig.addPlugin(pluginBundle, { 24 | transforms: [ 25 | async function (content) { 26 | // this.type returns the bundle name. 27 | if (this.type === 'css') { 28 | // Same as Eleventy transforms, this.page is available here. 29 | let result = await postcss([ 30 | require('autoprefixer'), 31 | require('tailwindcss') 32 | ]).process(content, { from: this.page.inputPath, to: null }) 33 | 34 | return result.css 35 | } 36 | 37 | return content 38 | } 39 | ] 40 | }) 41 | 42 | eleventyConfig.addGlobalData('APLINE_VERSION', () => lockFile.packages['node_modules/alpinejs'].version) 43 | eleventyConfig.addGlobalData('APLINE_AJAX_VERSION', () => lockFile.version) 44 | 45 | eleventyConfig.addFilter('sort', (collection, path = '') => { 46 | let keys = path.split('.') 47 | let value = (entry) => keys.reduce((v, k) => v?.[k], entry) 48 | 49 | return collection.slice().sort((a, b) => value(a) - value(b)) 50 | }) 51 | 52 | eleventyConfig.on('eleventy.before', async () => { 53 | await esbuild.build({ 54 | entryPoints: ['js/main.js'], 55 | bundle: true, 56 | outfile: '_site/js/main.js', 57 | sourcemap: true, 58 | minify: true, 59 | }) 60 | }) 61 | 62 | let markdownLibrary = markdownIt({ 63 | html: true, 64 | linkify: true 65 | }).use(markdownItAnchor, { 66 | permalink: markdownItAnchor.permalink.ariaHidden({ 67 | placement: 'after', 68 | class: 'direct-link', 69 | symbol: '#' 70 | }), 71 | level: [1, 2, 3, 4], 72 | slugify: eleventyConfig.getFilter('slugify') 73 | }) 74 | eleventyConfig.setLibrary('md', markdownLibrary) 75 | 76 | eleventyConfig.setServerOptions({ 77 | watch: ['_site/**/*.js'], 78 | }) 79 | 80 | return { 81 | markdownTemplateEngine: 'njk', 82 | htmlTemplateEngine: "njk", 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /docs/examples.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page.njk 3 | title: Examples 4 | description: Examples and UX patterns built with Alpine AJAX 5 | eleventyNavigation: 6 | key: Examples 7 | order: 2 8 | --- 9 | 10 |

Examples

11 | 12 |

Below are a set of UX patterns implemented using Alpine AJAX with minimal HTML and styling.

13 | 14 |

You can copy and paste them and then adjust them for your needs.

15 | 16 |
17 | {% for entry in collections.all | eleventyNavigation('Examples') %} 18 |
19 |
{{ entry.title }}
20 |
{{ entry.excerpt }}
21 |
22 | {% endfor %} 23 |
24 | -------------------------------------------------------------------------------- /docs/examples/bulk-update.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Bulk Update 3 | eleventyNavigation: 4 | key: Bulk Update 5 | excerpt: Change multiple items in a collection at once. 6 | order: 6 7 | --- 8 | 9 | This demo shows how to implement a common pattern where rows are selected and then bulk updated. This is 10 | accomplished by putting an AJAX form below a table, with associated checkboxes in the table. When the AJAX form is submitted, each checked value will be included in a `PUT` request to `/contacts`. 11 | 12 | ```html 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ... 28 | 29 |
EditNameStatus
Finn MertinsActive
30 |
31 | 32 | 33 |
34 | ``` 35 | 36 | Notice the AJAX form is targeting the `contacts` table. The server will either activate or deactivate the checked users and then rerender the `contacts` table with 37 | updated rows. 38 | 39 | 91 | -------------------------------------------------------------------------------- /docs/examples/delete-row.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Delete Row 3 | eleventyNavigation: 4 | key: Delete Row 5 | excerpt: Delete a row from a table. 6 | order: 4 7 | --- 8 | 9 | This example shows how to implement a delete button that removes a table row when clicked. First let's look at the 10 | table markup: 11 | 12 | ```html 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ... 23 | 24 |
NameEmail
25 | ``` 26 | 27 | The table body is assigned `id="contacts"` and is listening for the `ajax:before` event to confirm any delete actions. 28 | 29 | Each row has a form that will issue a `DELETE` request to delete the row from the server. This request responds 30 | with a table that is lacking the row which was just deleted. 31 | 32 | ```html 33 | 34 | Finn 35 | fmertins@candykingdom.gov 36 | Active 37 | 38 |
39 | 40 |
41 | 42 | 43 | ``` 44 | 45 | 102 | -------------------------------------------------------------------------------- /docs/examples/dialog-form.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dialog (Modal) Form 3 | eleventyNavigation: 4 | key: Dialog (Modal) Form 5 | excerpt: Handle forms inside a dialog window. 6 | order: 16 7 | --- 8 | 9 | This example shows how to handle forms within a dialog window. 10 | 11 | We start with an empty `` and a `` of contact data. 12 | 13 | ```html 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ... 25 | 26 |
NameStatusEmailAction
27 | 28 | 29 |
30 |
31 | ``` 32 | 33 | Notice that the `` has an `id` and is listening for two events: 34 | 1. When the `ajax:before` event is triggered we dispatch a `dialog:open` event. 35 | 2. When the `contact:updated` event is triggered we issue a `GET` request to `/contacts` and refresh the `` to match the server. 36 | 37 | The `` is also listening for two events: 38 | 1. When the `dialog:open` event is triggered the dialog will open. 39 | 2. When the `contact:updated` event is triggered the dialog with close. 40 | 41 | Here is the HTML for a table row: 42 | 43 | ```html 44 | 45 | Finn Mertins 46 | Active 47 | fmertins@candykingdom.gov 48 | Edit 49 | 50 | ``` 51 | 52 | In each table row we have an "Edit" link targeting the empty `#contact` `
` inside our ``. 53 | 54 | Clicking the "Edit" link issues a `GET` request to `/contacts/1/edit` which returns the corresponding `
` for the contact inside the ``: 55 | 56 | ```html 57 | 58 |
59 | 60 | 61 |
62 |
63 | 64 | 68 |
69 |
70 | 71 | 72 |
73 | 74 | 75 | ``` 76 | 77 | Notice the `
` has the `x-target` attribute so that both success and error responses are rendered within the ``. 78 | 79 | When the `` is submitted, a `PUT` request is issued to `/contacts/1` and the server responds with an updated form and a `contact:updated` event: 80 | 81 | ```html 82 | 83 |
84 | ... 85 | 86 | ``` 87 | 88 | Finally, the `contact:updated` event from the server causes the `` to refresh with the updated contact data and dialog to close. 89 | 90 | 179 | -------------------------------------------------------------------------------- /docs/examples/dialog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dialog (Modal) 3 | eleventyNavigation: 4 | key: Dialog (Modal) 5 | excerpt: Load remote content in a dialog window. 6 | order: 11 7 | --- 8 | 9 | This example shows how to load remote content into a dialog window. 10 | 11 | We start an empty `` and a list of links that target the ``. 12 | 13 | ```html 14 |
    15 |
  • Finn Mertins
  • 16 | ... 17 |
      18 | 19 | 20 |
      21 |
      22 |
      23 | ``` 24 | 25 | Clicking a link issues a `GET` request to the server and triggers the `ajax:before` event. When the `ajax:before` event is triggered we dispatch a `dialog:open` event. 26 | 27 | The `` is set to listen for `dialog:open` and will open when that event is fired. 28 | 29 | Finally, the server responds with the modal content: 30 | 31 | ```html 32 |
      33 |

      First Name: Finn

      34 |

      Last Name: Mertens

      35 |

      Email: fmertens@candykingdom.gov

      36 |

      Status: Active

      37 |
      38 | ``` 39 | 40 | 82 | -------------------------------------------------------------------------------- /docs/examples/edit-row.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Edit Row 3 | eleventyNavigation: 4 | key: Edit Row 5 | excerpt: Edit a table row inline. 6 | order: 5 7 | --- 8 | 9 | This example shows how to implement editable table rows. First let's look at the table: 10 | 11 | ```html 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ... 22 | 23 |
      NameEmailActions
      24 | ``` 25 | Here is the HTML for a table row: 26 | 27 | ```html 28 | 29 | Finn Mertins 30 | fmertins@candykingdom.gov 31 | 32 | Edit 33 | 34 | 35 | ``` 36 | 37 | Notice the "Edit" link in the table row is targeting its own row, this will tell the request triggered by the "Edit" link to replace the entire table row. 38 | 39 | Finally, here is the "edit mode" state that will replace a row: 40 | 41 | ```html 42 | 43 | 44 | 45 | 46 | 47 | Cancel 48 |
      49 | 50 |
      51 | 52 | 53 | ``` 54 | 55 | When submitted, the form issues a `PUT` back to `/contacts/1`, which will again display the "view mode" with updated contact details. 56 | 57 | ## Improving focus 58 | 59 | Our editable table is functioning now, but we can sprinkle in a few more attributes to ensure that it's a good experience for keyboard users. We'll use the `x-autofocus` attribute to control the keyboard focus as we switch between the "view" and "edit" modes on the page. 60 | 61 | First, we'll add `x-autofocus` to the "Name" field so that it is focused when our edit form is rendered: 62 | 63 | ```html 64 | 65 | ``` 66 | 67 | Next, we'll add `x-autofocus` to the "Edit" link, so that it is focused when returning back to the details page: 68 | 69 | ```html 70 | Edit 71 | ``` 72 | 73 | Try using the keyboard in the following demo and notice how keyboard focus is maintained as your navigate between the "view" and "edit" modes. 74 | 75 | 82 | 83 | 156 | -------------------------------------------------------------------------------- /docs/examples/examples.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "example.njk", 3 | "tags": [ 4 | "examples" 5 | ], 6 | "eleventyNavigation": { 7 | "parent": "Examples" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/examples/filterable-content.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Filterable Content 3 | eleventyNavigation: 4 | key: Filterable Content 5 | excerpt: Filter down a table or list of content. 6 | order: 8 7 | dependencies: 8 | - https://cdn.jsdelivr.net/npm/@alpinejs/morph@3.x.x/dist/cdn.min.js 9 | --- 10 | 11 | This example filters down a table of contacts based on the user's selection. 12 | 13 | We start with some filter buttons and a table inside a container with `id="contacts"` and `x-merge="morph"`. 14 | 15 | ```html 16 |
      17 |
      18 | 19 | 20 |
      21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ... 41 | 42 |
      NameEmailStatus
      Finnfmertins@candykingdom.govActive
      Jakejake@candykingdom.govInactive
      43 |
      44 | ``` 45 | 46 | The `x-merge="morph` attribute does a lot of the heavy lifting in this example. The `morph` option ensures that the keyboard focus state of our filter buttons will be preserved as the HTML changes between AJAX requests. Morphing requires an extra dependency, so let's pull in Alpine's [Morph Plugin](https://alpinejs.dev/plugins/morph): 47 | 48 | ```html 49 | 50 | ``` 51 | 52 | _If you'd rather bundle your JavaScript, the [Morph Plugin installation instructions](https://alpinejs.dev/plugins/morph#installation) explain how to do this too._ 53 | 54 | Clicking a filter button issues a `GET` request to `/contacts?status=` which returns a response with updated content. 55 | 56 | First, the response should include the modified state of the filter form: 57 | 58 | ```html 59 |
      60 | 61 | 62 | 63 |
      64 | ``` 65 | 66 | The "Active" button has `aria-pressed="true"` to indicate that it has been selected and the form includes a new button to reset the filter settings. 67 | 68 | Second, the response should also include the markup for our table with only content related to the active filter: 69 | 70 | ```html 71 | 72 | 73 | Finn 74 | fmertins@candykingdom.gov 75 | Active 76 | 77 | 78 | ``` 79 | 80 | Let's see our filterable table in action. Try activating a filter button using the keyboard, notice that the keyboard focus stays consistent even as the content on the page changes: 81 | 82 | 87 | 88 | 139 | -------------------------------------------------------------------------------- /docs/examples/infinite-scroll.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Infinite Scroll 3 | eleventyNavigation: 4 | key: Infinite Scroll 5 | excerpt: Load additional content as the user scrolls. 6 | order: 13 7 | dependencies: 8 | - https://cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js 9 | --- 10 | 11 | This example demonstrates how to load content automatically when the user scrolls to the end of the page. We'll start by building basic pagination and then we'll enhance it with Alpine AJAX to automatically fetch the next page. Here's our table followed by simple page navigation: 12 | ```html 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ... 28 | 29 | 37 |
      NameEmailStatus
      AMOamo@mo.coActive
      38 | ``` 39 | 40 | Alpine already provides a great way to react to a users's scroll position: We can use the first-party [Intercept Plugin](https://alpinejs.dev/plugins/intersect), so let's load that onto the page: 41 | 42 | ```html 43 | 44 | ``` 45 | 46 | _If you'd rather bundle your JavaScript, the [Intercept Plugin installation instructions](https://alpinejs.dev/plugins/intersect#installation) explain how to do this too._ 47 | 48 | With the Intercept Plugin installed we can update our pagination markup to issue an AJAX request when it is scrolled into view: 49 | 50 | ```html 51 | 53 | ``` 54 | 55 | Note that the `target` option includes both the table **and** pagination elements. This ensures that the table is updated with fresh records and the pagination is updated with a fresh page URL after each AJAX request. 56 | 57 | Lastly, we need to ensure that the new table rows from subsequent pages are _appended_ to the end of the table. The default behavior is for Alpine AJAX to _replace_ the existing table rows with the incoming rows. To change this behavior we need to add `x-merge="append"` to the element that will receive the new records, in this case that's our table's `tbody`: 58 | 59 | ```html 60 | 61 | ``` 62 | 63 | 85 | 86 | 164 | -------------------------------------------------------------------------------- /docs/examples/inline-edit.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Inline Edit 3 | eleventyNavigation: 4 | key: Inline Edit 5 | excerpt: Edit details inline. 6 | order: 3 7 | --- 8 | 9 | The inline edit pattern provides a way to edit parts of a record by toggling between a "view mode" and "edit mode" without a page refresh . 10 | 11 | This pattern starts with a "view mode" showing the details of a contact inside an element with `id="contact_1"`. The "Edit" link will fetch the "edit mode" for editing a contact at the URL `/contacts/1/edit`. 12 | 13 | ```html 14 |
      15 |

      First Name: Finn

      16 |

      Last Name: Mertens

      17 |

      Email: fmertens@candykingdom.gov

      18 | Edit 19 |
      20 | ``` 21 | 22 | This returns a form that can be used to edit the contact: 23 | 24 | ```html 25 |
      26 |
      27 | 28 | 29 |
      30 |
      31 | 32 | 33 |
      34 |
      35 | 36 | 37 |
      38 | 39 | Cancel 40 |
      41 | ``` 42 | 43 | When submitted, the form issues a `PUT` back to `/contacts/1`, which will again display the "view mode" with updated contact details. 44 | 45 | ## Improving focus 46 | 47 | Our inline edit pattern is functioning now, but we can sprinkle in a few more attributes to ensure that it's a good experience for keyboard users. We'll use the `x-autofocus` attribute to control the keyboard focus as we switch between the view and edit modes on the page. 48 | 49 | First, we'll add `x-autofocus` to the "First Name" field so that it is focused when our edit form is rendered: 50 | 51 | ```html 52 | 53 | ``` 54 | 55 | Next, we'll add `x-autofocus` to the "Edit" link, so that it is focused when returning back to the details page: 56 | 57 | ```html 58 | Edit 59 | ``` 60 | 61 | Try using the keyboard in the following demo and notice how keyboard focus is maintained as your navigate between the "view" and "edit" modes. 62 | 63 | 93 | 94 | 95 | 142 | -------------------------------------------------------------------------------- /docs/examples/inline-validation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Inline Validation 3 | eleventyNavigation: 4 | key: Inline Validation 5 | excerpt: Validate an input field before it is submitted. 6 | order: 9 7 | --- 8 | 9 | This example shows how to do inline field validation. In this example we'll create an email field that can issue requests to our server with an email to be validated, the server will then render validation messages that will be inserted into the DOM. 10 | 11 | We start with this form: 12 | 13 | ```html 14 |
      15 |
      19 | 20 | 21 |
      22 |
      23 | 24 | 25 |
      26 | 27 |
      28 | ``` 29 | 30 | Note that the first `div` in the form has an `id` of `email_field` and it is listening for the `change` event. When the change event occurs the field will issue a `POST` request to the `/validate-email` endpoint. 31 | 32 | The server will return the same form markup containing a new validation error message: 33 | 34 | ```html 35 |
      36 |
      40 | 41 |
      The email is already taken.
      42 | 43 |
      44 | 45 |
      46 | ``` 47 | 48 | Below is a working demo of this example. Any email input without an "@" is considered invalid and the only email that is accepted is **test@example.com**. 49 | 50 | 55 | 56 | 90 | -------------------------------------------------------------------------------- /docs/examples/instant-search.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Instant Search 3 | eleventyNavigation: 4 | key: Instant Search 5 | excerpt: Search or filter a data collection while you type. 6 | order: 7 7 | --- 8 | 9 | This example actively searches a contacts database as the user enters text. 10 | 11 | We start with a search form and a table: 12 | 13 | ```html 14 |
      15 | 16 | 17 |
      18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ... 33 | 34 |
      NameEmailStatus
      Finnfmertins@candykingdom.govActive
      35 | ``` 36 | 37 | Note that the search form targets the table's ``, and that the input inside the form is listening for the `input` event. 38 | 39 | The input issues a `GET` request to `/contacts?search=` on the `input` event and sets the body of the table to be the resulting content. 40 | 41 | We add the `debounce` modifier to the `input` event so that the AJAX request is only sent once the user stops typing. 42 | 43 | Since we use a `search` type input we will get an "x" in the input field to clear the input. To make this trigger a new `GET` request we also add a `search` listener. 44 | 45 | We use `x-show="false"` on the form's submit button so that it is hidden when JavaScript is loaded. This ensures that the search form is still functional if JavaScript fails to load or is disabled. 46 | 47 | 104 | -------------------------------------------------------------------------------- /docs/examples/lazy-load.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Lazy Loading 3 | eleventyNavigation: 4 | key: Lazy Loading 5 | excerpt: Lazily load remote content. 6 | order: 10 7 | --- 8 | 9 | This example shows how to lazily load an element on a page. 10 | 11 | We start with a loading indicator inside an `
      `. Note that the article is assigned `id="post"` : 12 | 13 | ```html 14 |
      15 | ... 16 |
      17 | ``` 18 | 19 | This loading indicator will exist on the page while we fetch the articles's content. You can use any CSS or SVG magic you'd like to create a fancy looking loading indicator. 20 | 21 | The loaded content is then inserted into the UI once the request has succeeded: 22 | 23 | ```html 24 |
      25 |
      ...
      26 |

      ...

      27 |
      28 | ``` 29 | 30 | 80 | 81 | 113 | -------------------------------------------------------------------------------- /docs/examples/loading.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Loading Indicator 3 | eleventyNavigation: 4 | key: Loading Indicator 5 | excerpt: Indicate when AJAX requests are processing. 6 | order: 2 7 | --- 8 | 9 | This example shows how you can use a little CSS to create a nice looking loading indicator that will appear while AJAX requests are in progress. 10 | 11 | We start with a card that contains a link. When the link is clicked, a `GET` request is issued to retrieve a table of contact information. 12 | 13 | ```html 14 |
      15 |
      16 | Load Contacts 17 |
      18 |
      19 | ``` 20 | 21 | The contact table could take a long time to load if it is large, so it would be helpful to indicate to our users that the app is processing their request. 22 | 23 | Fortunately, Alpine AJAX adds `aria-busy="true"` to targets while a request is processing. We can use this attribute in our CSS to automatically show and hide a loading indicator: 24 | 25 | ```css 26 | [aria-busy] { 27 | --loading-size: 64px; 28 | --loading-stroke: 6px; 29 | --loading-duration: 1s; 30 | position: relative; 31 | opacity: .75 32 | } 33 | [aria-busy]:before { 34 | content: ''; 35 | position: absolute; 36 | top: 50%; 37 | left: 50%; 38 | width: var(--loading-size); 39 | height: var(--loading-size); 40 | margin-top: calc(var(--loading-size) / 2 * -1); 41 | margin-left: calc(var(--loading-size) / 2 * -1); 42 | border: var(--loading-stroke) solid rgba(0, 0, 0, 0.15); 43 | border-radius: 50%; 44 | border-top-color: rgba(0, 0, 0, 0.5); 45 | animation: rotate calc(var(--loading-duration)) linear infinite; 46 | } 47 | @keyframes rotate { 48 | 100% { transform: rotate(360deg); } 49 | } 50 | ``` 51 | 52 | 85 | 86 | 87 | 137 | -------------------------------------------------------------------------------- /docs/examples/notifications.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Notifications 3 | eleventyNavigation: 4 | key: Notifications 5 | excerpt: Display notification “toasts”. 6 | order: 14 7 | --- 8 | 9 | This demo shows how to implement notification "toasts". 10 | 11 | This pattern starts with an empty list for our notifications, the list needs an `id` and `x-sync`. We've added `aria-live` to the list so that screen readers will read out new notifications when the are added to the list. 12 | 13 | ```html 14 |
        15 |
      16 | ``` 17 | 18 | We'll also add an AJAX form to this demo so that we can issue requests to the server that will trigger new notifications. 19 | 20 | ```html 21 |
      22 | 23 |
      24 | ``` 25 | 26 | When the AJAX form is submitted the server will respond with a new notification in the list: 27 | 28 | ```html 29 |
        30 |
      • 31 | The button was clicked 1 time. 32 |
      • 33 |
      34 | ``` 35 | 36 | Notice that our AJAX form **does not** target the `notification_list` element, however since our list has the `x-sync` attribute, it will automatically update any time the server responds with an element assigned `id="notification_list"`. 37 | 38 | Our notifications should now be appearing with each form submission, however, every time the form is submitted the new incoming notification will replace the existing notification in our list of notifications; Essentially, our UI can only display a single notification at a time. Instead, we should prepend incoming notifications to our list so that older notifications aren't clobbered with each AJAX request. We can control how new content is added to our list using the `x-merge` attribute: 39 | 40 | ```html 41 |
        42 | ``` 43 | 44 | The basic functionality of our notifications is complete, next there are a few refinements we can make to the notification messages to further improve the user experience. First, let's sprinkle in some additional Alpine code to animate our notifications: 45 | 46 | ```html 47 |
      • 56 | The button was clicked 1 time. 57 |
      • 58 | ``` 59 | 60 | Now our messages will smoothly transition in and out as they are added and removed from the notification list. Next, we can add a "Dismiss" button to each notification: 61 | 62 | ```html 63 |
      • 76 | The button was clicked 1 time. 77 | 78 |
      • 79 | ``` 80 | 81 | And finally, we can make our notifications automatically dismiss after 6 seconds, by adding a `setTimeout` in the `init` method: 82 | 83 | ```html 84 |
      • 98 | The button was clicked 1 time. 99 | 100 |
      • 101 | ``` 102 | 103 | 124 | 125 | 165 | -------------------------------------------------------------------------------- /docs/examples/progress-bar.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Progress Bar 3 | eleventyNavigation: 4 | key: Progress Bar 5 | excerpt: Indicate the progress of a long running process. 6 | order: 12 7 | --- 8 | 9 | This example shows how to implement a smoothly scrolling progress bar. 10 | 11 | We start with an AJAX form that issues a `POST` request to `/jobs` to begin a job process: 12 | 13 | ```html 14 |
        15 |

        New Job

        16 | 17 |
        18 | ``` 19 | 20 | Note that the form is assigned `id="jobs"`. When the form is submitted, it is replaced with a new `
        ` that reloads itself every 600ms: 21 | 22 | ```html 23 |
        24 |

        Job Progress

        25 |
        26 | 29 |
        30 |
        31 | ``` 32 | 33 | On each reload the `aria-valuenow` attribute should change to indicate the server's progress. The `width` of the SVG element should also change to visually indicate progress. 34 | 35 | Finally, when the job is complete, the `x-init` directive is removed and a `
        ` to restart the job is added to the UI: 36 | 37 | ```html 38 |
        39 |

        Job Progress

        40 |
        41 | 44 |
        45 | 46 | 47 | 48 |
        49 | ``` 50 | 51 | 119 | -------------------------------------------------------------------------------- /docs/examples/server-events.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Server Events 3 | eleventyNavigation: 4 | key: Server Events 5 | excerpt: Build a comment form experience using server initiated events. 6 | order: 15 7 | --- 8 | 9 | This example demonstrates how you can configure components to respond to events that occur on your server. Alpine already provides a pattern for communicating between components using an [event listener on the `window` object](https://alpinejs.dev/essentials/events#listening-for-events-on-window). We can use this same pattern to also communicate from the server to any component on the page. Consider this list of comments followed by a comment form: 10 | 11 | ```html 12 |
          13 | ... 14 |
        15 | 16 |
        17 | 18 |
        19 |