├── .editorconfig ├── .eleventy.js ├── .env.example ├── .github ├── FUNDING.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── LICENSE ├── README.md ├── _data ├── articles.yaml ├── builtWith.yaml ├── commit.js ├── components.yaml ├── demos.yaml ├── podcasts.yaml ├── tools.yaml ├── tweets.yaml └── videos.yaml ├── _includes └── example.njk ├── _redirects ├── assets ├── editor.js ├── sharing.js └── tokenize.js ├── emails.json ├── package-lock.json ├── package.json ├── pages ├── benchmark-array-includes-vs-regex.html ├── details-open.html ├── fetch-data.html ├── index.njk ├── js-data-access.html ├── magic-properties-in-js.html ├── magic-property-on-this.html ├── newsletter.html ├── playground.html ├── pre-rendered-content.html ├── read-query-param.html ├── select-and-access.html ├── thank-you.html ├── write-query-param.html ├── x-data-access.html ├── x-for-n-times.html └── x-for-object.html ├── prettier.config.js └── scripts ├── config.js ├── load-newsletters.js └── new.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # The JSON files contain newlines inconsistently 13 | [*.json] 14 | insert_final_newline = ignore 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | 19 | -------------------------------------------------------------------------------- /.eleventy.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const yaml = require("js-yaml"); 3 | const esbuild = require("esbuild"); 4 | 5 | function bundle() { 6 | esbuild.buildSync({ 7 | entryPoints: ['./assets/editor.js'], 8 | bundle: true, 9 | outfile: './dist/editor.js', 10 | minify: process.env.NODE_ENV === 'production' 11 | }) 12 | } 13 | 14 | fs.mkdirSync('./dist', {recursive: true}); 15 | fs.copyFileSync('./node_modules/alpinejs/dist/alpine.js', './dist/alpine.js'); 16 | bundle() 17 | 18 | module.exports = function(eleventyConfig) { 19 | eleventyConfig.addDataExtension("yaml", contents => yaml.load(contents)); 20 | 21 | eleventyConfig.addWatchTarget("./assets"); 22 | eleventyConfig.addTransform('bundle', (content) => { 23 | bundle(); 24 | return content; 25 | }); 26 | 27 | return { 28 | templateFormats: [ 29 | "md", 30 | "njk", 31 | "html", 32 | // "liquid", 33 | "js" 34 | ], 35 | // markdownTemplateEngine: "liquid", 36 | htmlTemplateEngine: "njk", 37 | dataTemplateEngine: "njk", 38 | dir: { 39 | input: "./pages", 40 | includes: "../_includes", 41 | data: "../_data", 42 | output: "dist" 43 | } 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | export BUTTONDOWN_API_KEY= 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: HugoDF 2 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [lts/*] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, format, and build 21 | run: | 22 | npm i 23 | npm run format 24 | npm run build 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ 89 | 90 | dist 91 | .tailwind-cache.css 92 | .inline-css-cache 93 | .remote-asset-cache 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019-2020 Hugo Di Francesco 3 | 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, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alpine.js Playground 2 | 3 | A set of ready to use Alpine.js examples with TailwindCSS. 4 | 5 | Find the examples in the [pages](./pages) directory. 6 | 7 | ## Requirements 8 | 9 | - Node 10 10 | - Yarn 1.x or npm 11 | 12 | ## Setup 13 | 14 | 1. Clone the repository 15 | 2. Run `yarn` or `npm install` installs all required dependencies. 16 | 17 | ## npm scripts 18 | 19 | > Equivalent `npm run 17 | {% endfor %} 18 | 23 | 24 | 25 | 26 |
29 | 33 | Alpine.js Playground 34 | 35 |
36 | Examples 37 | Newsletter 38 |
39 |
40 |
45 |
49 |

{{ title }}

50 |
51 | 56 | 64 | 73 | 82 |
83 |
84 |
85 | Copied! 91 |
96 | {{ content | safe }} 97 |
98 |
101 |

107 |         
108 |
109 |
110 | 111 | 112 |
115 |

