├── .gitignore ├── LICENSE.md ├── README.md ├── benchmarks ├── giant.html ├── init.html ├── loop.html ├── memory.html └── mutation_observer.html ├── cypress.json ├── editorconfig ├── index.html ├── jest.config.js ├── package.json ├── packages ├── alpinejs │ ├── builds │ │ ├── cdn.js │ │ └── module.js │ ├── dist │ │ ├── cdn.js │ │ ├── cdn.min.js │ │ ├── module.cjs.js │ │ └── module.esm.js │ ├── node_modules │ │ └── .package-lock.json │ ├── package.json │ └── src │ │ ├── alpine.js │ │ ├── clone.js │ │ ├── datas.js │ │ ├── directives.js │ │ ├── directives │ │ ├── index.js │ │ ├── x-bind.js │ │ ├── x-cloak.js │ │ ├── x-data.js │ │ ├── x-effect.js │ │ ├── x-for.js │ │ ├── x-if.js │ │ ├── x-ignore.js │ │ ├── x-init.js │ │ ├── x-model.js │ │ ├── x-on.js │ │ ├── x-ref.js │ │ ├── x-show.js │ │ ├── x-text.js │ │ └── x-transition.js │ │ ├── evaluator.js │ │ ├── index.js │ │ ├── interceptor.js │ │ ├── lifecycle.js │ │ ├── magics.js │ │ ├── magics │ │ ├── $dispatch.js │ │ ├── $el.js │ │ ├── $nextTick.js │ │ ├── $refs.js │ │ ├── $store.js │ │ ├── $watch.js │ │ └── index.js │ │ ├── mutation.js │ │ ├── nextTick.js │ │ ├── plugin.js │ │ ├── reactivity.js │ │ ├── scheduler.js │ │ ├── scope.js │ │ ├── store.js │ │ └── utils │ │ ├── bind.js │ │ ├── classes.js │ │ ├── dispatch.js │ │ ├── on.js │ │ ├── once.js │ │ ├── styles.js │ │ ├── walk.js │ │ └── warn.js ├── csp │ ├── builds │ │ ├── cdn.js │ │ └── module.js │ ├── dist │ │ ├── cdn.js │ │ ├── cdn.min.js │ │ ├── module.cjs.js │ │ ├── module.esm.js │ │ └── module.js │ ├── package.json │ └── src │ │ └── index.js ├── docs │ ├── package.json │ └── src │ │ └── en │ │ ├── advanced.md │ │ ├── advanced │ │ ├── async.md │ │ ├── csp.md │ │ ├── extending.md │ │ └── reactivity.md │ │ ├── alpine-101.md │ │ ├── directives.md │ │ ├── directives │ │ ├── bind.md │ │ ├── cloak.md │ │ ├── data.md │ │ ├── effect.md │ │ ├── for.md │ │ ├── if.md │ │ ├── ignore.md │ │ ├── init.md │ │ ├── model.md │ │ ├── on.md │ │ ├── ref.md │ │ ├── show.md │ │ ├── text.md │ │ └── transition.md │ │ ├── essentials.md │ │ ├── essentials │ │ ├── events.md │ │ ├── installation.md │ │ ├── lifecycle.md │ │ ├── state.md │ │ └── templating.md │ │ ├── globals.md │ │ ├── globals │ │ ├── alpine-data.md │ │ └── alpine-store.md │ │ ├── magics.md │ │ ├── magics │ │ ├── dispatch.md │ │ ├── el.md │ │ ├── nextTick.md │ │ ├── refs.md │ │ ├── store.md │ │ └── watch.md │ │ └── upgrade-guide.md ├── history │ ├── builds │ │ ├── cdn.js │ │ └── module.js │ ├── dist │ │ ├── cdn.js │ │ ├── cdn.min.js │ │ ├── module.cjs.js │ │ └── module.esm.js │ ├── package.json │ └── src │ │ ├── index.js │ │ └── url.js ├── intersect │ ├── builds │ │ ├── cdn.js │ │ └── module.js │ ├── dist │ │ ├── cdn.js │ │ ├── cdn.min.js │ │ ├── module.cjs.js │ │ └── module.esm.js │ ├── node_modules │ │ └── .package-lock.json │ ├── package.json │ └── src │ │ └── index.js ├── morph │ ├── builds │ │ ├── cdn.js │ │ └── module.js │ ├── dist │ │ ├── cdn.js │ │ ├── cdn.min.js │ │ ├── module.cjs.js │ │ └── module.esm.js │ ├── package.json │ └── src │ │ ├── index.js │ │ └── morph.js ├── persist │ ├── builds │ │ ├── cdn.js │ │ └── module.js │ ├── dist │ │ ├── cdn.js │ │ ├── cdn.min.js │ │ ├── module.cjs.js │ │ └── module.esm.js │ ├── package.json │ └── src │ │ └── index.js └── trap │ ├── .gitignore │ ├── builds │ ├── cdn.js │ └── module.js │ ├── dist │ ├── cdn.js │ ├── cdn.min.js │ ├── module.cjs.js │ └── module.esm.js │ ├── package.json │ └── src │ └── index.js ├── scripts └── build.js └── tests ├── cypress ├── .gitignore ├── fixtures │ └── example.json ├── integration │ ├── clone.spec.js │ ├── custom-data.spec.js │ ├── custom-directives.spec.js │ ├── custom-magics.spec.js │ ├── custom-prefix.spec.js │ ├── directives │ │ ├── x-bind.spec.js │ │ ├── x-bind:class.spec.js │ │ ├── x-bind:style.spec.js │ │ ├── x-cloak.spec.js │ │ ├── x-data.spec.js │ │ ├── x-for.spec.js │ │ ├── x-if.spec.js │ │ ├── x-ignore.spec.js │ │ ├── x-init.spec.js │ │ ├── x-model.spec.js │ │ ├── x-on.spec.js │ │ ├── x-show.spec.js │ │ ├── x-text.spec.js │ │ └── x-transition.spec.js │ ├── magics │ │ ├── $dispatch.spec.js │ │ ├── $el.spec.js │ │ ├── $nextTick.spec.js │ │ ├── $refs.spec.js │ │ └── $watch.spec.js │ ├── mutation.spec.js │ ├── plugins │ │ ├── csp-compatibility.spec.js │ │ ├── history.spec.js │ │ ├── intersect.spec.js │ │ ├── morph.spec.js │ │ ├── persist.spec.js │ │ └── trap.spec.js │ └── store.spec.js ├── manual-transition-test.html ├── plugins │ └── index.js ├── spec-csp.html ├── spec.html ├── support │ ├── commands.js │ └── index.js └── utils.js └── jest └── morph ├── alpine-scope.spec.js ├── createElement.js ├── external.spec.js ├── hooks.spec.js └── internal.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | scratch.md 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright © 2019-2021 Caleb Porzio and contributors 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 | ### IMPORTANT: 2 | This repo has moved to the main Alpine repo here: https://github.com/alpinejs/alpine 3 | -------------------------------------------------------------------------------- /benchmarks/memory.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 8 | 9 | 10 |
11 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /benchmarks/mutation_observer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 56 | 57 | 58 |
59 |

