浏览代码

Merge branch 'main' into livewire3

Caleb Porzio 2 年之前
父节点
当前提交
ae686675fa
共有 65 个文件被更改,包括 1344 次插入485 次删除
  1. 3 3
      .github/workflows/run-tests.yml
  2. 1 1
      README.md
  3. 662 284
      package-lock.json
  4. 1 5
      package.json
  5. 1 1
      packages/alpinejs/package.json
  6. 0 2
      packages/alpinejs/src/alpine.js
  7. 1 1
      packages/alpinejs/src/clone.js
  8. 2 16
      packages/alpinejs/src/directives.js
  9. 1 1
      packages/alpinejs/src/directives/x-data.js
  10. 14 4
      packages/alpinejs/src/directives/x-for.js
  11. 13 6
      packages/alpinejs/src/directives/x-model.js
  12. 6 6
      packages/alpinejs/src/directives/x-transition.js
  13. 1 1
      packages/alpinejs/src/entangle.js
  14. 3 5
      packages/alpinejs/src/evaluator.js
  15. 6 0
      packages/alpinejs/src/lifecycle.js
  16. 13 6
      packages/alpinejs/src/magics.js
  17. 4 2
      packages/alpinejs/src/plugin.js
  18. 4 1
      packages/alpinejs/src/scheduler.js
  19. 2 10
      packages/alpinejs/src/scope.js
  20. 19 0
      packages/alpinejs/src/utils/bind.js
  21. 18 15
      packages/alpinejs/src/utils/on.js
  22. 1 1
      packages/collapse/package.json
  23. 1 1
      packages/docs/package.json
  24. 2 2
      packages/docs/src/en/advanced/csp.md
  25. 1 1
      packages/docs/src/en/advanced/extending.md
  26. 5 5
      packages/docs/src/en/directives/bind.md
  27. 3 3
      packages/docs/src/en/directives/data.md
  28. 11 0
      packages/docs/src/en/directives/model.md
  29. 15 2
      packages/docs/src/en/directives/on.md
  30. 6 6
      packages/docs/src/en/essentials/installation.md
  31. 1 1
      packages/docs/src/en/essentials/lifecycle.md
  32. 14 1
      packages/docs/src/en/plugins/mask.md
  33. 0 6
      packages/docs/src/en/ui.md
  34. 1 1
      packages/focus/package.json
  35. 8 3
      packages/focus/src/index.js
  36. 1 1
      packages/intersect/package.json
  37. 1 1
      packages/mask/package.json
  38. 48 38
      packages/mask/src/index.js
  39. 1 1
      packages/morph/package.json
  40. 31 0
      packages/morph/src/morph.js
  41. 4 1
      packages/navigate/package.json
  42. 1 1
      packages/persist/package.json
  43. 1 1
      packages/ui/package.json
  44. 1 1
      packages/ui/src/combobox.js
  45. 1 1
      packages/ui/src/disclosure.js
  46. 1 1
      packages/ui/src/listbox.js
  47. 1 1
      packages/ui/src/menu.js
  48. 1 1
      packages/ui/src/radio.js
  49. 1 1
      packages/ui/src/switch.js
  50. 1 1
      packages/ui/src/tabs.js
  51. 6 17
      scripts/build.js
  52. 31 0
      tests/cypress/integration/clone.spec.js
  53. 3 3
      tests/cypress/integration/directives/x-bind-style.spec.js
  54. 23 4
      tests/cypress/integration/directives/x-bind.spec.js
  55. 46 0
      tests/cypress/integration/directives/x-for.spec.js
  56. 37 0
      tests/cypress/integration/directives/x-if.spec.js
  57. 59 0
      tests/cypress/integration/directives/x-model.spec.js
  58. 68 2
      tests/cypress/integration/directives/x-on.spec.js
  59. 60 0
      tests/cypress/integration/directives/x-transition.spec.js
  60. 2 2
      tests/cypress/integration/entangle.spec.js
  61. 45 0
      tests/cypress/integration/plugins/mask.spec.js
  62. 20 0
      tests/cypress/integration/plugins/morph.spec.js
  63. 1 1
      tests/cypress/integration/plugins/navigate.spec.js
  64. 1 1
      tests/cypress/integration/plugins/ui/popover.spec.js
  65. 3 1
      tests/cypress/utils.js

+ 3 - 3
.github/workflows/run-tests.yml

@@ -4,10 +4,10 @@ jobs:
   build:
   build:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
-      - uses: actions/checkout@v1
-      - uses: actions/setup-node@v2
+      - uses: actions/checkout@v3
+      - uses: actions/setup-node@v3
         with:
         with:
-          node-version: '15'
+          node-version: '18'
       - run: npm install
       - run: npm install
       - run: npm run build
       - run: npm run build
       - run: npm run test
       - run: npm run test

+ 1 - 1
README.md

@@ -8,7 +8,7 @@ Stay here for contribution-related information.
 
 
 > Looking for V2 docs? [here they are](https://github.com/alpinejs/alpine/tree/v2.8.2)
 > Looking for V2 docs? [here they are](https://github.com/alpinejs/alpine/tree/v2.8.2)
 
 
-<p align="center"><a href="https://alpinejs.dev/patterns"><img src="/hero.jpg" alt="Alpine Compoenent Patterns"></a></p>
+<p align="center"><a href="https://alpinejs.dev/patterns"><img src="/hero.jpg" alt="Alpine Component Patterns"></a></p>
 
 
 ## Contribution Guide:
 ## Contribution Guide:
 
 

文件差异内容过多而无法显示
+ 662 - 284
package-lock.json


+ 1 - 5
package.json

@@ -5,12 +5,11 @@
     ],
     ],
     "devDependencies": {
     "devDependencies": {
         "axios": "^0.21.1",
         "axios": "^0.21.1",
-        "brotli-size": "^4.0.0",
         "chalk": "^4.1.1",
         "chalk": "^4.1.1",
         "cypress": "^7.0.0",
         "cypress": "^7.0.0",
         "cypress-plugin-tab": "^1.0.5",
         "cypress-plugin-tab": "^1.0.5",
         "dot-json": "^1.2.2",
         "dot-json": "^1.2.2",
-        "esbuild": "^0.8.39",
+        "esbuild": "~0.16.17",
         "jest": "^26.6.3"
         "jest": "^26.6.3"
     },
     },
     "scripts": {
     "scripts": {
@@ -21,8 +20,5 @@
         "jest": "jest test",
         "jest": "jest test",
         "update-docs": "node ./scripts/update-docs.js",
         "update-docs": "node ./scripts/update-docs.js",
         "release": "node ./scripts/release.js"
         "release": "node ./scripts/release.js"
-    },
-    "dependencies": {
-        "nprogress": "^0.2.0"
     }
     }
 }
 }

+ 1 - 1
packages/alpinejs/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "alpinejs",
     "name": "alpinejs",
-    "version": "3.10.5",
+    "version": "3.12.2",
     "description": "The rugged, minimal JavaScript framework",
     "description": "The rugged, minimal JavaScript framework",
     "homepage": "https://alpinejs.dev",
     "homepage": "https://alpinejs.dev",
     "repository": {
     "repository": {

+ 0 - 2
packages/alpinejs/src/alpine.js

@@ -11,7 +11,6 @@ import { getBinding as bound } from './utils/bind'
 import { debounce } from './utils/debounce'
 import { debounce } from './utils/debounce'
 import { throttle } from './utils/throttle'
 import { throttle } from './utils/throttle'
 import { setStyles } from './utils/styles'
 import { setStyles } from './utils/styles'
-import { entangle } from './entangle'
 import { nextTick } from './nextTick'
 import { nextTick } from './nextTick'
 import { walk } from './utils/walk'
 import { walk } from './utils/walk'
 import { plugin } from './plugin'
 import { plugin } from './plugin'
@@ -53,7 +52,6 @@ let Alpine = {
     setStyles, // INTERNAL
     setStyles, // INTERNAL
     mutateDom,
     mutateDom,
     directive,
     directive,
-    entangle,
     throttle,
     throttle,
     debounce,
     debounce,
     evaluate,
     evaluate,

+ 1 - 1
packages/alpinejs/src/clone.js

@@ -2,7 +2,7 @@ import { effect, release, overrideEffect } from "./reactivity"
 import { initTree, isRoot } from "./lifecycle"
 import { initTree, isRoot } from "./lifecycle"
 import { walk } from "./utils/walk"
 import { walk } from "./utils/walk"
 
 
-let isCloning = false
+export let isCloning = false
 
 
 export function skipDuringClone(callback, fallback = () => {}) {
 export function skipDuringClone(callback, fallback = () => {}) {
     return (...args) => isCloning ? fallback(...args) : callback(...args)
     return (...args) => isCloning ? fallback(...args) : callback(...args)

+ 2 - 16
packages/alpinejs/src/directives.js

@@ -27,11 +27,8 @@ export function directive(name, callback) {
                 );
                 );
                 return;
                 return;
             }
             }
-            const pos = directiveOrder.indexOf(directive)
-                ?? directiveOrder.indexOf('DEFAULT');
-            if (pos >= 0) {
-                directiveOrder.splice(pos, 0, name);
-            }
+            const pos = directiveOrder.indexOf(directive);
+            directiveOrder.splice(pos >= 0 ? pos : directiveOrder.indexOf('DEFAULT'), 0, name);
         }
         }
     }
     }
 }
 }