116 | 💥 Subscribe for Early Access 117 | to the 118 | Alpine.js Handbook 121 | 📖 💥 122 |

123 |
124 | 125 | 126 | -------------------------------------------------------------------------------- /_redirects: -------------------------------------------------------------------------------- 1 | /chat https://discord.gg/CphxBPR 2 | /chat/ https://discord.gg/CphxBPR 3 | /handbook https://alpinejshandbook.com 302 4 | /handbook/ https://alpinejshandbook.com 302 5 | /submit https://airtable.com/shr2YU6fOWAYyfDXB 302 6 | /submit/ https://airtable.com/shr2YU6fOWAYyfDXB 302 7 | -------------------------------------------------------------------------------- /assets/editor.js: -------------------------------------------------------------------------------- 1 | import "alpinejs"; 2 | import lolight from "lolight"; 3 | import { tokenize } from './tokenize'; 4 | // import { zip, unzip } from "./sharing"; 5 | 6 | // /** 7 | // * @param {string} hash 8 | // * @returns {string} 9 | // */ 10 | // function getHtmlFromHash(hash) { 11 | // if (hash.length > 1) { 12 | // return unzip(hash.substr(1)); 13 | // } 14 | // } 15 | 16 | window.preview = preview; 17 | 18 | function preview() { 19 | return { 20 | component: null, 21 | snippet: null, 22 | copied: false, 23 | stripClasses: false, 24 | stripStyles: false, 25 | isEditing: false, 26 | setup() { 27 | this.component = this.$refs.demo; 28 | // const hashHtml = getHtmlFromHash(window.location.hash); 29 | // if (hashHtml) { 30 | // this.component.innerHTML = hashHtml; 31 | // } 32 | this.$watch("stripClasses", this.buildSnippet.bind(this)); 33 | this.$watch("stripStyles", this.buildSnippet.bind(this)); 34 | 35 | // initialise 36 | this.buildSnippet(); 37 | }, 38 | buildSnippet() { 39 | const html = this.component.innerHTML; 40 | if (!html) { 41 | this.snippet = "No component found"; 42 | return; 43 | } 44 | const tokens = tokenize(html); 45 | this.snippet = tokens.join(""); 46 | // use refs instead of x-html in order to control `lolight` reset more easily 47 | this.$refs.snippet.innerHTML = this.snippet; 48 | // re-initialise https://larsjung.de/lolight/ 49 | lolight.el(this.$refs.snippet); 50 | }, 51 | selectSnippet(event) { 52 | const range = document.createRange(); 53 | range.selectNode(event.target); 54 | window.getSelection().removeAllRanges(); 55 | window.getSelection().addRange(range); 56 | document.execCommand("copy"); 57 | }, 58 | copySnippet(event) { 59 | if (this.copied) return; 60 | window.getSelection().removeAllRanges(); 61 | 62 | const component = this.component.cloneNode(true); 63 | 64 | if (this.stripClasses) { 65 | component.removeAttribute("class"); 66 | component 67 | .querySelectorAll("[class]") 68 | .forEach((el) => el.removeAttribute("class")); 69 | } 70 | if (this.stripStyles) { 71 | component.removeAttribute("style"); 72 | component 73 | .querySelectorAll("[style]") 74 | .forEach((el) => el.removeAttribute("style")); 75 | } 76 | 77 | event.clipboardData.setData("text/plain", component.outerHTML); 78 | 79 | this.copied = true; 80 | setTimeout(() => { 81 | this.copied = false; 82 | }, 2000); 83 | }, 84 | toggleEdit() { 85 | this.stripClasses = false; 86 | this.stripStyles = false; 87 | this.isEditing = !this.isEditing; 88 | if (this.isEditing) { 89 | this.startEdit(); 90 | } else { 91 | this.save(); 92 | } 93 | }, 94 | startEdit() { 95 | this.$refs.snippet.innerHTML = this.snippet; 96 | }, 97 | save() { 98 | this.component.innerHTML = this.snippet; 99 | this.component.innerHTML = this.component.innerText; 100 | // const html = this.component.innerHTML; 101 | // window.location.hash = zip(html); 102 | lolight.el(this.$refs.snippet); 103 | }, 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /assets/sharing.js: -------------------------------------------------------------------------------- 1 | export function zip(s) { 2 | try { 3 | var dict = {}; 4 | var data = (s + "").split(""); 5 | var out = []; 6 | var currChar; 7 | var phrase = data[0]; 8 | var code = 256; 9 | for (var i = 1; i < data.length; i++) { 10 | currChar = data[i]; 11 | if (dict[phrase + currChar] != null) { 12 | phrase += currChar; 13 | } else { 14 | out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0)); 15 | dict[phrase + currChar] = code; 16 | code++; 17 | phrase = currChar; 18 | } 19 | } 20 | out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0)); 21 | for (var j = 0; j < out.length; j++) { 22 | out[j] = String.fromCharCode(out[j]); 23 | } 24 | return utoa(out.join("")); 25 | } catch (e) { 26 | console.log("Failed to zip string return empty string", e); 27 | return ""; 28 | } 29 | } 30 | 31 | // Decompress an LZW-encoded base64 string 32 | export function unzip(base64ZippedString) { 33 | try { 34 | var s = atou(base64ZippedString); 35 | var dict = {}; 36 | var data = (s + "").split(""); 37 | var currChar = data[0]; 38 | var oldPhrase = currChar; 39 | var out = [currChar]; 40 | var code = 256; 41 | var phrase; 42 | for (var i = 1; i < data.length; i++) { 43 | var currCode = data[i].charCodeAt(0); 44 | if (currCode < 256) { 45 | phrase = data[i]; 46 | } else { 47 | phrase = dict[currCode] ? dict[currCode] : oldPhrase + currChar; 48 | } 49 | out.push(phrase); 50 | currChar = phrase.charAt(0); 51 | dict[code] = oldPhrase + currChar; 52 | code++; 53 | oldPhrase = phrase; 54 | } 55 | return out.join(""); 56 | } catch (e) { 57 | console.log("Failed to unzip string return empty string", e); 58 | return ""; 59 | } 60 | } 61 | 62 | // ucs-2 string to base64 encoded ascii 63 | function utoa(str) { 64 | return window.btoa(unescape(encodeURIComponent(str))); 65 | } 66 | // base64 encoded ascii to ucs-2 string 67 | function atou(str) { 68 | return decodeURIComponent(escape(window.atob(str))); 69 | } 70 | -------------------------------------------------------------------------------- /assets/tokenize.js: -------------------------------------------------------------------------------- 1 | import lolight from "lolight"; 2 | /** 3 | * @param {string} html 4 | * @returns {Array} 5 | */ 6 | export function tokenize(html) { 7 | const rawTokens = lolight.tok(html.trim()); 8 | 9 | const tokens = []; 10 | let indentDepth = 0; 11 | let indentations = indentDepth; 12 | const INDENT_NUM = 2; 13 | for (let i = 0; i < rawTokens.length; i++) { 14 | const token = rawTokens[i]; 15 | 16 | switch (token[1]) { 17 | // enable toggling of `style` and `class` attributes 18 | // users might want just the markup (without styles) 19 | case this.stripStyles && "style": 20 | case this.stripClasses && "class": { 21 | // remove 'class="foo bar"' including leading/trailing whitespace 22 | // get rid of class="foo bar" + trailing whitespace 23 | let tokensToSkip = 0; 24 | // "=" of `class="foo bar"` 25 | // && 26 | // "foo bar" of `class="foo bar"`, token of type "str" 27 | if (rawTokens[i + 1][1] === "=" && rawTokens[i + 2][0] === "str") { 28 | tokensToSkip += 2; 29 | } else { 30 | // class without a value, do nothing 31 | break; 32 | } 33 | let spaceTokenIdx = i + 3; 34 | while (rawTokens[spaceTokenIdx][0] === "spc") { 35 | tokensToSkip += 1; 36 | spaceTokenIdx += 1; 37 | } 38 | 39 | // see if next token after removing class is `>` 40 | // to get rid of leftover whitespaces 41 | // eg. ` class="foo bar">` should be `>` not ` >` 42 | if (rawTokens[i + tokensToSkip + 1][1] === ">") { 43 | let lastTokenIdx = tokens.length - 1; 44 | while (tokens[lastTokenIdx] === " ") { 45 | tokens.pop(); 46 | lastTokenIdx -= 1; 47 | } 48 | } 49 | 50 | i += tokensToSkip; 51 | break; 52 | } 53 | default: { 54 | // If it's a return or new line 55 | if (/\r|\n/.exec(token[1])) { 56 | // The return should be first, ie strip leading spaces 57 | token[1] = token[1].replace(/\s+[\n|\r]$/, ""); 58 | 59 | indentDepth = /\s/.exec(token[1]).input.length; 60 | // Check the length of the \r + tabs 61 | const length = /\r|\n/.exec(token[1]).input.length; 62 | 63 | token[1] = token[1].slice(0, indentDepth); 64 | 65 | // Replace tabs with 4 spaces 66 | token[1] = token[1].replace(/\t/g, "    "); 67 | tokens.push(token[1]); 68 | } else { 69 | function getIndentation(depth) { 70 | return (Math.floor(depth / 2) + 1) * INDENT_NUM; 71 | } 72 | function makeSpaces(count) { 73 | return Array.from({ 74 | length: indentations, 75 | }) 76 | .map(() => " ") 77 | .join(""); 78 | } 79 | // tokens that we want to put a newline + re-indent in front of 80 | if (["@", "x"].includes(token[1])) { 81 | indentations = getIndentation(indentDepth); 82 | token[1] = `\n${makeSpaces(indentations)}${token[1]}`; 83 | } 84 | // put onto newline with -2 indent if we reindented the attributes 85 | if ( 86 | indentations === getIndentation(indentDepth) && 87 | [">"].includes(token[1]) && 88 | rawTokens[i - 2][1] !== "/" 89 | ) { 90 | indentations -= INDENT_NUM; 91 | token[1] = `\n${makeSpaces(indentations)}${token[1]}`; 92 | } 93 | 94 | tokens.push( 95 | token[1].replace( 96 | /[\u00A0-\u9999<>\&]/gim, 97 | (i) => "&#" + i.charCodeAt(0) + ";" 98 | ) 99 | ); 100 | } 101 | } 102 | } 103 | } 104 | 105 | return tokens; 106 | } 107 | -------------------------------------------------------------------------------- /emails.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alpinejs-playground", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "A set of ready to use Alpine.js examples with TailwindCSS", 6 | "scripts": { 7 | "build": "rm -rf dist && NODE_ENV=production eleventy && inline-remote-assets 'dist/**/*.html' && cp _redirects dist", 8 | "start": "eleventy --serve", 9 | "format": "prettier ./pages/* ./_includes/* README.md scripts/* assets/* --write", 10 | "fmt": "yarn format" 11 | }, 12 | "keywords": [ 13 | "alpinejs", 14 | "eleventy", 15 | "11ty", 16 | "tailwindcss" 17 | ], 18 | "author": "Hugo Di Francesco", 19 | "license": "MIT", 20 | "dependencies": {}, 21 | "devDependencies": { 22 | "@11ty/eleventy": "^1.0.1", 23 | "alpinejs": "^2.8.2", 24 | "buttondown": "^1.1.1", 25 | "esbuild": "^0.12.10", 26 | "glob": "^7.2.0", 27 | "inline-remote-assets": "^0.7.0", 28 | "js-yaml": "^4.1.0", 29 | "lolight": "^1.4.0", 30 | "node-fetch": "^2.6.1", 31 | "prettier": "^2.6.0", 32 | "purgecss": "^4.0.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pages/benchmark-array-includes-vs-regex.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Benchmark Array#includes vs RegExp literal & 3 | instance" 4 | layout: example.njk 5 | scripts: 6 | - url: https://unpkg.com/lodash@4.17.15/lodash.js 7 | - url: https://unpkg.com/benchmark@2.1.4/benchmark.js 8 | --- 9 | 10 |
11 |
12 | 18 |
19 | Status:  20 | 23 | 26 | 29 |
30 |
31 | 36 | 42 | 127 |
128 | -------------------------------------------------------------------------------- /pages/details-open.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Details Open Binding" 3 | layout: example.njk 4 | --- 5 | 6 |
7 | 13 | 14 |
15 | Summary 16 |
    17 |
  • First of all
  • 18 |
  • Second of all
  • 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /pages/fetch-data.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Data Fetch Example" 3 | layout: example.njk 4 | --- 5 | 6 |