yo

60 |

there

61 | 62 | 63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignoreTestFiles": "*.html", 3 | "screenshotOnRunFailure": false, 4 | "video": false, 5 | "fixturesFolder": "tests/cypress/fixtures", 6 | "integrationFolder": "tests/cypress/integration", 7 | "pluginsFile": "tests/cypress/plugins/index.js", 8 | "screenshotsFolder": "tests/cypress/screenshots", 9 | "videosFolder": "tests/cypress/videos", 10 | "supportFile": "tests/cypress/support/index.js" 11 | } 12 | -------------------------------------------------------------------------------- /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 = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | let config = { 2 | rootDir: './tests/jest', 3 | } 4 | 5 | module.exports = config 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "packages/*" 5 | ], 6 | "devDependencies": { 7 | "brotli-size": "^4.0.0", 8 | "cypress": "^5.5.0", 9 | "cypress-plugin-tab": "^1.0.5", 10 | "dot-json": "^1.2.2", 11 | "esbuild": "^0.8.39", 12 | "jest": "^26.6.3" 13 | }, 14 | "scripts": { 15 | "build": "node ./scripts/build.js", 16 | "watch": "node ./scripts/build.js --watch", 17 | "test": "jest test && cypress run", 18 | "cypress": "cypress open", 19 | "jest": "jest test" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/alpinejs/builds/cdn.js: -------------------------------------------------------------------------------- 1 | import Alpine from './../src/index' 2 | 3 | window.Alpine = Alpine 4 | 5 | queueMicrotask(() => { 6 | Alpine.start() 7 | }) 8 | -------------------------------------------------------------------------------- /packages/alpinejs/builds/module.js: -------------------------------------------------------------------------------- 1 | import Alpine from './../src/index' 2 | 3 | export default Alpine 4 | -------------------------------------------------------------------------------- /packages/alpinejs/node_modules/.package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alpinejs", 3 | "version": "3.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "node_modules/@vue/reactivity": { 8 | "version": "3.0.11", 9 | "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.0.11.tgz", 10 | "integrity": "sha512-SKM3YKxtXHBPMf7yufXeBhCZ4XZDKP9/iXeQSC8bBO3ivBuzAi4aZi0bNoeE2IF2iGfP/AHEt1OU4ARj4ao/Xw==", 11 | "dependencies": { 12 | "@vue/shared": "3.0.11" 13 | } 14 | }, 15 | "node_modules/@vue/shared": { 16 | "version": "3.0.11", 17 | "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.0.11.tgz", 18 | "integrity": "sha512-b+zB8A2so8eCE0JsxjL24J7vdGl8rzPQ09hZNhystm+KqSbKcAej1A+Hbva1rCMmTTqA+hFnUSDc5kouEo0JzA==" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/alpinejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alpinejs", 3 | "version": "3.0.0-alpha.0", 4 | "description": "The rugged, minimal JavaScript framework", 5 | "author": "Caleb Porzio", 6 | "license": "MIT", 7 | "main": "dist/module.cjs.js", 8 | "module": "dist/module.esm.js", 9 | "dependencies": { 10 | "@vue/reactivity": "^3.0.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/alpinejs/src/alpine.js: -------------------------------------------------------------------------------- 1 | import { setReactivityEngine, reactive, effect, release, raw } from './reactivity' 2 | import { mapAttributes, directive, setPrefix as prefix } from './directives' 3 | import { setEvaluator, evaluate, evaluateLater } from './evaluator' 4 | import { start, addRootSelector, closestRoot } from './lifecycle' 5 | import { interceptor } from './interceptor' 6 | import { mutateDom } from './mutation' 7 | import { nextTick } from './nextTick' 8 | import { plugin } from './plugin' 9 | import { magic } from './magics' 10 | import { store } from './store' 11 | import { clone } from './clone' 12 | import { data } from './datas' 13 | 14 | let Alpine = { 15 | get reactive() { return reactive }, 16 | get release() { return release }, 17 | get effect() { return effect }, 18 | get raw() { return raw }, 19 | version: ALPINE_VERSION, 20 | setReactivityEngine, 21 | addRootSelector, 22 | mapAttributes, 23 | evaluateLater, 24 | setEvaluator, 25 | closestRoot, 26 | interceptor, 27 | mutateDom, 28 | directive, 29 | evaluate, 30 | nextTick, 31 | prefix, 32 | plugin, 33 | magic, 34 | store, 35 | start, 36 | clone, 37 | data, 38 | } 39 | 40 | export default Alpine 41 | -------------------------------------------------------------------------------- /packages/alpinejs/src/clone.js: -------------------------------------------------------------------------------- 1 | import { effect, release, overrideEffect } from "./reactivity" 2 | import { initTree, isRoot } from "./lifecycle" 3 | import { walk } from "./utils/walk" 4 | 5 | let isCloning = false 6 | 7 | export function skipDuringClone(callback) { 8 | return (...args) => isCloning || callback(...args) 9 | } 10 | 11 | export function onlyDuringClone(callback) { 12 | return (...args) => isCloning && callback(...args) 13 | } 14 | 15 | export function skipWalkingSubClone(callback) { 16 | return (...args) => isCloning || callback(...args) 17 | } 18 | 19 | export function interuptCrawl(callback) { 20 | return (...args) => isCloning || callback(...args) 21 | } 22 | 23 | export function clone(oldEl, newEl) { 24 | newEl._x_dataStack = oldEl._x_dataStack 25 | 26 | isCloning = true 27 | 28 | dontRegisterReactiveSideEffects(() => { 29 | cloneTree(newEl) 30 | }) 31 | 32 | isCloning = false 33 | } 34 | 35 | export function cloneTree(el) { 36 | let hasRunThroughFirstEl = false 37 | 38 | let shallowWalker = (el, callback) => { 39 | walk(el, (el, skip) => { 40 | if (hasRunThroughFirstEl && isRoot(el)) return skip() 41 | 42 | hasRunThroughFirstEl = true 43 | 44 | callback(el, skip) 45 | }) 46 | } 47 | 48 | initTree(el, shallowWalker) 49 | } 50 | 51 | function dontRegisterReactiveSideEffects(callback) { 52 | let cache = effect 53 | 54 | overrideEffect((callback, el) => { 55 | let storedEffect = cache(callback) 56 | 57 | release(storedEffect) 58 | }) 59 | 60 | callback() 61 | 62 | overrideEffect(cache) 63 | } 64 | -------------------------------------------------------------------------------- /packages/alpinejs/src/datas.js: -------------------------------------------------------------------------------- 1 | 2 | let datas = {} 3 | 4 | export function data(name, callback) { 5 | datas[name] = callback 6 | } 7 | 8 | export function getNamedDataProvider(name) { 9 | return datas[name] 10 | } 11 | -------------------------------------------------------------------------------- /packages/alpinejs/src/directives/index.js: -------------------------------------------------------------------------------- 1 | import './x-transition' 2 | import './x-ignore' 3 | import './x-effect' 4 | import './x-model' 5 | import './x-cloak' 6 | import './x-init' 7 | import './x-text' 8 | import './x-bind' 9 | import './x-data' 10 | import './x-show' 11 | import './x-for' 12 | import './x-ref' 13 | import './x-if' 14 | import './x-on' 15 | -------------------------------------------------------------------------------- /packages/alpinejs/src/directives/x-bind.js: -------------------------------------------------------------------------------- 1 | import { directive, directives, into, mapAttributes, prefix, startingWith } from '../directives' 2 | import { evaluateLater } from '../evaluator' 3 | import { mutateDom } from '../mutation' 4 | import bind from '../utils/bind' 5 | 6 | mapAttributes(startingWith(':', into(prefix('bind:')))) 7 | 8 | directive('bind', (el, { value, modifiers, expression, original }, { effect }) => { 9 | if (! value) return applyBindingsObject(el, expression, original, effect) 10 | 11 | if (value === 'key') return storeKeyForXFor(el, expression) 12 | 13 | let evaluate = evaluateLater(el, expression) 14 | 15 | effect(() => evaluate(result => { 16 | mutateDom(() => bind(el, value, result, modifiers)) 17 | })) 18 | }) 19 | 20 | function applyBindingsObject(el, expression, original, effect) { 21 | let getBindings = evaluateLater(el, expression) 22 | 23 | let cleanupRunners = [] 24 | 25 | effect(() => { 26 | while (cleanupRunners.length) cleanupRunners.pop()() 27 | 28 | getBindings(bindings => { 29 | let attributes = Object.entries(bindings).map(([name, value]) => ({ name, value })) 30 | 31 | directives(el, attributes, original).map(handle => { 32 | cleanupRunners.push(handle.runCleanups) 33 | 34 | handle() 35 | }) 36 | }) 37 | 38 | }) 39 | } 40 | 41 | function storeKeyForXFor(el, expression) { 42 | el._x_key_expression = expression 43 | } 44 | -------------------------------------------------------------------------------- /packages/alpinejs/src/directives/x-cloak.js: -------------------------------------------------------------------------------- 1 | import { directive, prefix } from '../directives' 2 | import { mutateDom } from '../mutation' 3 | import { nextTick } from '../nextTick' 4 | 5 | directive('cloak', el => nextTick(() => mutateDom(() => el.removeAttribute(prefix('cloak'))))) 6 | -------------------------------------------------------------------------------- /packages/alpinejs/src/directives/x-data.js: -------------------------------------------------------------------------------- 1 | import { directive, prefix } from '../directives' 2 | import { initInterceptors } from '../interceptor' 3 | import { getNamedDataProvider } from '../datas' 4 | import { addRootSelector } from '../lifecycle' 5 | import { skipDuringClone } from '../clone' 6 | import { addScopeToNode } from '../scope' 7 | import { injectMagics } from '../magics' 8 | import { reactive } from '../reactivity' 9 | import { evaluate } from '../evaluator' 10 | 11 | addRootSelector(() => `[${prefix('data')}]`) 12 | 13 | directive('data', skipDuringClone((el, { expression }, { cleanup }) => { 14 | expression = expression === '' ? '{}' : expression 15 | 16 | let dataProvider = getNamedDataProvider(expression) 17 | 18 | let data = {} 19 | 20 | if (dataProvider) { 21 | let magics = injectMagics({}, el) 22 | 23 | data = dataProvider.bind(magics)() 24 | } else { 25 | data = evaluate(el, expression) 26 | } 27 | 28 | initInterceptors(data) 29 | 30 | injectMagics(data, el) 31 | 32 | let reactiveData = reactive(data) 33 | 34 | let undo = addScopeToNode(el, reactiveData) 35 | 36 | if (reactiveData['init']) reactiveData['init']() 37 | 38 | cleanup(() => { 39 | undo() 40 | 41 | reactiveData['destroy'] && reactiveData['destroy']() 42 | }) 43 | })) 44 | -------------------------------------------------------------------------------- /packages/alpinejs/src/directives/x-effect.js: -------------------------------------------------------------------------------- 1 | import { directive } from '../directives' 2 | import { evaluateLater } from '../evaluator' 3 | 4 | directive('effect', (el, { expression }, { effect }) => effect(evaluateLater(el, expression))) 5 | -------------------------------------------------------------------------------- /packages/alpinejs/src/directives/x-if.js: -------------------------------------------------------------------------------- 1 | import { evaluateLater } from '../evaluator' 2 | import { setStyles } from '../utils/styles' 3 | import { directive } from '../directives' 4 | import { once } from '../utils/once' 5 | 6 | directive('if', (el, { modifiers, expression }, { effect, cleanup }) => { 7 | let evaluate = evaluateLater(el, expression) 8 | 9 | let show = () => { 10 | if (el._x_currentIfEl) return el._x_currentIfEl 11 | 12 | let clone = el.content.cloneNode(true).firstElementChild 13 | 14 | el.after(clone) 15 | 16 | el._x_currentIfEl = clone 17 | 18 | el._x_undoIf = () => { clone.remove(); delete el._x_currentIfEl } 19 | 20 | return clone 21 | } 22 | 23 | let hide = () => el._x_undoIf?.() || delete el._x_undoIf 24 | 25 | effect(() => evaluate(value => { 26 | value ? show() : hide() 27 | })) 28 | 29 | cleanup(() => el._x_undoIf && el._x_undoIf()) 30 | }) 31 | -------------------------------------------------------------------------------- /packages/alpinejs/src/directives/x-ignore.js: -------------------------------------------------------------------------------- 1 | import { directive } from "../directives" 2 | 3 | let handler = () => {} 4 | 5 | handler.inline = (el, { modifiers }, { cleanup }) => { 6 | modifiers.includes('self') 7 | ? el._x_ignore_self = true 8 | : el._x_ignore = true 9 | 10 | cleanup(() => { 11 | modifiers.includes('self') 12 | ? delete el._x_ignore_self 13 | : delete el._x_ignore 14 | }) 15 | } 16 | 17 | directive('ignore', handler) 18 | -------------------------------------------------------------------------------- /packages/alpinejs/src/directives/x-init.js: -------------------------------------------------------------------------------- 1 | import { directive, prefix } from "../directives"; 2 | import { addRootSelector } from "../lifecycle"; 3 | import { skipDuringClone } from "../clone"; 4 | import { evaluate } from "../evaluator"; 5 | 6 | addRootSelector(() => `[${prefix('init')}]`) 7 | 8 | directive('init', skipDuringClone((el, { expression }) => evaluate(el, expression, {}, false))) 9 | -------------------------------------------------------------------------------- /packages/alpinejs/src/directives/x-model.js: -------------------------------------------------------------------------------- 1 | import { evaluateLater } from '../evaluator' 2 | import { directive } from '../directives' 3 | import { mutateDom } from '../mutation' 4 | import bind from '../utils/bind' 5 | import on from '../utils/on' 6 | 7 | directive('model', (el, { modifiers, expression }, { effect, cleanup }) => { 8 | let evaluate = evaluateLater(el, expression) 9 | let assignmentExpression = `${expression} = rightSideOfExpression($event, ${expression})` 10 | let evaluateAssignment = evaluateLater(el, assignmentExpression) 11 | 12 | // If the element we are binding to is a select, a radio, or checkbox 13 | // we'll listen for the change event instead of the "input" event. 14 | var event = (el.tagName.toLowerCase() === 'select') 15 | || ['checkbox', 'radio'].includes(el.type) 16 | || modifiers.includes('lazy') 17 | ? 'change' : 'input' 18 | 19 | let assigmentFunction = generateAssignmentFunction(el, modifiers, expression) 20 | 21 | let removeListener = on(el, event, modifiers, (e) => { 22 | evaluateAssignment(() => {}, { scope: { 23 | '$event': e, 24 | rightSideOfExpression: assigmentFunction 25 | }}) 26 | }) 27 | 28 | cleanup(() => removeListener()) 29 | 30 | el._x_forceModelUpdate = () => { 31 | evaluate(value => { 32 | // If nested model key is undefined, set the default value to empty string. 33 | if (value === undefined && expression.match(/\./)) value = '' 34 | 35 | // @todo: This is nasty 36 | window.fromModel = true 37 | mutateDom(() => bind(el, 'value', value)) 38 | delete window.fromModel 39 | }) 40 | } 41 | 42 | effect(() => { 43 | // Don't modify the value of the input if it's focused. 44 | if (modifiers.includes('unintrusive') && document.activeElement.isSameNode(el)) return 45 | 46 | el._x_forceModelUpdate() 47 | }) 48 | }) 49 | 50 | function generateAssignmentFunction(el, modifiers, expression) { 51 | if (el.type === 'radio') { 52 | // Radio buttons only work properly when they share a name attribute. 53 | // People might assume we take care of that for them, because 54 | // they already set a shared "x-model" attribute. 55 | mutateDom(() => { 56 | if (! el.hasAttribute('name')) el.setAttribute('name', expression) 57 | }) 58 | } 59 | 60 | return (event, currentValue) => { 61 | return mutateDom(() => { 62 | // Check for event.detail due to an issue where IE11 handles other events as a CustomEvent. 63 | if (event instanceof CustomEvent && event.detail !== undefined) { 64 | return event.detail 65 | } else if (el.type === 'checkbox') { 66 | // If the data we are binding to is an array, toggle its value inside the array. 67 | if (Array.isArray(currentValue)) { 68 | let newValue = modifiers.includes('number') ? safeParseNumber(event.target.value) : event.target.value 69 | 70 | return event.target.checked ? currentValue.concat([newValue]) : currentValue.filter(el => ! checkedAttrLooseCompare(el, newValue)) 71 | } else { 72 | return event.target.checked 73 | } 74 | } else if (el.tagName.toLowerCase() === 'select' && el.multiple) { 75 | return modifiers.includes('number') 76 | ? Array.from(event.target.selectedOptions).map(option => { 77 | let rawValue = option.value || option.text 78 | return safeParseNumber(rawValue) 79 | }) 80 | : Array.from(event.target.selectedOptions).map(option => { 81 | return option.value || option.text 82 | }) 83 | } else { 84 | let rawValue = event.target.value 85 | return modifiers.includes('number') 86 | ? safeParseNumber(rawValue) 87 | : (modifiers.includes('trim') ? rawValue.trim() : rawValue) 88 | } 89 | }) 90 | } 91 | } 92 | 93 | function safeParseNumber(rawValue) { 94 | let number = rawValue ? parseFloat(rawValue) : null 95 | 96 | return isNumeric(number) ? number : rawValue 97 | } 98 | 99 | function checkedAttrLooseCompare(valueA, valueB) { 100 | return valueA == valueB 101 | } 102 | 103 | function isNumeric(subject){ 104 | return ! Array.isArray(subject) && ! isNaN(subject) 105 | } 106 | -------------------------------------------------------------------------------- /packages/alpinejs/src/directives/x-on.js: -------------------------------------------------------------------------------- 1 | import { directive, into, mapAttributes, prefix, startingWith } from '../directives' 2 | import { evaluateLater } from '../evaluator' 3 | import { skipDuringClone } from '../clone' 4 | import on from '../utils/on' 5 | 6 | mapAttributes(startingWith('@', into(prefix('on:')))) 7 | 8 | directive('on', skipDuringClone((el, { value, modifiers, expression }, { cleanup }) => { 9 | let evaluate = expression ? evaluateLater(el, expression) : () => {} 10 | 11 | let removeListener = on(el, value, modifiers, e => { 12 | evaluate(() => {}, { scope: { '$event': e }, params: [e] }) 13 | }) 14 | 15 | cleanup(() => removeListener()) 16 | })) 17 | -------------------------------------------------------------------------------- /packages/alpinejs/src/directives/x-ref.js: -------------------------------------------------------------------------------- 1 | import { closestRoot } from '../lifecycle' 2 | import { directive } from '../directives' 3 | 4 | function handler () {} 5 | 6 | handler.inline = (el, { expression }, { cleanup }) => { 7 | let root = closestRoot(el) 8 | 9 | if (! root._x_refs) root._x_refs = {} 10 | 11 | root._x_refs[expression] = el 12 | 13 | cleanup(() => delete root._x_refs[expression]) 14 | } 15 | 16 | directive('ref', handler) 17 | -------------------------------------------------------------------------------- /packages/alpinejs/src/directives/x-show.js: -------------------------------------------------------------------------------- 1 | import { evaluateLater } from '../evaluator' 2 | import { directive } from '../directives' 3 | import { mutateDom } from '../mutation' 4 | import { once } from '../utils/once' 5 | 6 | directive('show', (el, { modifiers, expression }, { effect }) => { 7 | let evaluate = evaluateLater(el, expression) 8 | 9 | let hide = () => mutateDom(() => { 10 | el.style.display = 'none' 11 | 12 | el._x_is_shown = false 13 | }) 14 | 15 | let show = () => mutateDom(() => { 16 | if (el.style.length === 1 && el.style.display === 'none') { 17 | el.removeAttribute('style') 18 | } else { 19 | el.style.removeProperty('display') 20 | } 21 | 22 | el._x_is_shown = true 23 | }) 24 | 25 | // We are wrapping this function in a setTimeout here to prevent 26 | // a race condition from happening where elements that have a 27 | // @click.away always view themselves as shown on the page. 28 | let clickAwayCompatibleShow = () => setTimeout(show) 29 | 30 | let toggle = once( 31 | value => value ? show() : hide(), 32 | value => { 33 | if (typeof el._x_toggleAndCascadeWithTransitions === 'function') { 34 | el._x_toggleAndCascadeWithTransitions(el, value, show, hide) 35 | } else { 36 | value ? clickAwayCompatibleShow() : hide() 37 | } 38 | } 39 | ) 40 | 41 | effect(() => evaluate(value => { 42 | if (modifiers.includes('immediate')) value ? clickAwayCompatibleShow() : hide() 43 | 44 | toggle(value) 45 | })) 46 | }) 47 | -------------------------------------------------------------------------------- /packages/alpinejs/src/directives/x-text.js: -------------------------------------------------------------------------------- 1 | import { evaluateLater } from '../evaluator' 2 | import { directive } from '../directives' 3 | import { mutateDom } from '../mutation' 4 | 5 | directive('text', (el, { expression }, { effect, cleanup }) => { 6 | let evaluate = evaluateLater(el, expression) 7 | 8 | effect(() => { 9 | evaluate(value => { 10 | mutateDom(() => { 11 | el.textContent = value 12 | }) 13 | }) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/alpinejs/src/evaluator.js: -------------------------------------------------------------------------------- 1 | import { closestDataStack, mergeProxies } from './scope' 2 | import { injectMagics } from './magics' 3 | 4 | export function evaluate(el, expression, extras = {}) { 5 | let result 6 | 7 | evaluateLater(el, expression)(value => result = value, extras) 8 | 9 | return result 10 | } 11 | 12 | export function evaluateLater(...args) { 13 | return theEvaluatorFunction(...args) 14 | } 15 | 16 | let theEvaluatorFunction = normalEvaluator 17 | 18 | export function setEvaluator(newEvaluator) { 19 | theEvaluatorFunction = newEvaluator 20 | } 21 | 22 | export function normalEvaluator(el, expression) { 23 | let overriddenMagics = {} 24 | 25 | injectMagics(overriddenMagics, el) 26 | 27 | let dataStack = [overriddenMagics, ...closestDataStack(el)] 28 | 29 | if (typeof expression === 'function') { 30 | return generateEvaluatorFromFunction(dataStack, expression) 31 | } 32 | 33 | let evaluator = generateEvaluatorFromString(dataStack, expression) 34 | 35 | return tryCatch.bind(null, el, expression, evaluator) 36 | } 37 | 38 | export function generateEvaluatorFromFunction(dataStack, func) { 39 | return (receiver = () => {}, { scope = {}, params = [] } = {}) => { 40 | let result = func.apply(mergeProxies([scope, ...dataStack]), params) 41 | 42 | runIfTypeOfFunction(receiver, result) 43 | } 44 | } 45 | 46 | let evaluatorMemo = {} 47 | 48 | function generateFunctionFromString(expression) { 49 | if (evaluatorMemo[expression]) { 50 | return evaluatorMemo[expression] 51 | } 52 | 53 | let AsyncFunction = Object.getPrototypeOf(async function(){}).constructor 54 | 55 | // Some expressions that are useful in Alpine are not valid as the right side of an expression. 56 | // Here we'll detect if the expression isn't valid for an assignement and wrap it in a self- 57 | // calling function so that we don't throw an error AND a "return" statement can b e used. 58 | let rightSideSafeExpression = 0 59 | // Support expressions starting with "if" statements like: "if (...) doSomething()" 60 | || /^[\n\s]*if.*\(.*\)/.test(expression) 61 | // Support expressions starting with "let/const" like: "let foo = 'bar'" 62 | || /^(let|const)/.test(expression) 63 | ? `(() => { ${expression} })()` 64 | : expression 65 | 66 | let func = new AsyncFunction(['__self', 'scope'], `with (scope) { __self.result = ${rightSideSafeExpression} }; __self.finished = true; return __self.result;`) 67 | 68 | evaluatorMemo[expression] = func 69 | 70 | return func 71 | } 72 | 73 | function generateEvaluatorFromString(dataStack, expression) { 74 | let func = generateFunctionFromString(expression) 75 | 76 | return (receiver = () => {}, { scope = {}, params = [] } = {}) => { 77 | func.result = undefined 78 | func.finished = false 79 | 80 | // Run the function. 81 | 82 | let completeScope = mergeProxies([ scope, ...dataStack ]) 83 | 84 | let promise = func(func, completeScope) 85 | 86 | // Check if the function ran synchronously, 87 | if (func.finished) { 88 | // Return the immediate result. 89 | runIfTypeOfFunction(receiver, func.result, completeScope, params) 90 | } else { 91 | // If not, return the result when the promise resolves. 92 | promise.then(result => { 93 | runIfTypeOfFunction(receiver, result, completeScope, params) 94 | }) 95 | } 96 | } 97 | } 98 | 99 | export function runIfTypeOfFunction(receiver, value, scope, params) { 100 | if (typeof value === 'function') { 101 | let result = value.apply(scope, params) 102 | 103 | if (result instanceof Promise) { 104 | result.then(i => runIfTypeOfFunction(receiver, i, scope, params)) 105 | } else { 106 | receiver(result) 107 | } 108 | } else { 109 | receiver(value) 110 | } 111 | } 112 | 113 | export function tryCatch(el, expression, callback, ...args) { 114 | try { 115 | return callback(...args) 116 | } catch (e) { 117 | console.warn(`Alpine Expression Error: ${e.message}\n\nExpression: "${expression}"\n\n`, el) 118 | 119 | throw e 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /packages/alpinejs/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * _ 3 | * /\ | | (_) (_) 4 | * / \ | |_ __ _ _ __ ___ _ ___ 5 | * / /\ \ | | '_ \| | '_ \ / _ \ | / __| 6 | * / ____ \| | |_) | | | | | __/_| \__ \ 7 | * /_/ \_\_| .__/|_|_| |_|\___(_) |___/ 8 | * | | _/ | 9 | * |_| |__/ 10 | * 11 | * Let's build Alpine together. It's easier than you think. 12 | * For starters, we'll import Alpine's core. This is the 13 | * object that will expose all of Alpine's public API. 14 | */ 15 | import Alpine from './alpine' 16 | 17 | /** 18 | * _______________________________________________________ 19 | * The Evaluator 20 | * ------------------------------------------------------- 21 | * 22 | * Now we're ready to bootstrap Alpine's evaluation system. 23 | * It's the function that converts raw JavaScript string 24 | * expressions like @click="toggle()", into actual JS. 25 | */ 26 | import { normalEvaluator } from './evaluator' 27 | 28 | Alpine.setEvaluator(normalEvaluator) 29 | 30 | /** 31 | * _______________________________________________________ 32 | * The Reactivity Engine 33 | * ------------------------------------------------------- 34 | * 35 | * This is the reactivity core of Alpine. It's the part of 36 | * Alpine that triggers an element with x-text="message" 37 | * to update its inner text when "message" is changed. 38 | */ 39 | import { reactive, effect, stop, toRaw } from '@vue/reactivity' 40 | 41 | Alpine.setReactivityEngine({ reactive, effect, release: stop, raw: toRaw }) 42 | 43 | /** 44 | * _______________________________________________________ 45 | * The Magics 46 | * ------------------------------------------------------- 47 | * 48 | * Yeah, we're calling them magics here like they're nouns. 49 | * These are the properties that are magically available 50 | * to all the Alpine expressions, within your web app. 51 | */ 52 | import './magics/index' 53 | 54 | /** 55 | * _______________________________________________________ 56 | * The Directives 57 | * ------------------------------------------------------- 58 | * 59 | * Now that the core is all set up, we can register Alpine 60 | * directives like x-text or x-on that form the basis of 61 | * how Alpine adds behavior to an app's static markup. 62 | */ 63 | import './directives/index' 64 | 65 | /** 66 | * _______________________________________________________ 67 | * The Alpine Global 68 | * ------------------------------------------------------- 69 | * 70 | * Now that we have set everything up internally, anything 71 | * Alpine-related that will need to be accessed on-going 72 | * will be made available through the "Alpine" global. 73 | */ 74 | export default Alpine 75 | -------------------------------------------------------------------------------- /packages/alpinejs/src/interceptor.js: -------------------------------------------------------------------------------- 1 | export function initInterceptors(data) { 2 | let isObject = val => typeof val === 'object' && !Array.isArray(val) && val !== null 3 | 4 | let recurse = (obj, basePath = '') => { 5 | Object.entries(obj).forEach(([key, value]) => { 6 | let path = basePath === '' ? key : `${basePath}.${key}` 7 | 8 | if (typeof value === 'function' && value.interceptor) { 9 | let result = value(key, path) 10 | 11 | Object.defineProperty(obj, key, result[0]) 12 | } 13 | 14 | if (isObject(value)) { 15 | recurse(value, path) 16 | } 17 | }) 18 | } 19 | 20 | return recurse(data) 21 | } 22 | 23 | export function interceptor(callback, mutateFunc = () => {}) { 24 | return initialValue => { 25 | function func(key, path) { 26 | let parentFunc = func.parent 27 | ? func.parent 28 | : (key, path) => ([{}, { initer() {}, setter() {} }]) 29 | 30 | let [parentNoop, { initer: parentIniter, setter: parentSetter, initialValue: parentInitialValue }] = parentFunc(key, path) 31 | 32 | let store = parentInitialValue === undefined ? initialValue : parentInitialValue 33 | 34 | let { init: initer, set: setter } = callback(key, path) 35 | 36 | let inited = false 37 | 38 | let setValue = i => store = i 39 | let reactiveSetValue = function (i) { this[key] = i } 40 | 41 | let setup = (context) => { 42 | if (inited) return 43 | 44 | parentIniter.bind(context)(store, setValue, reactiveSetValue.bind(context)) 45 | initer.bind(context)(store, setValue, reactiveSetValue.bind(context)) 46 | 47 | inited = true 48 | } 49 | 50 | return [{ 51 | get() { 52 | setup(this) 53 | 54 | return store 55 | }, 56 | set(value) { 57 | setup(this) 58 | 59 | parentSetter.bind(this)(value, setValue, reactiveSetValue.bind(this)) 60 | setter.bind(this)(value, setValue, reactiveSetValue.bind(this)) 61 | }, 62 | enumerable: true, 63 | configurable: true, 64 | }, { initer, setter, initialValue }] 65 | } 66 | 67 | func.interceptor = true 68 | 69 | mutateFunc(func) 70 | 71 | if (typeof initialValue === 'function' && initialValue.interceptor) { 72 | func.parent = initialValue 73 | } 74 | 75 | return func 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/alpinejs/src/lifecycle.js: -------------------------------------------------------------------------------- 1 | import { startObservingMutations, onAttributesAdded, onElAdded, onElRemoved } from "./mutation" 2 | import { deferHandlingDirectives, directives } from "./directives" 3 | import { dispatch } from './utils/dispatch' 4 | import { nextTick } from "./nextTick" 5 | import { walk } from "./utils/walk" 6 | import { warn } from './utils/warn' 7 | 8 | export function start() { 9 | if (! document.body) warn('Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine\'s ` 25 | 26 | ``` 27 | 28 | 29 | ### Module import 30 | 31 | ```js 32 | import Alpine from '@alpinejs/csp' 33 | 34 | window.Alpine = Alpine 35 | window.Alpine.start() 36 | ``` 37 | 38 | 39 | ## Restrictions 40 | 41 | Because Alpine can no longer interpret strings as plain JavaScript, it has to parse and construct JavaScript functions from them manually. 42 | 43 | Because of this limitation, you must `Alpine.data` to register your `x-data` objects, and must reference properties and methods from it by key only. 44 | 45 | For example, an inline component like this will not work. 46 | 47 | ```html 48 | 49 |
50 | 51 | 52 | 53 |
54 | ``` 55 | 56 | However, breaking out the expressions into external APIs, the following is valid with the CSP build: 57 | 58 | ```html 59 | 60 |
61 | 62 | 63 | 64 |
65 | ``` 66 | ```js 67 | Alpine.data('counter', () => ({ 68 | count: 1, 69 | 70 | increment() { this.count++ } 71 | })) 72 | ``` 73 | -------------------------------------------------------------------------------- /packages/docs/src/en/advanced/reactivity.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 1 3 | title: Reactivity 4 | --- 5 | 6 | # Reactivity 7 | 8 | Alpine is "reactive" in the sense that when you change a peice of data, everything that depends on that data "reacts" automatically to that change. 9 | 10 | Every bit of reactivity that takes place in Alpine, happens because of two very important reactive functions in Alpine's core: `Alpine.reactive()`, and `Alpine.effect()`. 11 | 12 | > Alpine uses VueJS's reactivity engine under the hood to provide these functions. 13 | > [→ Read more about @vue/reactivity](https://github.com/vuejs/vue-next/tree/master/packages/reactivity) 14 | 15 | Understanding these two functions will give you super powers as an Alpine developer, but also just as a web developer in general. 16 | 17 | 18 | ## Alpine.reactive() 19 | 20 | Let's first look at `Alpine.reactive()`. This function accepts a JavaScript object as its parameter and returns a "reactive" version of that object. For example: 21 | 22 | ```js 23 | let data = { count: 1 } 24 | 25 | let reactiveData = Alpine.reactive(data) 26 | ``` 27 | 28 | Under the hood, when `Alpine.reactive` receives `data`, it wraps it inside a custom JavaScript proxy. 29 | 30 | A proxy is a special kind of object in JavaScript that can intercept "get" and "set" calls to a JavaScript objct. 31 | 32 | [→ Read more about JavaScript proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) 33 | 34 | At face value, `reactiveData` should behave exactly like `data`. For example: 35 | 36 | ```js 37 | console.log(data.count) // 1 38 | console.log(reactiveData.count) // 1 39 | 40 | reactiveData.count = 2 41 | 42 | console.log(data.count) // 2 43 | console.log(reactiveData.count) // 2 44 | ``` 45 | 46 | What you see here is that because `reactiveData` is a thin wrapper around `data`, any attempts to get or set a property will behave exactly as if you had interacted with `data` directly. 47 | 48 | The main difference here is that any time you modify or retreive (get or set) a value from `reactiveData`, Alpine is aware of it and can execute any other logic that depends on this data. 49 | 50 | `Alpine.reactive` is only the first half of the story. `Alpine.effect` is the other half, let's dig in. 51 | 52 | 53 | ## Alpine.effect() 54 | 55 | `Alpine.effect` accepts a single callback function. As soon as `Alpine.effect` is called, it will run the provided function, but actively look for any interactions with reactive data. If it detects an interaction (a get or set from the aforementioned reactive proxy) it will keep track of it and make sure to re-run the callback if any of reactive data changes in the future. For example: 56 | 57 | ```js 58 | let data = Alpine.reactive({ count: 1 }) 59 | 60 | Alpine.effect(() => { 61 | console.log(data.count) 62 | }) 63 | ``` 64 | 65 | When this code is firt run, "1" will be logged to the console. Any time `data.count` changes, it's value will be logged to the console again. 66 | 67 | This is the mechanism that unlocks all of the reactivity at the core of Alpine. 68 | 69 | To connect the dots further, let's look at a simple "counter" component example without using Alpine syntax at all, only using `Alpine.reactive` and `Alpine.effect`: 70 | 71 | ```html 72 | 73 | 74 | Count: 75 | ``` 76 | ```js 77 | let button = document.querySelector('button') 78 | let span = document.querySelector('span') 79 | 80 | let data = Alpine.reactive({ count: 1 }) 81 | 82 | Alpine.effect(() => { 83 | span.textContent = data.count 84 | }) 85 | 86 | button.addEventListener('click', () => { 87 | data.count = data.count + 1 88 | }) 89 | ``` 90 | 91 | 92 |
93 | 94 | 95 |
Count:
96 |
97 | 98 | 99 | As you can see, you can make any data reactive, and you can also wrap any functionality in `Alpine.effect`. 100 | 101 | This combination unlocks an incredibly powerful programming paradaigm for web development. Run wild and free. 102 | -------------------------------------------------------------------------------- /packages/docs/src/en/directives.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 4 3 | title: Directives 4 | prefix: x- 5 | font-type: mono 6 | type: sub-directory 7 | --- 8 | -------------------------------------------------------------------------------- /packages/docs/src/en/directives/cloak.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 12 3 | title: cloak 4 | --- 5 | 6 | # `x-cloak` 7 | 8 | Sometimes, when you're using AlpineJS for a part of your template, there is a "blip" where you might see your uninitialized template after the page loads, but before Alpine loads. 9 | 10 | `x-cloak` addresses this scenerio by hiding the element it's attached to until Alpine is fully loaded on the page. 11 | 12 | For `x-cloak` to work however, you must add the following CSS to the page. 13 | 14 | ```css 15 | [x-cloak] { display: none !important; } 16 | ``` 17 | 18 | Now, the following example will hide the `` tag until Alpine has set its text content to the `message` property. 19 | 20 | ```html 21 | 22 | ``` 23 | 24 | When Alpine loads on the page, it removes all `x-cloak` property from the element, which also removes the `display: none;` applied by CSS, therefore showing the element. 25 | 26 | If you'd like to achieve this same behavior, but avoid having to include a global style, you can use the following cool, but admittadly odd trick: 27 | 28 | ```html 29 | 32 | ``` 33 | 34 | This will achieve the same goal as `x-cloak` by just leveraging the way `x-if` works. 35 | 36 | Because `