@@ -206,20 +203,9 @@ let directiveOrder = [
     'ref',
     'ref',
     'data',
     'data',
     'id',
     'id',
-    // @todo: provide better directive ordering mechanisms so
-    // that I don't have to manually add things like "tabs"
-    // to the order list...
-    'radio',
-    'tabs',
-    'switch',
-    'disclosure',
-    'menu',
-    'listbox',
-    'combobox',
     'bind',
     'bind',
     'init',
     'init',
     'for',
     'for',
-    'mask',
     'model',
     'model',
     'modelable',
     'modelable',
     'transition',
     'transition',

+ 1 - 1
packages/alpinejs/src/directives/x-data.js

@@ -21,7 +21,7 @@ directive('data', skipDuringClone((el, { expression }, { cleanup }) => {
 
 
     let data = evaluate(el, expression, { scope: dataProviderContext })
     let data = evaluate(el, expression, { scope: dataProviderContext })
 
 
-    if (data === undefined) data = {}
+    if (data === undefined || data === true) data = {}
 
 
     injectMagics(data, el)
     injectMagics(data, el)
 
 

+ 14 - 4
packages/alpinejs/src/directives/x-for.js

@@ -1,4 +1,4 @@
-import { addScopeToNode, refreshScope } from '../scope'
+import { addScopeToNode } from '../scope'
 import { evaluateLater } from '../evaluator'
 import { evaluateLater } from '../evaluator'
 import { directive } from '../directives'
 import { directive } from '../directives'
 import { reactive } from '../reactivity'
 import { reactive } from '../reactivity'
@@ -158,6 +158,8 @@ function loop(el, iteratorNames, evaluateItems, evaluateKey) {
             let marker = document.createElement('div')
             let marker = document.createElement('div')
 
 
             mutateDom(() => {
             mutateDom(() => {
+                if (! elForSpot) warn(`x-for ":key" is undefined or invalid`, templateEl)
+
                 elForSpot.after(marker)
                 elForSpot.after(marker)
                 elInSpot.after(elForSpot)
                 elInSpot.after(elForSpot)
                 elForSpot._x_currentIfEl && elForSpot.after(elForSpot._x_currentIfEl)
                 elForSpot._x_currentIfEl && elForSpot.after(elForSpot._x_currentIfEl)
@@ -166,7 +168,7 @@ function loop(el, iteratorNames, evaluateItems, evaluateKey) {
                 marker.remove()
                 marker.remove()
             })
             })
 
 
-            refreshScope(elForSpot, scopes[keys.indexOf(keyForSpot)])
+            elForSpot._x_refreshXForScope(scopes[keys.indexOf(keyForSpot)])
         }
         }
 
 
         // We can now create and add new elements.
         // We can now create and add new elements.
@@ -183,7 +185,15 @@ function loop(el, iteratorNames, evaluateItems, evaluateKey) {
 
 
             let clone = document.importNode(templateEl.content, true).firstElementChild
             let clone = document.importNode(templateEl.content, true).firstElementChild
 
 
-            addScopeToNode(clone, reactive(scope), templateEl)
+            let reactiveScope = reactive(scope)
+
+            addScopeToNode(clone, reactiveScope, templateEl)
+
+            clone._x_refreshXForScope = (newScope) => {
+                Object.entries(newScope).forEach(([key, value]) => {
+                    reactiveScope[key] = value
+                })
+            }
 
 
             mutateDom(() => {
             mutateDom(() => {
                 lastEl.after(clone)
                 lastEl.after(clone)
@@ -202,7 +212,7 @@ function loop(el, iteratorNames, evaluateItems, evaluateKey) {
         // data it depends on in case the data has changed in an
         // data it depends on in case the data has changed in an
         // "unobservable" way.
         // "unobservable" way.
         for (let i = 0; i < sames.length; i++) {
         for (let i = 0; i < sames.length; i++) {
-            refreshScope(lookup[sames[i]], scopes[keys.indexOf(sames[i])])
+            lookup[sames[i]]._x_refreshXForScope(scopes[keys.indexOf(sames[i])])
         }
         }
 
 
         // Now we'll log the keys (and the order they're in) for comparing
         // Now we'll log the keys (and the order they're in) for comparing

+ 13 - 6
packages/alpinejs/src/directives/x-model.js

@@ -5,6 +5,7 @@ import { nextTick } from '../nextTick'
 import bind from '../utils/bind'
 import bind from '../utils/bind'
 import on from '../utils/on'
 import on from '../utils/on'
 import { warn } from '../utils/warn'
 import { warn } from '../utils/warn'
+import { isCloning } from '../clone'
 
 
 directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
 directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
     let scopeTarget = el
     let scopeTarget = el
@@ -45,7 +46,7 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
             })
             })
         }
         }
     }
     }
-
+    
     if (typeof expression === 'string' && el.type === 'radio') {
     if (typeof expression === 'string' && el.type === 'radio') {
         // Radio buttons only work properly when they share a name attribute.
         // Radio buttons only work properly when they share a name attribute.
         // People might assume we take care of that for them, because
         // People might assume we take care of that for them, because
@@ -62,10 +63,16 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
         || modifiers.includes('lazy')
         || modifiers.includes('lazy')
             ? 'change' : 'input'
             ? 'change' : 'input'
 
 
-    let removeListener = on(el, event, modifiers, (e) => {
+    // We only want to register the event listener when we're not cloning, since the
+    // mutation observer handles initializing the x-model directive already when
+    // the element is inserted into the DOM. Otherwise we register it twice.
+    let removeListener = isCloning ? () => {} : on(el, event, modifiers, (e) => {
         setValue(getInputValue(el, modifiers, e, getValue()))
         setValue(getInputValue(el, modifiers, e, getValue()))
     })
     })
-
+    
+    if (modifiers.includes('fill') && [null, ''].includes(getValue())) {
+        el.dispatchEvent(new Event(event, {}));
+    }
     // Register the listener removal callback on the element, so that
     // Register the listener removal callback on the element, so that
     // in addition to the cleanup function, x-modelable may call it.
     // in addition to the cleanup function, x-modelable may call it.
     // Also, make this a keyed object if we decide to reintroduce
     // Also, make this a keyed object if we decide to reintroduce
@@ -124,9 +131,9 @@ function getInputValue(el, modifiers, event, currentValue) {
         // Check for event.detail due to an issue where IE11 handles other events as a CustomEvent.
         // Check for event.detail due to an issue where IE11 handles other events as a CustomEvent.
         // Safari autofill triggers event as CustomEvent and assigns value to target
         // Safari autofill triggers event as CustomEvent and assigns value to target
         // so we return event.target.value instead of event.detail
         // so we return event.target.value instead of event.detail
-        if (event instanceof CustomEvent && event.detail !== undefined) {
-            return typeof event.detail != 'undefined' ? event.detail : event.target.value
-        } else if (el.type === 'checkbox') {
+        if (event instanceof CustomEvent && event.detail !== undefined)
+            return event.detail ?? event.target.value
+        else if (el.type === 'checkbox') {
             // If the data we are binding to is an array, toggle its value inside the array.
             // If the data we are binding to is an array, toggle its value inside the array.
             if (Array.isArray(currentValue)) {
             if (Array.isArray(currentValue)) {
                 let newValue = modifiers.includes('number') ? safeParseNumber(event.target.value) : event.target.value
                 let newValue = modifiers.includes('number') ? safeParseNumber(event.target.value) : event.target.value

+ 6 - 6
packages/alpinejs/src/directives/x-transition.js

@@ -7,8 +7,8 @@ import { once } from '../utils/once'
 
 
 directive('transition', (el, { value, modifiers, expression }, { evaluate }) => {
 directive('transition', (el, { value, modifiers, expression }, { evaluate }) => {
     if (typeof expression === 'function') expression = evaluate(expression)
     if (typeof expression === 'function') expression = evaluate(expression)
-
-    if (! expression) {
+    if (expression === false) return
+    if (!expression || typeof expression === 'boolean') {
         registerTransitionsFromHelper(el, modifiers, value)
         registerTransitionsFromHelper(el, modifiers, value)
     } else {
     } else {
         registerTransitionsFromClassString(el, expression, value)
         registerTransitionsFromClassString(el, expression, value)
@@ -50,7 +50,7 @@ function registerTransitionsFromHelper(el, modifiers, stage) {
     let wantsScale = wantsAll || modifiers.includes('scale')
     let wantsScale = wantsAll || modifiers.includes('scale')
     let opacityValue = wantsOpacity ? 0 : 1
     let opacityValue = wantsOpacity ? 0 : 1
     let scaleValue = wantsScale ? modifierValue(modifiers, 'scale', 95) / 100 : 1
     let scaleValue = wantsScale ? modifierValue(modifiers, 'scale', 95) / 100 : 1
-    let delay = modifierValue(modifiers, 'delay', 0)
+    let delay = modifierValue(modifiers, 'delay', 0) / 1000
     let origin = modifierValue(modifiers, 'origin', 'center')
     let origin = modifierValue(modifiers, 'origin', 'center')
     let property = 'opacity, transform'
     let property = 'opacity, transform'
     let durationIn = modifierValue(modifiers, 'duration', 150) / 1000
     let durationIn = modifierValue(modifiers, 'duration', 150) / 1000
@@ -60,7 +60,7 @@ function registerTransitionsFromHelper(el, modifiers, stage) {
     if (transitioningIn) {
     if (transitioningIn) {
         el._x_transition.enter.during = {
         el._x_transition.enter.during = {
             transformOrigin: origin,
             transformOrigin: origin,
-            transitionDelay: delay,
+            transitionDelay: `${delay}s`,
             transitionProperty: property,
             transitionProperty: property,
             transitionDuration: `${durationIn}s`,
             transitionDuration: `${durationIn}s`,
             transitionTimingFunction: easing,
             transitionTimingFunction: easing,
@@ -80,7 +80,7 @@ function registerTransitionsFromHelper(el, modifiers, stage) {
     if (transitioningOut) {
     if (transitioningOut) {
         el._x_transition.leave.during = {
         el._x_transition.leave.during = {
             transformOrigin: origin,
             transformOrigin: origin,
-            transitionDelay: delay,
+            transitionDelay: `${delay}s`,
             transitionProperty: property,
             transitionProperty: property,
             transitionDuration: `${durationOut}s`,
             transitionDuration: `${durationOut}s`,
             transitionTimingFunction: easing,
             transitionTimingFunction: easing,
@@ -318,7 +318,7 @@ export function modifierValue(modifiers, key, fallback) {
         if (isNaN(rawValue)) return fallback
         if (isNaN(rawValue)) return fallback
     }
     }
 
 
-    if (key === 'duration') {
+    if (key === 'duration' || key === 'delay') {
         // Support x-transition.duration.500ms && duration.500
         // Support x-transition.duration.500ms && duration.500
         let match = rawValue.match(/([0-9]+)ms/)
         let match = rawValue.match(/([0-9]+)ms/)
         if (match) return match[1]
         if (match) return match[1]

+ 1 - 1
packages/alpinejs/src/entangle.js

@@ -2,7 +2,7 @@ import { effect, release } from './reactivity'
 
 
 export function entangle({ get: outerGet, set: outerSet }, { get: innerGet, set: innerSet }) {
 export function entangle({ get: outerGet, set: outerSet }, { get: innerGet, set: innerSet }) {
     let firstRun = true
     let firstRun = true
-    let outerHash, innerHash
+    let outerHash, innerHash, outerHashLatest, innerHashLatest
 
 
     let reference = effect(() => {
     let reference = effect(() => {
         let outer, inner
         let outer, inner

+ 3 - 5
packages/alpinejs/src/evaluator.js

@@ -39,11 +39,9 @@ export function normalEvaluator(el, expression) {
 
 
     let dataStack = [overriddenMagics, ...closestDataStack(el)]
     let dataStack = [overriddenMagics, ...closestDataStack(el)]
 
 
-    if (typeof expression === 'function') {
-        return generateEvaluatorFromFunction(dataStack, expression)
-    }
-
-    let evaluator = generateEvaluatorFromString(dataStack, expression, el)
+    let evaluator = (typeof expression === 'function')
+        ? generateEvaluatorFromFunction(dataStack, expression)
+        : generateEvaluatorFromString(dataStack, expression, el)
 
 
     return tryCatch.bind(null, el, expression, evaluator)
     return tryCatch.bind(null, el, expression, evaluator)
 }
 }

+ 6 - 0
packages/alpinejs/src/lifecycle.js

@@ -5,7 +5,13 @@ import { walk } from "./utils/walk"
 import { warn } from './utils/warn'
 import { warn } from './utils/warn'
 import Alpine from "./alpine"
 import Alpine from "./alpine"
 
 
+let started = false
+
 export function start() {
 export function start() {
+    if (started) warn('Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems.')
+
+    started = true
+
     if (! document.body) warn('Unable to initialize. Trying to load Alpine before `<body>` is available. Did you forget to add `defer` in Alpine\'s `<script>` tag?')
     if (! document.body) warn('Unable to initialize. Trying to load Alpine before `<body>` is available. Did you forget to add `defer` in Alpine\'s `<script>` tag?')
 
 
     dispatch(document, 'alpine:init')
     dispatch(document, 'alpine:init')

+ 13 - 6
packages/alpinejs/src/magics.js

@@ -11,17 +11,24 @@ export function magic(name, callback) {
 
 
 export function injectMagics(obj, el) {
 export function injectMagics(obj, el) {
     Object.entries(magics).forEach(([name, callback]) => {
     Object.entries(magics).forEach(([name, callback]) => {
-        Object.defineProperty(obj, `$${name}`, {
-            get() {
+        let memoizedUtilities = null;
+        function getUtilities() {
+            if (memoizedUtilities) {
+                return memoizedUtilities;
+            } else {
                 let [utilities, cleanup] = getElementBoundUtilities(el)
                 let [utilities, cleanup] = getElementBoundUtilities(el)
                 
                 
-                utilities = {interceptor, ...utilities}
+                memoizedUtilities = {interceptor, ...utilities}
                 
                 
                 onElRemoved(el, cleanup)
                 onElRemoved(el, cleanup)
-
-                return callback(el, utilities)
+                return memoizedUtilities;
+            }
+        }
+        
+        Object.defineProperty(obj, `$${name}`, {
+            get() {
+                return callback(el, getUtilities());
             },
             },
-
             enumerable: false,
             enumerable: false,
         })
         })
     })
     })

+ 4 - 2
packages/alpinejs/src/plugin.js

@@ -1,5 +1,7 @@
-import Alpine from './alpine'
+import Alpine from "./alpine";
 
 
 export function plugin(callback) {
 export function plugin(callback) {
-    callback(Alpine)
+    let callbacks = Array.isArray(callback) ? callback : [callback]
+
+    callbacks.forEach(i => i(Alpine))
 }
 }

+ 4 - 1
packages/alpinejs/src/scheduler.js

@@ -2,6 +2,7 @@
 let flushPending = false
 let flushPending = false
 let flushing = false
 let flushing = false
 let queue = []
 let queue = []
+let lastFlushedIndex = -1
 
 
 export function scheduler (callback) { queueJob(callback) }
 export function scheduler (callback) { queueJob(callback) }
 
 
@@ -13,7 +14,7 @@ function queueJob(job) {
 export function dequeueJob(job) {
 export function dequeueJob(job) {
     let index = queue.indexOf(job)
     let index = queue.indexOf(job)
 
 
-    if (index !== -1) queue.splice(index, 1)
+    if (index !== -1 && index > lastFlushedIndex) queue.splice(index, 1)
 }
 }
 
 
 function queueFlush() {
 function queueFlush() {
@@ -30,9 +31,11 @@ export function flushJobs() {
 
 
     for (let i = 0; i < queue.length; i++) {
     for (let i = 0; i < queue.length; i++) {
         queue[i]()
         queue[i]()
+        lastFlushedIndex = i
     }
     }
 
 
     queue.length = 0
     queue.length = 0
+    lastFlushedIndex = -1
 
 
     flushing = false
     flushing = false
 }
 }

+ 2 - 10
packages/alpinejs/src/scope.js

@@ -15,14 +15,6 @@ export function hasScope(node) {
     return !! node._x_dataStack
     return !! node._x_dataStack
 }
 }
 
 
-export function refreshScope(element, scope) {
-    let existingScope = element._x_dataStack[0]
-
-    Object.entries(scope).forEach(([key, value]) => {
-        existingScope[key] = value
-    })
-}
-
 export function closestDataStack(node) {
 export function closestDataStack(node) {
     if (node._x_dataStack) return node._x_dataStack
     if (node._x_dataStack) return node._x_dataStack
 
 
@@ -60,7 +52,7 @@ export function mergeProxies(objects) {
                     if ((descriptor.get && descriptor.get._x_alreadyBound) || (descriptor.set && descriptor.set._x_alreadyBound)) {
                     if ((descriptor.get && descriptor.get._x_alreadyBound) || (descriptor.set && descriptor.set._x_alreadyBound)) {
                         return true
                         return true
                     }
                     }
-                    
+
                     // Properly bind getters and setters to this wrapper Proxy.
                     // Properly bind getters and setters to this wrapper Proxy.
                     if ((descriptor.get || descriptor.set) && descriptor.enumerable) {
                     if ((descriptor.get || descriptor.set) && descriptor.enumerable) {
                         // Only bind user-defined getters, not our magic properties.
                         // Only bind user-defined getters, not our magic properties.
@@ -81,7 +73,7 @@ export function mergeProxies(objects) {
                         })
                         })
                     }
                     }
 
 
-                    return true 
+                    return true
                 }
                 }
 
 
                 return false
                 return false

+ 19 - 0
packages/alpinejs/src/utils/bind.js

@@ -22,6 +22,14 @@ export default function bind(el, name, value, modifiers = []) {
         case 'class':
         case 'class':
             bindClasses(el, value)
             bindClasses(el, value)
             break;
             break;
+        
+        // 'selected' and 'checked' are special attributes that aren't necessarily
+        // synced with their corresponding properties when updated, so both the 
+        // attribute and property need to be updated when bound.
+        case 'selected':
+        case 'checked':
+            bindAttributeAndProperty(el, name, value)
+            break;
 
 
         default:
         default:
             bindAttribute(el, name, value)
             bindAttribute(el, name, value)
@@ -78,6 +86,11 @@ function bindStyles(el, value) {
     el._x_undoAddedStyles = setStyles(el, value)
     el._x_undoAddedStyles = setStyles(el, value)
 }
 }
 
 
+function bindAttributeAndProperty(el, name, value) {
+    bindAttribute(el, name, value)
+    setPropertyIfChanged(el, name, value)
+}
+
 function bindAttribute(el, name, value) {
 function bindAttribute(el, name, value) {
     if ([null, undefined, false].includes(value) && attributeShouldntBePreservedIfFalsy(name)) {
     if ([null, undefined, false].includes(value) && attributeShouldntBePreservedIfFalsy(name)) {
         el.removeAttribute(name)
         el.removeAttribute(name)
@@ -94,6 +107,12 @@ function setIfChanged(el, attrName, value) {
     }
     }
 }
 }
 
 
+function setPropertyIfChanged(el, propName, value) {
+    if (el[propName] !== value) {
+        el[propName] = value
+    }
+}
+
 function updateSelect(el, value) {
 function updateSelect(el, value) {
     const arrayWrappedValue = [].concat(value).map(value => { return value + '' })
     const arrayWrappedValue = [].concat(value).map(value => { return value + '' })
 
 

+ 18 - 15
packages/alpinejs/src/utils/on.js

@@ -18,6 +18,23 @@ export default function on (el, event, modifiers, callback) {
     if (modifiers.includes('capture')) options.capture = true
     if (modifiers.includes('capture')) options.capture = true
     if (modifiers.includes('window')) listenerTarget = window
     if (modifiers.includes('window')) listenerTarget = window
     if (modifiers.includes('document')) listenerTarget = document
     if (modifiers.includes('document')) listenerTarget = document
+
+    // By wrapping the handler with debounce & throttle first, we ensure that the wrapping logic itself is not
+    // throttled/debounced, only the user's callback is. This way, if the user expects
+    // `e.preventDefault()` to happen, it'll still happen even if their callback gets throttled.
+    if (modifiers.includes('debounce')) {
+        let nextModifier = modifiers[modifiers.indexOf('debounce')+1] || 'invalid-wait'
+        let wait = isNumeric(nextModifier.split('ms')[0]) ? Number(nextModifier.split('ms')[0]) : 250
+
+        handler = debounce(handler, wait)
+    }
+    if (modifiers.includes('throttle')) {
+        let nextModifier = modifiers[modifiers.indexOf('throttle')+1] || 'invalid-wait'
+        let wait = isNumeric(nextModifier.split('ms')[0]) ? Number(nextModifier.split('ms')[0]) : 250
+
+        handler = throttle(handler, wait)
+    }
+
     if (modifiers.includes('prevent')) handler = wrapHandler(handler, (next, e) => { e.preventDefault(); next(e) })
     if (modifiers.includes('prevent')) handler = wrapHandler(handler, (next, e) => { e.preventDefault(); next(e) })
     if (modifiers.includes('stop')) handler = wrapHandler(handler, (next, e) => { e.stopPropagation(); next(e) })
     if (modifiers.includes('stop')) handler = wrapHandler(handler, (next, e) => { e.stopPropagation(); next(e) })
     if (modifiers.includes('self')) handler = wrapHandler(handler, (next, e) => { e.target === el && next(e) })
     if (modifiers.includes('self')) handler = wrapHandler(handler, (next, e) => { e.target === el && next(e) })
@@ -59,20 +76,6 @@ export default function on (el, event, modifiers, callback) {
         next(e)
         next(e)
     })
     })
 
 
-    if (modifiers.includes('debounce')) {
-        let nextModifier = modifiers[modifiers.indexOf('debounce')+1] || 'invalid-wait'
-        let wait = isNumeric(nextModifier.split('ms')[0]) ? Number(nextModifier.split('ms')[0]) : 250
-
-        handler = debounce(handler, wait)
-    }
-
-    if (modifiers.includes('throttle')) {
-        let nextModifier = modifiers[modifiers.indexOf('throttle')+1] || 'invalid-wait'
-        let wait = isNumeric(nextModifier.split('ms')[0]) ? Number(nextModifier.split('ms')[0]) : 250
-
-        handler = throttle(handler, wait)
-    }
-
     listenerTarget.addEventListener(event, handler, options)
     listenerTarget.addEventListener(event, handler, options)
 
 
     return () => {
     return () => {
@@ -104,7 +107,7 @@ function isKeyEvent(event) {
 
 
 function isListeningForASpecificKeyThatHasntBeenPressed(e, modifiers) {
 function isListeningForASpecificKeyThatHasntBeenPressed(e, modifiers) {
     let keyModifiers = modifiers.filter(i => {
     let keyModifiers = modifiers.filter(i => {
-        return ! ['window', 'document', 'prevent', 'stop', 'once'].includes(i)
+        return ! ['window', 'document', 'prevent', 'stop', 'once', 'capture'].includes(i)
     })
     })
 
 
     if (keyModifiers.includes('debounce')) {
     if (keyModifiers.includes('debounce')) {

+ 1 - 1
packages/collapse/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/collapse",
     "name": "@alpinejs/collapse",
-    "version": "3.10.5",
+    "version": "3.12.2",
     "description": "Collapse and expand elements with robust animations",
     "description": "Collapse and expand elements with robust animations",
     "homepage": "https://alpinejs.dev/plugins/collapse",
     "homepage": "https://alpinejs.dev/plugins/collapse",
     "repository": {
     "repository": {

+ 1 - 1
packages/docs/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/docs",
     "name": "@alpinejs/docs",
-    "version": "3.10.5-revision.1",
+    "version": "3.12.2-revision.1",
     "description": "The documentation for Alpine",
     "description": "The documentation for Alpine",
     "author": "Caleb Porzio",
     "author": "Caleb Porzio",
     "license": "MIT"
     "license": "MIT"

+ 2 - 2
packages/docs/src/en/advanced/csp.md

@@ -9,12 +9,12 @@ In order for Alpine to be able to execute plain strings from HTML attributes as
 
 
 > Under the hood, Alpine doesn't actually use eval() itself because it's slow and problematic. Instead it uses Function declarations, which are much better, but still violate "unsafe-eval".
 > Under the hood, Alpine doesn't actually use eval() itself because it's slow and problematic. Instead it uses Function declarations, which are much better, but still violate "unsafe-eval".
 
 
-In order to accommodate environments where this CSP is necessary, Alpine offers an alternate build that doesn't violate "unsafe-eval", but has a more restrictive syntax.
+In order to accommodate environments where this CSP is necessary, Alpine will offer an alternate build that doesn't violate "unsafe-eval", but has a more restrictive syntax.
 
 
 <a name="installation"></a>
 <a name="installation"></a>
 ## Installation
 ## Installation
 
 
-Like all Alpine extensions, you can include this either via `<script>` tag or module import:
+The CSP build hasn’t been officially released yet. In the meantime, you may [build it from source](https://github.com/alpinejs/alpine/tree/main/packages/csp). Once released, like all Alpine extensions, you will be able to include this either via `<script>` tag or module import:
 
 
 <a name="script-tag"></a>
 <a name="script-tag"></a>
 ### Script tag
 ### Script tag

+ 1 - 1
packages/docs/src/en/advanced/extending.md

@@ -228,7 +228,7 @@ Now if the directive is removed from this element or the element is removed itse
 ### Custom order
 ### Custom order
 
 
 By default, any new directive will run after the majority of the standard ones (with the exception of `x-teleport`). This is usually acceptable but some times you might need to run your custom directive before another specific one.
 By default, any new directive will run after the majority of the standard ones (with the exception of `x-teleport`). This is usually acceptable but some times you might need to run your custom directive before another specific one.
-This can be achieved by chaining the `.before() function to `Alpine.directive()` and specifing which directive needs to run after your custom one.
+This can be achieved by chaining the `.before() function to `Alpine.directive()` and specifying which directive needs to run after your custom one.
  
  
 ```js
 ```js
 Alpine.directive('foo', (el, { value, modifiers, expression }) => {
 Alpine.directive('foo', (el, { value, modifiers, expression }) => {

+ 5 - 5
packages/docs/src/en/directives/bind.md

@@ -11,7 +11,7 @@ For example, here's a component where we will use `x-bind` to set the placeholde
 
 
 ```alpine
 ```alpine
 <div x-data="{ placeholder: 'Type here...' }">
 <div x-data="{ placeholder: 'Type here...' }">
-  <input type="text" x-bind:placeholder="placeholder">
+    <input type="text" x-bind:placeholder="placeholder">
 </div>
 </div>
 ```
 ```
 
 
@@ -33,11 +33,11 @@ Here's a simple example of a simple dropdown toggle, but instead of using `x-sho
 
 
 ```alpine
 ```alpine
 <div x-data="{ open: false }">
 <div x-data="{ open: false }">
-  <button x-on:click="open = ! open">Toggle Dropdown</button>
+    <button x-on:click="open = ! open">Toggle Dropdown</button>
 
 
-  <div :class="open ? '' : 'hidden'">
-    Dropdown Contents...
-  </div>
+    <div :class="open ? '' : 'hidden'">
+        Dropdown Contents...
+    </div>
 </div>
 </div>
 ```
 ```
 
 

+ 3 - 3
packages/docs/src/en/directives/data.md

@@ -86,9 +86,9 @@ Let's refactor our component to use a getter called `isOpen` instead of accessin
 
 
 ```alpine
 ```alpine
 <div x-data="{
 <div x-data="{
-  open: false,
-  get isOpen() { return this.open },
-  toggle() { this.open = ! this.open },
+    open: false,
+    get isOpen() { return this.open },
+    toggle() { this.open = ! this.open },
 }">
 }">
     <button @click="toggle()">Toggle Content</button>
     <button @click="toggle()">Toggle Content</button>
 
 

+ 11 - 0
packages/docs/src/en/directives/model.md

@@ -337,6 +337,17 @@ The default throttle interval is 250 milliseconds, you can easily customize this
 <input type="text" x-model.throttle.500ms="search">
 <input type="text" x-model.throttle.500ms="search">
 ```
 ```
 
 
+<a name="fill"></a>
+### `.fill`
+
+By default, if an input has a value attribute, it is ignored by Alpine and instead, the value of the input is set to the value of the property bound using `x-model`.
+
+But if a bound property is empty, then you can use an input's value attribute to populate the property by adding the `.fill` modifier.
+
+<div x-data="{ message: null }">
+  <input x-model.fill="message" value="This is the default message.">
+</div>
+
 <a name="programmatic access"></a>
 <a name="programmatic access"></a>
 ## Programmatic access
 ## Programmatic access
 
 

+ 15 - 2
packages/docs/src/en/directives/on.md

@@ -100,7 +100,7 @@ Here's an example of a component that dispatches a custom DOM event and listens
 
 
 ```alpine
 ```alpine
 <div x-data @foo="alert('Button Was Clicked!')">
 <div x-data @foo="alert('Button Was Clicked!')">
-	<button @click="$event.target.dispatchEvent(new CustomEvent('foo', { bubbles: true }))">...</button>
+    <button @click="$event.target.dispatchEvent(new CustomEvent('foo', { bubbles: true }))">...</button>
 </div>
 </div>
 ```
 ```
 
 
@@ -112,7 +112,7 @@ Here's the same component re-written with the `$dispatch` magic property.
 
 
 ```alpine
 ```alpine
 <div x-data @foo="alert('Button Was Clicked!')">
 <div x-data @foo="alert('Button Was Clicked!')">
-  <button @click="$dispatch('foo')">...</button>
+    <button @click="$dispatch('foo')">...</button>
 </div>
 </div>
 ```
 ```
 
 
@@ -298,3 +298,16 @@ If you are listening for touch events, it's important to add `.passive` to your
 ```
 ```
 
 
 [→ Read more about passive listeners](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners)
 [→ Read more about passive listeners](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners)
+
+### .capture
+
+Add this modifier if you want to execute this listener in the event's capturing phase, e.g. before the event bubbles from the target element up the DOM.
+
+```
+<div @click.capture="console.log('I will log first')">
+    <button @click="console.log('I will log second')"></button>
+</div>
+```
+
+[→ Read more about the capturing and bubbling phase of events](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#usecapture)
+

+ 6 - 6
packages/docs/src/en/essentials/installation.md

@@ -19,12 +19,12 @@ This is by far the simplest way to get started with Alpine. Include the followin
 
 
 ```alpine
 ```alpine
 <html>
 <html>
-  <head>
-    ...
+    <head>
+        ...
 
 
-    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
-  </head>
-  ...
+        <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
+    </head>
+    ...
 </html>
 </html>
 ```
 ```
 
 
@@ -33,7 +33,7 @@ This is by far the simplest way to get started with Alpine. Include the followin
 Notice the `@3.x.x` in the provided CDN link. This will pull the latest version of Alpine version 3. For stability in production, it's recommended that you hardcode the latest version in the CDN link.
 Notice the `@3.x.x` in the provided CDN link. This will pull the latest version of Alpine version 3. For stability in production, it's recommended that you hardcode the latest version in the CDN link.
 
 
 ```alpine
 ```alpine
-<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.10.5/dist/cdn.min.js"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.2/dist/cdn.min.js"></script>
 ```
 ```
 
 
 That's it! Alpine is now available for use inside your page.
 That's it! Alpine is now available for use inside your page.

+ 1 - 1
packages/docs/src/en/essentials/lifecycle.md

@@ -87,7 +87,7 @@ document.addEventListener('alpine:init', () => {
 <a name="alpine-initialized"></a>
 <a name="alpine-initialized"></a>
 ### `alpine:initialized`
 ### `alpine:initialized`
 
 
-Alpine also offers a hook that you can use to execute code After it's done initializing called `alpine:initialized`:
+Alpine also offers a hook that you can use to execute code AFTER it's done initializing called `alpine:initialized`:
 
 
 ```js
 ```js
 document.addEventListener('alpine:initialized', () => {
 document.addEventListener('alpine:initialized', () => {

+ 14 - 1
packages/docs/src/en/plugins/mask.md

@@ -58,7 +58,7 @@ Alpine.plugin(mask)
 <button :aria-expanded="expanded" @click="expanded = ! expanded" class="text-cyan-600 font-medium underline">
 <button :aria-expanded="expanded" @click="expanded = ! expanded" class="text-cyan-600 font-medium underline">
     <span x-text="expanded ? 'Hide' : 'Show more'">Show</span> <span x-text="expanded ? '↑' : '↓'">↓</span>
     <span x-text="expanded ? 'Hide' : 'Show more'">Show</span> <span x-text="expanded ? '↑' : '↓'">↓</span>
 </button>
 </button>
- </div>
+</div>
 
 
 <a name="x-mask"></a>
 <a name="x-mask"></a>
 
 
@@ -171,3 +171,16 @@ You may also choose to override the thousands separator by supplying a third opt
     <input type="text" x-mask:dynamic="$money($input, '.', ' ')"  placeholder="3 000.00">
     <input type="text" x-mask:dynamic="$money($input, '.', ' ')"  placeholder="3 000.00">
 </div>
 </div>
 <!-- END_VERBATIM -->
 <!-- END_VERBATIM -->
+
+
+You can also override the default precision of 2 digits by using any desired number of digits as the fourth optional argument:
+
+```alpine
+<input x-mask:dynamic="$money($input, '.', ',', 4)">
+```
+
+<!-- START_VERBATIM -->
+<div class="demo" x-data>
+    <input type="text" x-mask:dynamic="$money($input, '.', ',', 4)"  placeholder="0.0001">
+</div>
+<!-- END_VERBATIM -->

+ 0 - 6
packages/docs/src/en/ui.md

@@ -1,6 +0,0 @@
----
-order: 5
-title: UI
-font-type: mono
-type: sub-directory
----

+ 1 - 1
packages/focus/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/focus",
     "name": "@alpinejs/focus",
-    "version": "3.10.5",
+    "version": "3.12.2",
     "description": "Manage focus within a page",
     "description": "Manage focus within a page",
     "homepage": "https://alpinejs.dev/plugins/focus",
     "homepage": "https://alpinejs.dev/plugins/focus",
     "repository": {
     "repository": {

+ 8 - 3
packages/focus/src/index.js

@@ -102,12 +102,17 @@ export default function (Alpine) {
 
 
             let oldValue = false
             let oldValue = false
 
 
-            let trap = createFocusTrap(el, {
+            let options = {
                 escapeDeactivates: false,
                 escapeDeactivates: false,
                 allowOutsideClick: true,
                 allowOutsideClick: true,
                 fallbackFocus: () => el,
                 fallbackFocus: () => el,
-                initialFocus: el.querySelector('[autofocus]')
-            })
+            }
+
+            let autofocusEl = el.querySelector('[autofocus]')
+
+            if (autofocusEl) options.initialFocus = autofocusEl
+
+            let trap = createFocusTrap(el, options)
 
 
             let undoInert = () => {}
             let undoInert = () => {}
             let undoDisableScrolling = () => {}
             let undoDisableScrolling = () => {}

+ 1 - 1
packages/intersect/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/intersect",
     "name": "@alpinejs/intersect",
-    "version": "3.10.5",
+    "version": "3.12.2",
     "description": "Trigger JavaScript when an element enters the viewport",
     "description": "Trigger JavaScript when an element enters the viewport",
     "homepage": "https://alpinejs.dev/plugins/intersect",
     "homepage": "https://alpinejs.dev/plugins/intersect",
     "repository": {
     "repository": {

+ 1 - 1
packages/mask/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/mask",
     "name": "@alpinejs/mask",
-    "version": "3.10.5",
+    "version": "3.12.2",
     "description": "An Alpine plugin for input masking",
     "description": "An Alpine plugin for input masking",
     "homepage": "https://alpinejs.dev/plugins/mask",
     "homepage": "https://alpinejs.dev/plugins/mask",
     "repository": {
     "repository": {

+ 48 - 38
packages/mask/src/index.js

@@ -4,39 +4,44 @@ export default function (Alpine) {
         let templateFn = () => expression
         let templateFn = () => expression
         let lastInputValue = ''
         let lastInputValue = ''
 
 
-        if (['function', 'dynamic'].includes(value)) {
-            // This is an x-mask:function directive.
-
-            let evaluator = evaluateLater(expression)
-
-            effect(() => {
-                templateFn = input => {
-                    let result
-
-                    // We need to prevent "auto-evaluation" of functions like
-                    // x-on expressions do so that we can use them as mask functions.
-                    Alpine.dontAutoEvaluateFunctions(() => {
-                        evaluator(value => {
-                            result = typeof value === 'function' ? value(input) : value
-                        }, { scope: {
-                            // These are "magics" we'll make available to the x-mask:function:
-                            '$input': input,
-                            '$money': formatMoney.bind({ el }),
-                        }})
-                    })
-
-                    return result
-                }
-
-                // Run on initialize which serves a dual purpose:
-                // - Initializing the mask on the input if it has an initial value.
-                // - Running the template function to set up reactivity, so that
-                //   when a dependency inside it changes, the input re-masks.
-                processInputValue(el)
-            })
-        } else {
-            processInputValue(el)
-        }
+        queueMicrotask(() => {
+            if (['function', 'dynamic'].includes(value)) {
+                // This is an x-mask:function directive.
+
+                let evaluator = evaluateLater(expression)
+
+                effect(() => {
+                    templateFn = input => {
+                        let result
+
+                        // We need to prevent "auto-evaluation" of functions like
+                        // x-on expressions do so that we can use them as mask functions.
+                        Alpine.dontAutoEvaluateFunctions(() => {
+                            evaluator(value => {
+                                result = typeof value === 'function' ? value(input) : value
+                            }, { scope: {
+                                // These are "magics" we'll make available to the x-mask:function:
+                                '$input': input,
+                                '$money': formatMoney.bind({ el }),
+                            }})
+                        })
+
+                        return result
+                    }
+
+                    // Run on initialize which serves a dual purpose:
+                    // - Initializing the mask on the input if it has an initial value.
+                    // - Running the template function to set up reactivity, so that
+                    //   when a dependency inside it changes, the input re-masks.
+                    processInputValue(el, false)
+                })
+            } else {
+                processInputValue(el, false)
+            }
+
+            // Override x-model's initial value...
+            if (el._x_model) el._x_model.set(el.value)
+        })
 
 
         el.addEventListener('input', () => processInputValue(el))
         el.addEventListener('input', () => processInputValue(el))
         // Don't "restoreCursorPosition" on "blur", because Safari
         // Don't "restoreCursorPosition" on "blur", because Safari
@@ -56,7 +61,9 @@ export default function (Alpine) {
                 return lastInputValue = el.value
                 return lastInputValue = el.value
             }
             }
 
 
-            let setInput = () => { lastInputValue = el.value = formatInput(input, template) }
+            let setInput = () => {
+                lastInputValue = el.value = formatInput(input, template)
+            }
 
 
             if (shouldRestoreCursor) {
             if (shouldRestoreCursor) {
                 // When an input element's value is set, it moves the cursor to the end
                 // When an input element's value is set, it moves the cursor to the end
@@ -79,7 +86,7 @@ export default function (Alpine) {
 
 
             return rebuiltInput
             return rebuiltInput
         }
         }
-    })
+    }).before('model')
 }
 }
 
 
 export function restoreCursorPosition(el, template, callback) {
 export function restoreCursorPosition(el, template, callback) {
@@ -163,7 +170,8 @@ export function buildUp(template, input) {
     return output
     return output
 }
 }
 
 
-export function formatMoney(input, delimiter = '.', thousands) {
+export function formatMoney(input, delimiter = '.', thousands, precision = 2) {
+    if (input === '-') return '-'
     if (/^\D+$/.test(input)) return '9'
     if (/^\D+$/.test(input)) return '9'
 
 
     thousands = thousands ?? (delimiter === "," ? "." : ",")
     thousands = thousands ?? (delimiter === "," ? "." : ",")
@@ -187,12 +195,14 @@ export function formatMoney(input, delimiter = '.', thousands) {
         return output
         return output
     }
     }
 
 
+    let minus = input.startsWith('-') ? '-' : ''
     let strippedInput = input.replaceAll(new RegExp(`[^0-9\\${delimiter}]`, 'g'), '')
     let strippedInput = input.replaceAll(new RegExp(`[^0-9\\${delimiter}]`, 'g'), '')
     let template = Array.from({ length: strippedInput.split(delimiter)[0].length }).fill('9').join('')
     let template = Array.from({ length: strippedInput.split(delimiter)[0].length }).fill('9').join('')
 
 
-    template = addThousands(template, thousands)
+    template = `${minus}${addThousands(template, thousands)}`
 
 
-    if (input.includes(delimiter)) template += `${delimiter}99`
+    if (precision > 0 && input.includes(delimiter)) 
+        template += `${delimiter}` + '9'.repeat(precision)
 
 
     queueMicrotask(() => {
     queueMicrotask(() => {
         if (this.el.value.endsWith(delimiter)) return
         if (this.el.value.endsWith(delimiter)) return

+ 1 - 1
packages/morph/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/morph",
     "name": "@alpinejs/morph",
-    "version": "3.10.5",
+    "version": "3.12.2",
     "description": "Diff and patch a block of HTML on a page with an HTML template",
     "description": "Diff and patch a block of HTML on a page with an HTML template",
     "homepage": "https://alpinejs.dev/plugins/morph",
     "homepage": "https://alpinejs.dev/plugins/morph",
     "repository": {
     "repository": {

+ 31 - 0
packages/morph/src/morph.js

@@ -5,6 +5,8 @@ let resolveStep = () => {}
 let logger = () => {}
 let logger = () => {}
 
 
 export function morph(from, toHtml, options) {
 export function morph(from, toHtml, options) {
+    monkeyPatchDomSetAttributeToAllowAtSymbols()
+
     // We're defining these globals and methods inside this function (instead of outside)
     // We're defining these globals and methods inside this function (instead of outside)
     // because it's an async function and if run twice, they would overwrite
     // because it's an async function and if run twice, they would overwrite
     // each other.
     // each other.
@@ -386,3 +388,32 @@ function initializeAlpineOnTo(from, to, childrenOnly) {
         window.Alpine.clone(from, to)
         window.Alpine.clone(from, to)
     }
     }
 }
 }
+
+let patched = false
+
+function monkeyPatchDomSetAttributeToAllowAtSymbols() {
+    if (patched) return
+
+    patched = true
+
+    // Because morphdom may add attributes to elements containing "@" symbols
+    // like in the case of an Alpine `@click` directive, we have to patch
+    // the standard Element.setAttribute method to allow this to work.
+    let original = Element.prototype.setAttribute
+
+    let hostDiv = document.createElement('div')
+
+    Element.prototype.setAttribute = function newSetAttribute(name, value) {
+        if (! name.includes('@')) {
+            return original.call(this, name, value)
+        }
+
+        hostDiv.innerHTML = `<span ${name}="${value}"></span>`
+
+        let attr = hostDiv.firstElementChild.getAttributeNode(name)
+
+        hostDiv.firstElementChild.removeAttributeNode(attr)
+
+        this.setAttributeNode(attr)
+    }
+}

+ 4 - 1
packages/navigate/package.json

@@ -5,5 +5,8 @@
     "author": "Caleb Porzio",
     "author": "Caleb Porzio",
     "license": "MIT",
     "license": "MIT",
     "main": "dist/module.cjs.js",
     "main": "dist/module.cjs.js",
-    "module": "dist/module.esm.js"
+    "module": "dist/module.esm.js",
+    "dependencies": {
+        "nprogress": "^0.2.0"
+    }
 }
 }

+ 1 - 1
packages/persist/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/persist",
     "name": "@alpinejs/persist",
-    "version": "3.10.5",
+    "version": "3.12.2",
     "description": "Persist Alpine data across page loads",
     "description": "Persist Alpine data across page loads",
     "homepage": "https://alpinejs.dev/plugins/persist",
     "homepage": "https://alpinejs.dev/plugins/persist",
     "repository": {
     "repository": {

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/ui",
     "name": "@alpinejs/ui",
-    "version": "3.10.5-beta.8",
+    "version": "3.12.1-beta.0",
     "description": "Headless UI components for Alpine",
     "description": "Headless UI components for Alpine",
     "homepage": "https://alpinejs.dev/components#headless",
     "homepage": "https://alpinejs.dev/components#headless",
     "repository": {
     "repository": {

+ 1 - 1
packages/ui/src/combobox.js

@@ -7,7 +7,7 @@ export default function (Alpine) {
         else if (directive.value === 'options')      handleOptions(el, Alpine)
         else if (directive.value === 'options')      handleOptions(el, Alpine)
         else if (directive.value === 'option')       handleOption(el, Alpine, directive, evaluate)
         else if (directive.value === 'option')       handleOption(el, Alpine, directive, evaluate)
         else                                         handleRoot(el, Alpine)
         else                                         handleRoot(el, Alpine)
-    })
+    }).before('bind')
 
 
     Alpine.magic('comboboxOption', el => {
     Alpine.magic('comboboxOption', el => {
         let $data = Alpine.$data(el)
         let $data = Alpine.$data(el)

+ 1 - 1
packages/ui/src/disclosure.js

@@ -4,7 +4,7 @@ export default function (Alpine) {
         if      (! directive.value)            handleRoot(el, Alpine)
         if      (! directive.value)            handleRoot(el, Alpine)
         else if (directive.value === 'panel')  handlePanel(el, Alpine)
         else if (directive.value === 'panel')  handlePanel(el, Alpine)
         else if (directive.value === 'button') handleButton(el, Alpine)
         else if (directive.value === 'button') handleButton(el, Alpine)
-    })
+    }).before('bind')
 
 
     Alpine.magic('disclosure', el => {
     Alpine.magic('disclosure', el => {
         let $data = Alpine.$data(el)
         let $data = Alpine.$data(el)

+ 1 - 1
packages/ui/src/listbox.js

@@ -7,7 +7,7 @@ export default function (Alpine) {
         else if (directive.value === 'button') handleButton(el, Alpine)
         else if (directive.value === 'button') handleButton(el, Alpine)
         else if (directive.value === 'options') handleOptions(el, Alpine)
         else if (directive.value === 'options') handleOptions(el, Alpine)
         else if (directive.value === 'option') handleOption(el, Alpine)
         else if (directive.value === 'option') handleOption(el, Alpine)
-    })
+    }).before('bind')
 
 
     Alpine.magic('listbox', (el) => {
     Alpine.magic('listbox', (el) => {
         let data = Alpine.$data(el)
         let data = Alpine.$data(el)

+ 1 - 1
packages/ui/src/menu.js

@@ -4,7 +4,7 @@ export default function (Alpine) {
         else if (directive.value === 'items') handleItems(el, Alpine)
         else if (directive.value === 'items') handleItems(el, Alpine)
         else if (directive.value === 'item') handleItem(el, Alpine)
         else if (directive.value === 'item') handleItem(el, Alpine)
         else if (directive.value === 'button') handleButton(el, Alpine)
         else if (directive.value === 'button') handleButton(el, Alpine)
-    });
+    }).before('bind')
 
 
     Alpine.magic('menuItem', el => {
     Alpine.magic('menuItem', el => {
         let $data = Alpine.$data(el)
         let $data = Alpine.$data(el)

+ 1 - 1
packages/ui/src/radio.js

@@ -5,7 +5,7 @@ export default function (Alpine) {
         else if (directive.value === 'option')      handleOption(el, Alpine)
         else if (directive.value === 'option')      handleOption(el, Alpine)
         else if (directive.value === 'label')       handleLabel(el, Alpine)
         else if (directive.value === 'label')       handleLabel(el, Alpine)
         else if (directive.value === 'description') handleDescription(el, Alpine)
         else if (directive.value === 'description') handleDescription(el, Alpine)
-    })
+    }).before('bind')
 
 
     Alpine.magic('radioOption', el => {
     Alpine.magic('radioOption', el => {
         let $data = Alpine.$data(el)
         let $data = Alpine.$data(el)

+ 1 - 1
packages/ui/src/switch.js

@@ -5,7 +5,7 @@ export default function (Alpine) {
         else if (directive.value === 'label')       handleLabel(el, Alpine)
         else if (directive.value === 'label')       handleLabel(el, Alpine)
         else if (directive.value === 'description') handleDescription(el, Alpine)
         else if (directive.value === 'description') handleDescription(el, Alpine)
         else                                        handleRoot(el, Alpine)
         else                                        handleRoot(el, Alpine)
-    })
+    }).before('bind')
 
 
     Alpine.magic('switch', el => {
     Alpine.magic('switch', el => {
         let $data = Alpine.$data(el)
         let $data = Alpine.$data(el)

+ 1 - 1
packages/ui/src/tabs.js

@@ -6,7 +6,7 @@ export default function (Alpine) {
         else if (directive.value === 'tab')        handleTab(el, Alpine)
         else if (directive.value === 'tab')        handleTab(el, Alpine)
         else if (directive.value === 'panels')     handlePanels(el, Alpine)
         else if (directive.value === 'panels')     handlePanels(el, Alpine)
         else if (directive.value === 'panel')      handlePanel(el, Alpine)
         else if (directive.value === 'panel')      handlePanel(el, Alpine)
-    })
+    }).before('bind')
 
 
     Alpine.magic('tab', el => {
     Alpine.magic('tab', el => {
         let $data = Alpine.$data(el)
         let $data = Alpine.$data(el)

+ 6 - 17
scripts/build.js

@@ -1,6 +1,6 @@
+let { writeToPackageDotJson, getFromPackageDotJson } = require('./utils');
 let fs = require('fs');
 let fs = require('fs');
-let DotJson = require('dot-json');
-let brotliSize = require('brotli-size');
+let zlib = require('zlib');
 
 
 ([
 ([
     // Packages:
     // Packages:
@@ -37,7 +37,7 @@ function bundleFile(package, file) {
                 outfile: `packages/${package}/dist/${file}`,
                 outfile: `packages/${package}/dist/${file}`,
                 bundle: true,
                 bundle: true,
                 platform: 'browser',
                 platform: 'browser',
-                define: { CDN: true },
+                define: { CDN: 'true' },
             })
             })
 
 
             // Build a minified version.
             // Build a minified version.
@@ -47,7 +47,7 @@ function bundleFile(package, file) {
                 bundle: true,
                 bundle: true,
                 minify: true,
                 minify: true,
                 platform: 'browser',
                 platform: 'browser',
-                define: { CDN: true },
+                define: { CDN: 'true' },
             }).then(() => {
             }).then(() => {
                 outputSize(package, `packages/${package}/dist/${file.replace('.js', '.min.js')}`)
                 outputSize(package, `packages/${package}/dist/${file.replace('.js', '.min.js')}`)
             })
             })
@@ -86,26 +86,15 @@ function build(options) {
     options.define['process.env.NODE_ENV'] = process.argv.includes('--watch') ? `'production'` : `'development'`
     options.define['process.env.NODE_ENV'] = process.argv.includes('--watch') ? `'production'` : `'development'`
 
 
     return require('esbuild').build({
     return require('esbuild').build({
+        logLevel: process.argv.includes('--watch') ? 'info' : 'warning',
         watch: process.argv.includes('--watch'),
         watch: process.argv.includes('--watch'),
         // external: ['alpinejs'],
         // external: ['alpinejs'],
         ...options,
         ...options,
     }).catch(() => process.exit(1))
     }).catch(() => process.exit(1))
 }
 }
 
 
-function writeToPackageDotJson(package, key, value) {
-    let dotJson = new DotJson(`./packages/${package}/package.json`)
-
-    dotJson.set(key, value).save()
-}
-
-function getFromPackageDotJson(package, key) {
-    let dotJson = new DotJson(`./packages/${package}/package.json`)
-
-    return dotJson.get(key)
-}
-
 function outputSize(package, file) {
 function outputSize(package, file) {
-    let size = bytesToSize(brotliSize.sync(fs.readFileSync(file)))
+    let size = bytesToSize(zlib.brotliCompressSync(fs.readFileSync(file)).length)
 
 
     console.log("\x1b[32m", `${package}: ${size}`)
     console.log("\x1b[32m", `${package}: ${size}`)
 }
 }

+ 31 - 0
tests/cypress/integration/clone.spec.js

@@ -97,3 +97,34 @@ test('wont register listeners on clone',
         get('#copy span').should(haveText('1'))
         get('#copy span').should(haveText('1'))
     }
     }
 )
 )
+
+test('wont register extra listeners on x-model on clone',
+    html`
+        <script>
+            document.addEventListener('alpine:initialized', () => {
+                window.original = document.getElementById('original')
+                window.copy = document.getElementById('copy')
+            })
+        </script>
+
+        <button x-data @click="Alpine.clone(original, copy)">click</button>
+
+        <div x-data="{ checks: [] }" id="original">
+            <input type="checkbox" x-model="checks" value="1">
+            <span x-text="checks"></span>
+        </div>
+
+        <div x-data="{ checks: [] }" id="copy">
+            <input type="checkbox" x-model="checks" value="1">
+            <span x-text="checks"></span>
+        </div>
+    `,
+    ({ get }) => {
+        get('#original span').should(haveText(''))
+        get('#copy span').should(haveText(''))
+        get('button').click()
+        get('#copy span').should(haveText(''))
+        get('#copy input').click()
+        get('#copy span').should(haveText('1'))
+    }
+)

+ 3 - 3
tests/cypress/integration/directives/x-bind-style.spec.js

@@ -40,7 +40,7 @@ test('style attribute object binding with CSS variable',
         </div>
         </div>
     `,
     `,
     ({ get }) => {
     ({ get }) => {
-        get('div').should(haveAttribute('style', '--MyCSS-Variable:0.25;'))
+        get('div').should(haveAttribute('style', '--MyCSS-Variable: 0.25;'))
     }
     }
 )
 )
 
 
@@ -62,7 +62,7 @@ test('CSS custom properties are set',
         </div>
         </div>
     `,
     `,
     ({ get }) => {
     ({ get }) => {
-        get('span').should(haveAttribute('style', 'color: var(--custom-prop); --custom-prop:#f00;'))
+        get('span').should(haveAttribute('style', 'color: var(--custom-prop); --custom-prop: #f00;'))
     }
     }
 )
 )
 
 
@@ -73,6 +73,6 @@ test('existing CSS custom properties are preserved',
         </div>
         </div>
     `,
     `,
     ({ get }) => {
     ({ get }) => {
-        get('span').should(haveAttribute('style', 'color: var(--custom-prop-b); --custom-prop-a: red; --custom-prop-b:var(--custom-prop-a);'))
+        get('span').should(haveAttribute('style', 'color: var(--custom-prop-b); --custom-prop-a: red; --custom-prop-b: var(--custom-prop-a);'))
     }
     }
 )
 )

+ 23 - 4
tests/cypress/integration/directives/x-bind.spec.js

@@ -1,4 +1,4 @@
-import { beHidden, beVisible, haveText, beChecked, haveAttribute, haveClasses, haveValue, notBeChecked, notHaveAttribute, notHaveClasses, test, html } from '../../utils'
+import { beHidden, beVisible, haveText, beChecked, haveAttribute, haveClasses, haveProperty, haveValue, notBeChecked, notHaveAttribute, notHaveClasses, test, html } from '../../utils';
 
 
 test('sets attribute bindings on initialize',
 test('sets attribute bindings on initialize',
     html`
     html`
@@ -391,8 +391,8 @@ test('x-bind object syntax event handlers defined as functions receive the event
         <script>
         <script>
             window.data = () => { return {
             window.data = () => { return {
                 button: {
                 button: {
-                    ['@click']() {
-                        this.$refs.span.innerText = this.$el.id
+                    ['@click'](event) {
+                        this.$refs.span.innerText = event.currentTarget.id
                     }
                     }
                 }
                 }
             }}
             }}
@@ -410,7 +410,7 @@ test('x-bind object syntax event handlers defined as functions receive the event
     }
     }
 )
 )
 
 
-test('x-bind object syntax event handlers defined as functions receive the event object as their first argument',
+test('x-bind object syntax event handlers defined as functions receive element bound magics',
     html`
     html`
         <script>
         <script>
             window.data = () => { return {
             window.data = () => { return {
@@ -452,3 +452,22 @@ test('Can retrieve Alpine bound data with global bound method',
         get('#6').should(haveText('bar'))
         get('#6').should(haveText('bar'))
     }
     }
 )
 )
+
+test('x-bind updates checked attribute and property after user interaction',
+    html`
+        <div x-data="{ checked: true }">
+            <button @click="checked = !checked">toggle</button>
+            <input type="checkbox" x-bind:checked="checked" @change="checked = $event.target.checked" />
+        </div>
+    `,
+    ({ get }) => {
+        get('input').should(haveAttribute('checked', 'checked'))
+        get('input').should(haveProperty('checked', true))
+        get('input').click()
+        get('input').should(notHaveAttribute('checked'))
+        get('input').should(haveProperty('checked', false))
+        get('button').click()
+        get('input').should(haveAttribute('checked', 'checked'))
+        get('input').should(haveProperty('checked', true))
+    }
+)

+ 46 - 0
tests/cypress/integration/directives/x-for.spec.js

@@ -559,3 +559,49 @@ test('renders children using directives injected by x-html correctly',
         get('p:nth-of-type(2) span').should(haveText('bar'))
         get('p:nth-of-type(2) span').should(haveText('bar'))
     }
     }
 )
 )
+
+test(
+    'handles x-data directly inside x-for',
+    html`
+        <div x-data="{ items: [{x:0, k:1},{x:1, k:2}] }">
+            <button x-on:click="items = [{x:3, k:1},{x:4, k:2}]">update</button>
+            <template x-for="item in items" :key="item.k">
+                <div :id="'item-' + item.k" x-data="{ inner: true }">
+                    <span x-text="item.x.toString()"></span>:
+                    <span x-text="item.k"></span>
+                </div>
+            </template>
+        </div>
+    `,
+    ({ get }) => {
+        get('#item-1 span:nth-of-type(1)').should(haveText('0'))
+        get('#item-2 span:nth-of-type(1)').should(haveText('1'))
+        get('button').click()
+        get('#item-1 span:nth-of-type(1)').should(haveText('3'))
+        get('#item-2 span:nth-of-type(1)').should(haveText('4'))
+})
+
+test('x-for throws descriptive error when key is undefined',
+    html`
+        <div x-data="{ items: [
+            {
+                id: 1,
+                name: 'foo',
+            },
+            {
+                id: 2,
+                name: 'bar',
+            },
+            {
+                id: 3,
+                name: 'baz',
+            },
+        ]}">
+            <template x-for="item in items" :key="item.doesntExist">
+                <span x-text="i"></span>
+            </template>
+        </div>
+    `,
+    ({ get }) => {},
+    true
+)

+ 37 - 0
tests/cypress/integration/directives/x-if.spec.js

@@ -73,3 +73,40 @@ test('x-if removed dom does not evaluate reactive expressions in dom tree',
         get('span').should(notExist())
         get('span').should(notExist())
     }
     }
 )
 )
+
+// Attempting to skip an already-flushed reactive effect would cause inconsistencies when updating other effects.
+// See https://github.com/alpinejs/alpine/issues/2803 for more details.
+test('x-if removed dom does not attempt skipping already-processed reactive effects in dom tree',
+    html`
+    <div x-data="{
+        isEditing: true,
+        foo: 'random text',
+        stopEditing() {
+          this.foo = '';
+          this.isEditing = false;
+        },
+    }">
+        <button @click="stopEditing">Stop editing</button>
+        <template x-if="isEditing">
+            <div id="div-editing">
+              <h2>Editing</h2>
+              <input id="foo" name="foo" type="text" x-model="foo" />
+            </div>
+        </template>
+
+        <template x-if="!isEditing">
+            <div id="div-not-editing"><h2>Not editing</h2></div>
+        </template>
+
+        <template x-if="!isEditing">
+            <div id="div-also-not-editing"><h2>Also not editing</h2></div>
+        </template>
+    </div>
+    `,
+    ({ get }) => {
+        get('button').click()
+        get('div#div-editing').should(notExist())
+        get('div#div-not-editing').should(exist())
+        get('div#div-also-not-editing').should(exist())
+    }
+)

+ 59 - 0
tests/cypress/integration/directives/x-model.spec.js

@@ -129,3 +129,62 @@ test('x-model updates value when the form is reset',
         get('span').should(haveText(''))
         get('span').should(haveText(''))
     }
     }
 )
 )
+
+test('x-model with fill modifier takes input value on null or empty string',
+    html`
+    <div x-data="{ a: 123, b: 0, c: '', d: null }">
+      <input x-model.fill="a" value="123456" />
+      <span id="a" x-text="a"></span>
+      <input x-model.fill="b" value="123456" />
+      <span id="b" x-text="b"></span>
+      <input x-model.fill="c" value="123456" />
+      <span id="c" x-text="c"></span>
+      <input x-model.fill="d" value="123456" />
+      <span id="d" x-text="d"></span>
+    </div>
+    `,
+    ({ get }) => {
+        get('#a').should(haveText('123'))
+        get('#b').should(haveText('0'))
+        get('#c').should(haveText('123456'))
+        get('#d').should(haveText('123456'))
+    }
+)
+
+test('x-model with fill modifier works with select/radio elements',
+    html`
+        <div x-data="{ a: null, b: null, c: null, d: null }">
+            <select x-model.fill="a">
+                <option value="123">123</option>
+                <option value="456" selected>456</option>
+            </select>
+            <select x-model.fill="b" multiple>
+                <option value="123" selected>123</option>
+                <option value="456" selected>456</option>
+            </select>
+        </div>
+    `,
+    ({ get }) => {
+        get('[x-data]').should(haveData('a', '456'));
+        get('[x-data]').should(haveData('b', ['123', '456']));
+    }
+);
+
+test('x-model with fill modifier respects number modifier',
+    html`
+        <div x-data="{ a: null, b: null, c: null, d: null }">
+            <input type="text" x-model.fill.number="a" value="456" / >
+            <select x-model.fill.number="b" multiple>
+                <option value="123" selected>123</option>
+                <option value="456" selected>456</option>
+            </select>
+        </div>
+    `,
+    ({ get }) => {
+        get('[x-data]').should(haveData('a', 456));
+        get('[x-data]').should(haveData('b', [123,456]));
+    }
+);
+
+
+

+ 68 - 2
tests/cypress/integration/directives/x-on.spec.js

@@ -97,10 +97,30 @@ test('.stop modifier',
     }
     }
 )
 )
 
 
-test('.capture modifier',
+
+test('.stop modifier with a .throttle',
     html`
     html`
         <div x-data="{ foo: 'bar' }">
         <div x-data="{ foo: 'bar' }">
-            <button @click.capture="foo = 'baz'">
+            <button x-on:click="foo = 'baz'">
+                <h1>h1</h1>
+                <h2 @click.stop.throttle>h2</h2>
+            </button>
+        </div>
+    `,
+    ({ get }) => {
+        get('div').should(haveData('foo', 'bar'))
+        get('h2').click()
+        get('h2').click()
+        get('div').should(haveData('foo', 'bar'))
+        get('h1').click()
+        get('div').should(haveData('foo', 'baz'))
+    }
+)
+
+test('.capture modifier',
+    html`
+        <div x-data="{ foo: 'bar', count: 0 }">
+            <button @click.capture="count = count + 1; foo = 'baz'">
                 <h1>h1</h1>
                 <h1>h1</h1>
                 <h2 @click="foo = 'bob'">h2</h2>
                 <h2 @click="foo = 'bob'">h2</h2>
             </button>
             </button>
@@ -110,6 +130,39 @@ test('.capture modifier',
         get('div').should(haveData('foo', 'bar'))
         get('div').should(haveData('foo', 'bar'))
         get('h2').click()
         get('h2').click()
         get('div').should(haveData('foo', 'bob'))
         get('div').should(haveData('foo', 'bob'))
+        get('div').should(haveData('count', 1))
+    }
+)
+
+test('.capture modifier with @keyup',
+    html`
+        <div x-data="{ foo: 'bar', count: 0 }">
+            <span @keyup.capture="count = count + 1; foo = 'span'">
+                <input type="text" @keyup="foo = 'input'">
+            </span>
+        </div>
+    `,
+    ({ get }) => {
+        get('div').should(haveData('foo', 'bar'))
+        get('input').type('f')
+        get('div').should(haveData('foo', 'input'))
+        get('div').should(haveData('count', 1))
+    }
+)
+
+test('.capture modifier with @keyup and specified key',
+    html`
+        <div x-data="{ foo: 'bar', count: 0 }">
+            <span @keyup.enter.capture="count = count + 1; foo = 'span'">
+                <input type="text" @keyup.enter="foo = 'input'">
+            </span>
+        </div>
+    `,
+    ({ get }) => {
+        get('div').should(haveData('foo', 'bar'))
+        get('input').type('{enter}')
+        get('div').should(haveData('foo', 'input'))
+        get('div').should(haveData('count', 1))
     }
     }
 )
 )
 
 
@@ -145,6 +198,19 @@ test('.prevent modifier',
     }
     }
 )
 )
 
 
+test('.prevent modifier with a .debounce',
+    html`
+        <div x-data="{}">
+            <input type="checkbox" x-on:click.prevent.debounce>
+        </div>
+    `,
+    ({ get }) => {
+        get('input').check()
+        get('input').check()
+        get('input').should(notBeChecked())
+    }
+)
+
 test('.window modifier',
 test('.window modifier',
     html`
     html`
         <div x-data="{ foo: 'bar' }">
         <div x-data="{ foo: 'bar' }">

+ 60 - 0
tests/cypress/integration/directives/x-transition.spec.js

@@ -80,3 +80,63 @@ test('transition:enter in nested x-show visually runs',
         get('h1').should(haveComputedStyle('opacity', '1')) // Eventually opacity will be 1
         get('h1').should(haveComputedStyle('opacity', '1')) // Eventually opacity will be 1
     }
     }
 )
 )
+
+test('transition duration and delay with and without ms syntax',
+    html`
+        <div x-data="{ showMs: false, showBlank: false }">
+
+            <span class="ms"
+                  x-show="showMs"
+                  x-transition.delay.80ms.duration.120ms>ms syntax</span>
+            <span class="blank"
+                  x-show="showBlank"
+                  x-transition.delay.80.duration.120>blank syntax</span>
+
+            <button class="ms"    x-on:click="showMs = true"></button>
+            <button class="blank" x-on:click="showBlank = true"></button>
+        </div>
+    `,
+    ({ get }) => {
+        get('span.ms').should(notBeVisible())
+        get('button.ms').click()
+        get('span.ms').should(notBeVisible()) // Not visible due to delay
+        get('span.ms').should(beVisible())
+        get('span.ms').should(notHaveComputedStyle('opacity', '1')) // We expect a value between 0 and 1
+        get('span.ms').should(haveComputedStyle('opacity', '1')) // Eventually opacity will be 1
+
+        get('span.blank').should(notBeVisible())
+        get('button.blank').click()
+        get('span.blank').should(notBeVisible()) // Not visible due to delay
+        get('span.blank').should(beVisible())
+        get('span.blank').should(notHaveComputedStyle('opacity', '1')) // We expect a value between 0 and 1
+        get('span.blank').should(haveComputedStyle('opacity', '1')) // Eventually opacity will be 1
+    }
+)
+
+test(
+    'bound x-transition can handle empty string and true values',
+    html`
+        <script>
+            window.transitions = () => {
+                return {
+                    withEmptyString: {
+                        ["x-transition.opacity"]: "",
+                    },
+                    withBoolean: {
+                        ["x-transition.opacity"]: true,
+                    },
+                };
+            };
+        </script>
+        <div x-data="transitions()">
+            <button x-bind="withEmptyString"></button>
+            <span x-bind="withBoolean">thing</span>
+        </div>
+    `,
+    ({ get }) => 
+        {
+            get('span').should(beVisible())
+            get('span').should(beVisible())
+        }
+    
+);

+ 2 - 2
tests/cypress/integration/entangle.spec.js

@@ -1,6 +1,6 @@
 import { haveValue, html, test } from '../utils'
 import { haveValue, html, test } from '../utils'
 
 
-test('can entangle to getter/setter pairs',
+test.skip('can entangle to getter/setter pairs',
     [html`
     [html`
     <div x-data="{ outer: 'foo' }">
     <div x-data="{ outer: 'foo' }">
         <input x-model="outer" outer>
         <input x-model="outer" outer>
@@ -33,7 +33,7 @@ test('can entangle to getter/setter pairs',
     }
     }
 )
 )
 
 
-test('can release entanglement',
+test.skip('can release entanglement',
     [html`
     [html`
         <div x-data="{ outer: 'foo' }">
         <div x-data="{ outer: 'foo' }">
             <input x-model="outer" outer>
             <input x-model="outer" outer>

+ 45 - 0
tests/cypress/integration/plugins/mask.spec.js

@@ -60,6 +60,19 @@ test('x-mask with x-model',
     },
     },
 )
 )
 
 
+test('x-mask with x-model with initial value',
+    [html`
+        <div x-data="{ value: '1234567890' }">
+            <input x-mask="(999) 999-9999" x-model="value" id="1">
+            <input id="2" x-model="value">
+        </div>
+    `],
+    ({ get }) => {
+        get('#1').should(haveValue('(123) 456-7890'))
+        get('#2').should(haveValue('(123) 456-7890'))
+    },
+)
+
 test('x-mask with a falsy input',
 test('x-mask with a falsy input',
     [html`<input x-data x-mask="">`],
     [html`<input x-data x-mask="">`],
     ({ get }) => {
     ({ get }) => {
@@ -178,3 +191,35 @@ test('$money mask should remove letters or non numeric characters',
         get('input').type('40').should(haveValue('40'))
         get('input').type('40').should(haveValue('40'))
     }
     }
 )
 )
+
+test('$money mask negative values',
+    [html`
+        <input id="1" x-data x-mask:dynamic="$money($input)" value="-1234.50" />
+        <input id="2" x-data x-mask:dynamic="$money($input)" />
+    `],
+    ({ get }) => {
+        get('#1').should(haveValue('-1,234.50'))
+        get('#2').type('-12.509').should(haveValue('-12.50'))
+        get('#2').type('{leftArrow}{leftArrow}{leftArrow}-').should(haveValue('-12.50'))
+        get('#2').type('{leftArrow}{leftArrow}{backspace}').should(haveValue('12.50'))
+        get('#2').type('{rightArrow}-').should(haveValue('12.50'))
+        get('#2').type('{rightArrow}-').should(haveValue('12.50'))
+        get('#2').type('{rightArrow}{rightArrow}{rightArrow}-').should(haveValue('12.50'))
+        get('#2').type('{leftArrow}{leftArrow}{leftArrow}{leftArrow}{leftArrow}-').should(haveValue('-12.50'))
+    }
+)
+
+test('$money with custom decimal precision',
+    [html`
+        <input id="0" x-data x-mask:dynamic="$money($input, '.', ',', 0)" />
+        <input id="1" x-data x-mask:dynamic="$money($input, '.', ',', 1)" />
+        <input id="2" x-data x-mask:dynamic="$money($input, '.', ',', 2)" />
+        <input id="3" x-data x-mask:dynamic="$money($input, '.', ',', 3)" />
+    `],
+    ({ get }) => {
+        get('#0').type('1234.5678').should(haveValue('12,345,678'))
+        get('#1').type('1234.5678').should(haveValue('1,234.5'))
+        get('#2').type('1234.5678').should(haveValue('1,234.56'))
+        get('#3').type('1234.5678').should(haveValue('1,234.567'))
+    }
+)

+ 20 - 0
tests/cypress/integration/plugins/morph.spec.js

@@ -421,3 +421,23 @@ test('can morph with flat-nested conditional markers',
         get('div:nth-of-type(3) input').should(haveValue('bar'))
         get('div:nth-of-type(3) input').should(haveValue('bar'))
     },
     },
 )
 )
+
+// '@event' handlers cannot be assigned directly on the element without Alpine's internl monkey patching...
+test('can morph @event handlers', [
+    html`
+        <div x-data="{ foo: 'bar' }">
+            <button x-text="foo"></button>
+        </div>
+    `],
+    ({ get, click }, reload, window, document) => {
+        let toHtml = html`
+            <button @click="foo = 'buzz'" x-text="foo"></button>
+        `;
+
+        get('button').should(haveText('bar'));
+
+        get('button').then(([el]) => window.Alpine.morph(el, toHtml));
+        get('button').click();
+        get('button').should(haveText('buzz'));
+    }
+);

+ 1 - 1
tests/cypress/integration/plugins/navigate.spec.js

@@ -1,6 +1,6 @@
 import { beEqualTo, beVisible, haveAttribute, haveFocus, haveText, html, notBeVisible, test } from '../../utils'
 import { beEqualTo, beVisible, haveAttribute, haveFocus, haveText, html, notBeVisible, test } from '../../utils'
 
 
-// Test persistant peice of layout
+// Test persistent piece of layout
 // Handle non-origin links and such
 // Handle non-origin links and such
 // Handle 404
 // Handle 404
 // Middle/command click link in new tab works?
 // Middle/command click link in new tab works?

+ 1 - 1
tests/cypress/integration/plugins/ui/popover.spec.js

@@ -156,7 +156,7 @@ test('focusing away doesnt close panel if focusing inside a group',
     },
     },
 )
 )
 
 
-test('focusing away still closes panel inside a group if the focus attribute is present',
+test.retry(5)('focusing away still closes panel inside a group if the focus attribute is present',
     [html`
     [html`
         <div x-data>
         <div x-data>
             <div x-popover:group>
             <div x-popover:group>

+ 3 - 1
tests/cypress/utils.js

@@ -87,7 +87,7 @@ function injectHtmlAndBootAlpine(cy, templateAndPotentiallyScripts, callback, pa
     })
     })
 }
 }
 
 
-export let haveData = (key, value) => ([ el ]) => expect(root(el)._x_dataStack[0][key]).to.equal(value)
+export let haveData = (key, value) => ([el]) => expect(root(el)._x_dataStack[0][key]).to.deep.equal(value);
 
 
 export let haveFocus = () => el => expect(el).to.have.focus
 export let haveFocus = () => el => expect(el).to.have.focus
 
 
@@ -97,6 +97,8 @@ export let haveAttribute = (name, value) => el => expect(el).to.have.attr(name,
 
 
 export let notHaveAttribute = (name, value) => el => expect(el).not.to.have.attr(name, value)
 export let notHaveAttribute = (name, value) => el => expect(el).not.to.have.attr(name, value)
 
 
+export let haveProperty = (name, value) => el => expect(el).to.have.prop(name, value)
+
 export let haveText = text => el => expect(el).to.have.text(text)
 export let haveText = text => el => expect(el).to.have.text(text)
 
 
 export let notHaveText = text => el => expect(el).not.to.have.text(text)
 export let notHaveText = text => el => expect(el).not.to.have.text(text)

部分文件因为文件数量过多而无法显示