11 |
12 | 18 | 27 |
28 | 52 | 70 |
71 | -------------------------------------------------------------------------------- /pages/index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | eleventyExcludeFromCollections: true 3 | --- 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | Alpine.js Playground - {{ pkg.description }} 15 | 19 | 23 | 24 | 25 | 26 |
31 |

32 | Alpine.js Playground 33 |

34 |
35 | Last update: 36 | {{ commit.text }} 41 |
42 |

43 | A set of ready to use Alpine.js 44 | 57 | with TailwindCSS by 58 | Hugo 63 |

64 | 94 |
95 | 113 | 114 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /pages/js-data-access.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Accessing Data from JavaScript Example" 3 | layout: example.njk 4 | --- 5 | 6 |
7 | 13 | 14 | 24 |
25 | -------------------------------------------------------------------------------- /pages/magic-properties-in-js.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Magic Property Access in JavaScript" 3 | layout: example.njk 4 | --- 5 | 6 |
7 |

8 | Output:
9 | 10 |

11 | 18 | 37 |
38 | -------------------------------------------------------------------------------- /pages/magic-property-on-this.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Magic Properties on `this`" 3 | layout: example.njk 4 | --- 5 | 6 |
7 |
8 | 14 | 15 | 16 | 17 | 18 |

19 | tl;dr of the table (as of Alpine.js v2.3.x): 20 | $el, $refs, $nextTick and 21 | $watch are accessible from handlers for every directive 22 | (except x-data). 23 |

24 |

25 | See the full post on 26 | How to Access Alpine.js Magic Properties from inline handlers and 30 | function/component methods 32 |

33 |

34 | Get access to more Alpine.js content by purchasing 35 | Alpine.js Knowledge Base Membership 40 |

41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 58 | 59 |
HookMagic propertyPresence
60 |
61 | 127 |
128 | -------------------------------------------------------------------------------- /pages/newsletter.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Alpine.js Weekly - Newsletter" 3 | --- 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | Alpine.js Weekly Newsletter 15 | 19 | 23 | 24 | 25 | 26 |
27 |

28 | Alpine.js Weekly 29 |

30 | 31 |

32 | **ARCHIVED**: I'm not maintaining this newsletter any more, but I do publish the Code with Hugo 33 | Newsletter about JavaScript and Alpine.js does come up every now and then. 34 |

35 | 36 |

37 | You can find the archives at https://buttondown.email/alpinejs/archive 38 |

39 | 40 |

41 | Published by 42 | Hugo 47 | from 48 | codewithhugo.com 53 |

54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /pages/playground.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Code Preview Demo" 3 | layout: example.njk 4 | --- 5 | 6 |
7 | 13 |
17 | Hey there! 18 |
19 | 20 |

21 | Thanks to 22 | @kevinbatdorf, original codepen is at: 27 | https://codepen.io/KevinBatdorf/pen/wvMNebe 32 |

33 |
34 | -------------------------------------------------------------------------------- /pages/pre-rendered-content.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Integrating Alpine.js + Pre/Server-rendered content" 3 | layout: example.njk 4 | --- 5 | 6 |
7 |
11 |

12 | Hiding pre-rendered content with x-show 13 |

14 |

15 | pre-rendered content that won't be hidden 16 |

17 |

18 | the content in "pre-rendered to be hidden" 19 | will get hidden 20 |

21 |

22 | the content in "pre-rendered to be hidden" will 23 | get hidden 24 |

25 |

26 | Example with a select that doesn't quite work, we end up with 2 "hello" 27 | options, one is selected & hidden and the other is visible and unselected: 28 |

29 | 35 |
36 |
45 |

46 | Overwriting HTML/text with x-html/x-text 47 |

48 | pre-rendered text 49 |
50 |

Pre-rendered HTML

51 |
52 |
53 |
62 |

63 | Removing single nodes using x-ref + x-init 64 |

65 |

66 | The following text will get removed with x-ref approach "pre-rendered content" 70 |

71 |

72 | As will the following text: "pre-rendered content" 75 |

76 |

77 | A more sensible example with the "select" that works... mostly ("world" 78 | should be selected) 79 |

80 | 86 |
87 |
95 |

96 | Removing nodes using x-remove + x-init 97 |

98 |

99 | The following text will get removed with x-ref approach "pre-rendered content" 102 |

103 |

104 | As will the following text: "pre-rendered content" 105 |

106 |

107 | A more sensible example with the "select" that works... mostly ("world" 108 | should be selected) 109 |

110 | 116 |
117 |
118 | -------------------------------------------------------------------------------- /pages/read-query-param.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Read Query Parameter Demo" 3 | layout: example.njk 4 | --- 5 | 6 |
12 |

location.search: ""

13 |

Param "q" (from URL): ""

14 |

15 | Some sample links with "q" in the URL: 16 | my search, 21 | another search, 26 | reset 29 |

30 |
31 | -------------------------------------------------------------------------------- /pages/select-and-access.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Bound "select" and access selection data example 3 | layout: example.njk 4 | --- 5 | 6 |
7 |

8 | Bound "select" and access selection data 9 |

10 |
11 | 24 | 30 |
31 | 32 | 52 |
53 | -------------------------------------------------------------------------------- /pages/thank-you.html: -------------------------------------------------------------------------------- 1 | --- 2 | eleventyExcludeFromCollections: true 3 | --- 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Thank You - Alpine.js Weekly 12 | 16 | 20 | 21 | 22 | 23 | 24 |
25 |

26 | Alpine.js Weekly - Thank You 27 |

28 | 29 |

30 | Thanks for signing up to the 31 | 35 | Alpine.js Weekly 36 | 37 | newsletter, it’s great to have you! 38 |

39 | 40 |

41 | Just a few steps left to make sure you get the best out of it. 42 |

43 | 44 |

45 | Confirm your email address 46 |

47 | 48 |
    49 |
  1. Open your email account
  2. 50 |
  3. 51 | Open the email with title “Confirm your subscription to Alpine.js 52 | Weekly” 53 |
  4. 54 |
  5. Click the “Click here to confirm” link
  6. 55 |
56 | 57 |

58 | Make sure you receive new issues 59 |

60 |

61 | You’ll shortly receive the latest issue of the 62 | 66 | Alpine.js Weekly 67 | 68 | newsletter, follow these few steps to make sure you receive the emails: 69 |

70 |
    71 |
  1. 72 | Mark the domain as “Not Spam” or trusted (this will usually happen if 73 | you’re using Gmail + a custom domain) 74 |
  2. 75 |
  3. For Gmail users, mark as not “Promotions”
  4. 76 |
  5. 77 | Add the newsletter address to your contacts: 78 | hugo@alpinejs-news.codewithhugo.com 79 |
  6. 80 |
81 | 82 |

83 | The newsletter goes out once a week on Friday. 84 |

85 | 86 |

87 | Published by 88 | Hugo 94 |

95 |
96 | 97 | 98 | -------------------------------------------------------------------------------- /pages/write-query-param.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Write Query Parameter Demo" 3 | layout: example.njk 4 | --- 5 | 6 |
40 |

location.search: ""

41 |

42 | Param "library" (initialised from "lib" URL query param): "" 46 |

47 |
48 | 61 |
62 | 79 |
80 | -------------------------------------------------------------------------------- /pages/x-data-access.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "x-data data access" 3 | layout: example.njk 4 | --- 5 | 6 |
7 |
8 | 14 |
Message:
15 |
16 | 27 |
28 | -------------------------------------------------------------------------------- /pages/x-for-n-times.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "x-for n times/iterations" 3 | layout: example.njk 4 | --- 5 | 6 |
7 |

8 | 4 horizontal rules using Array.from({ length: 4 }): 9 |

10 | 13 | 14 |

15 | 4 horizontal rules using new Array(4).fill(null): 16 |

17 | 20 |

21 | 4 horizontal rules using new Array(4).fill(undefined): 22 |

23 | 26 |
27 | -------------------------------------------------------------------------------- /pages/x-for-object.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "x-for with objects" 3 | layout: example.njk 4 | --- 5 | 6 |
26 |
27 | Table of contents 28 |
    29 | 38 |
39 |

40 | See the full post on 41 | @todo 46 |

47 |
48 |
49 | x-data state: 50 |
51 |
52 |
53 |

54 | Only need the keys/ids - use Object.keys 55 |

56 |
    57 | 60 |
61 |
62 |
63 |

64 | Need keys/ids and values - use Object.entries 65 |

66 | 71 |
72 |
73 |

74 | Need keys/ids and values - use Object.keys + lookup 75 |

76 | 81 |
82 |
83 |

84 | Need only values - use Object.values 85 |

86 |
    87 | 90 |
91 |
92 |
93 |

94 | Need values - use Object.keys + lookup 95 |

96 |
    97 | 100 |
101 |
102 |
103 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | overrides: [ 3 | { 4 | files: "*.njk", 5 | options: { 6 | parser: "html", 7 | }, 8 | }, 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /scripts/config.js: -------------------------------------------------------------------------------- 1 | const pkg = require("../package.json"); 2 | const pagesDir = `${__dirname}/../pages`; 3 | const distDir = `${__dirname}/../dist`; 4 | const indexPath = `${distDir}/index.html`; 5 | module.exports = { 6 | pkg, 7 | pagesDir, 8 | distDir, 9 | indexPath, 10 | }; 11 | -------------------------------------------------------------------------------- /scripts/load-newsletters.js: -------------------------------------------------------------------------------- 1 | const buttondown = require("buttondown"); 2 | const fs = require("fs").promises; 3 | 4 | const { BUTTONDOWN_API_KEY } = process.env; 5 | if (!BUTTONDOWN_API_KEY) { 6 | console.error("BUTTONDOWN_API_KEY must be set"); 7 | process.exit(1); 8 | } 9 | 10 | buttondown.setApiKey(process.env.BUTTONDOWN_API_KEY); 11 | 12 | // change this to the latest issue whose data's been synced to this repo 13 | const LAST_SYNCED_NEWSLETTER_NUMBER = 9; 14 | 15 | async function main() { 16 | const emails = await buttondown.emails.list(); 17 | 18 | const latestEmails = emails.filter( 19 | (e) => e.secondary_id > LAST_SYNCED_NEWSLETTER_NUMBER 20 | ); 21 | 22 | await fs.writeFile( 23 | "emails.json", 24 | JSON.stringify(latestEmails, null, 2), 25 | "utf-8" 26 | ); 27 | } 28 | 29 | main(); 30 | -------------------------------------------------------------------------------- /scripts/new.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | const pageDir = `${__dirname}/../pages`; 4 | const args = process.argv.slice(2); 5 | const [name] = args; 6 | 7 | const fileName = name.endsWith(".html") ? name : `${name}.html`; 8 | 9 | const exampleContent = `--- 10 | title: "${fileName}" 11 | template: example.njk 12 | --- 13 |
14 |

Hello

15 |
`; 16 | 17 | fs.writeFileSync(`${pageDir}/${fileName}`, exampleContent, "utf8"); 18 | --------------------------------------------------------------------------------