Pārlūkot izejas kodu

Merge branch 'main' into feature/ui/combobox

Caleb Porzio 1 gadu atpakaļ
vecāks
revīzija
b9753e8da3
34 mainītis faili ar 726 papildinājumiem un 333 dzēšanām
  1. 1 1
      README.md
  2. 361 251
      package-lock.json
  3. 0 3
      package.json
  4. 1 1
      packages/alpinejs/package.json
  5. 14 4
      packages/alpinejs/src/directives/x-for.js
  6. 7 8
      packages/alpinejs/src/directives/x-model.js
  7. 6 6
      packages/alpinejs/src/directives/x-transition.js
  8. 6 0
      packages/alpinejs/src/lifecycle.js
  9. 13 6
      packages/alpinejs/src/magics.js
  10. 4 2
      packages/alpinejs/src/plugin.js
  11. 2 10
      packages/alpinejs/src/scope.js
  12. 19 0
      packages/alpinejs/src/utils/bind.js
  13. 17 14
      packages/alpinejs/src/utils/on.js
  14. 1 1
      packages/collapse/package.json
  15. 1 1
      packages/docs/package.json
  16. 1 1
      packages/docs/src/en/essentials/installation.md
  17. 0 6
      packages/docs/src/en/ui.md
  18. 1 1
      packages/focus/package.json
  19. 8 3
      packages/focus/src/index.js
  20. 1 1
      packages/intersect/package.json
  21. 1 1
      packages/mask/package.json
  22. 1 1
      packages/morph/package.json
  23. 31 0
      packages/morph/src/morph.js
  24. 4 1
      packages/navigate/package.json
  25. 1 1
      packages/persist/package.json
  26. 1 1
      packages/ui/package.json
  27. 3 3
      tests/cypress/integration/directives/x-bind-style.spec.js
  28. 23 4
      tests/cypress/integration/directives/x-bind.spec.js
  29. 46 0
      tests/cypress/integration/directives/x-for.spec.js
  30. 35 0
      tests/cypress/integration/directives/x-model.spec.js
  31. 33 0
      tests/cypress/integration/directives/x-on.spec.js
  32. 60 0
      tests/cypress/integration/directives/x-transition.spec.js
  33. 20 0
      tests/cypress/integration/plugins/morph.spec.js
  34. 3 1
      tests/cypress/utils.js

+ 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)
 
-<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:
 

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 361 - 251
package-lock.json


+ 0 - 3
package.json

@@ -20,8 +20,5 @@
         "jest": "jest test",
         "update-docs": "node ./scripts/update-docs.js",
         "release": "node ./scripts/release.js"
-    },
-    "dependencies": {
-        "nprogress": "^0.2.0"
     }
 }

+ 1 - 1
packages/alpinejs/package.json

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

+ 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 { directive } from '../directives'
 import { reactive } from '../reactivity'
@@ -163,6 +163,8 @@ function loop(el, iteratorNames, evaluateItems, evaluateKey) {
             let marker = document.createElement('div')
 
             mutateDom(() => {
+                if (! elForSpot) warn(`x-for ":key" is undefined or invalid`, templateEl)
+
                 elForSpot.after(marker)
                 elInSpot.after(elForSpot)
                 elForSpot._x_currentIfEl && elForSpot.after(elForSpot._x_currentIfEl)
@@ -171,7 +173,7 @@ function loop(el, iteratorNames, evaluateItems, evaluateKey) {
                 marker.remove()
             })
 
-            refreshScope(elForSpot, scopes[keys.indexOf(keyForSpot)])
+            elForSpot._x_refreshXForScope(scopes[keys.indexOf(keyForSpot)])
         }
 
         // We can now create and add new elements.
@@ -188,7 +190,15 @@ function loop(el, iteratorNames, evaluateItems, evaluateKey) {
 
             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(() => {
                 lastEl.after(clone)
@@ -207,7 +217,7 @@ function loop(el, iteratorNames, evaluateItems, evaluateKey) {
         // data it depends on in case the data has changed in an
         // "unobservable" way.
         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

+ 7 - 8
packages/alpinejs/src/directives/x-model.js

@@ -46,10 +46,6 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
             })
         }
     }
-
-    if (modifiers.includes('fill') && el.hasAttribute('value') && (getValue() === null || getValue() === '')) {
-        setValue(el.value)
-    }
     
     if (typeof expression === 'string' && el.type === 'radio') {
         // Radio buttons only work properly when they share a name attribute.
@@ -73,7 +69,10 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
     let removeListener = isCloning ? () => {} : on(el, event, modifiers, (e) => {
         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
     // in addition to the cleanup function, x-modelable may call it.
     // Also, make this a keyed object if we decide to reintroduce
@@ -134,9 +133,9 @@ function getInputValue(el, modifiers, event, currentValue) {
         // 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
         // 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 (Array.isArray(currentValue)) {
                 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 }) => {
     if (typeof expression === 'function') expression = evaluate(expression)
-
-    if (! expression) {
+    if (expression === false) return
+    if (!expression || typeof expression === 'boolean') {
         registerTransitionsFromHelper(el, modifiers, value)
     } else {
         registerTransitionsFromClassString(el, expression, value)
@@ -50,7 +50,7 @@ function registerTransitionsFromHelper(el, modifiers, stage) {
     let wantsScale = wantsAll || modifiers.includes('scale')
     let opacityValue = wantsOpacity ? 0 : 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 property = 'opacity, transform'
     let durationIn = modifierValue(modifiers, 'duration', 150) / 1000
@@ -60,7 +60,7 @@ function registerTransitionsFromHelper(el, modifiers, stage) {
     if (transitioningIn) {
         el._x_transition.enter.during = {
             transformOrigin: origin,
-            transitionDelay: delay,
+            transitionDelay: `${delay}s`,
             transitionProperty: property,
             transitionDuration: `${durationIn}s`,
             transitionTimingFunction: easing,
@@ -80,7 +80,7 @@ function registerTransitionsFromHelper(el, modifiers, stage) {
     if (transitioningOut) {
         el._x_transition.leave.during = {
             transformOrigin: origin,
-            transitionDelay: delay,
+            transitionDelay: `${delay}s`,
             transitionProperty: property,
             transitionDuration: `${durationOut}s`,
             transitionTimingFunction: easing,
@@ -318,7 +318,7 @@ export function modifierValue(modifiers, key, fallback) {
         if (isNaN(rawValue)) return fallback
     }
 
-    if (key === 'duration') {
+    if (key === 'duration' || key === 'delay') {
         // Support x-transition.duration.500ms && duration.500
         let match = rawValue.match(/([0-9]+)ms/)
         if (match) return match[1]

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

@@ -4,7 +4,13 @@ import { dispatch } from './utils/dispatch'
 import { walk } from "./utils/walk"
 import { warn } from './utils/warn'
 
+let started = false
+
 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?')
 
     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) {
     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)
                 
-                utilities = {interceptor, ...utilities}
+                memoizedUtilities = {interceptor, ...utilities}
                 
                 onElRemoved(el, cleanup)
-
-                return callback(el, utilities)
+                return memoizedUtilities;
+            }
+        }
+        
+        Object.defineProperty(obj, `$${name}`, {
+            get() {
+                return callback(el, getUtilities());
             },
-
             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) {
-    callback(Alpine)
+    let callbacks = Array.isArray(callback) ? callback : [callback]
+
+    callbacks.forEach(i => i(Alpine))
 }

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

@@ -15,14 +15,6 @@ export function hasScope(node) {
     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) {
     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)) {
                         return true
                     }
-                    
+
                     // Properly bind getters and setters to this wrapper Proxy.
                     if ((descriptor.get || descriptor.set) && descriptor.enumerable) {
                         // Only bind user-defined getters, not our magic properties.
@@ -81,7 +73,7 @@ export function mergeProxies(objects) {
                         })
                     }
 
-                    return true 
+                    return true
                 }
 
                 return false

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

@@ -23,6 +23,14 @@ export default function bind(el, name, value, modifiers = []) {
         case 'class':
             bindClasses(el, value)
             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:
             bindAttribute(el, name, value)
@@ -79,6 +87,11 @@ function bindStyles(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) {
     if ([null, undefined, false].includes(value) && attributeShouldntBePreservedIfFalsy(name)) {
         el.removeAttribute(name)
@@ -95,6 +108,12 @@ function setIfChanged(el, attrName, value) {
     }
 }
 
+function setPropertyIfChanged(el, propName, value) {
+    if (el[propName] !== value) {
+        el[propName] = value
+    }
+}
+
 function updateSelect(el, value) {
     const arrayWrappedValue = [].concat(value).map(value => { return value + '' })
 

+ 17 - 14
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('window')) listenerTarget = window
     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('stop')) handler = wrapHandler(handler, (next, e) => { e.stopPropagation(); 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)
     })
 
-    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)
 
     return () => {

+ 1 - 1
packages/collapse/package.json

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

+ 1 - 1
packages/docs/package.json

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

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

@@ -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.
 
 ```alpine
-<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.0/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.

+ 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",
-    "version": "3.12.0",
+    "version": "3.12.2",
     "description": "Manage focus within a page",
     "homepage": "https://alpinejs.dev/plugins/focus",
     "repository": {

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

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

+ 1 - 1
packages/intersect/package.json

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

+ 1 - 1
packages/mask/package.json

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

+ 1 - 1
packages/morph/package.json

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

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

@@ -5,6 +5,8 @@ let resolveStep = () => {}
 let logger = () => {}
 
 export function morph(from, toHtml, options) {
+    monkeyPatchDomSetAttributeToAllowAtSymbols()
+
     // 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
     // each other.
@@ -384,3 +386,32 @@ function initializeAlpineOnTo(from, to, childrenOnly) {
         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",
     "license": "MIT",
     "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",
-    "version": "3.12.0",
+    "version": "3.12.2",
     "description": "Persist Alpine data across page loads",
     "homepage": "https://alpinejs.dev/plugins/persist",
     "repository": {

+ 1 - 1
packages/ui/package.json

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

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

@@ -40,7 +40,7 @@ test('style attribute object binding with CSS variable',
         </div>
     `,
     ({ 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>
     `,
     ({ 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>
     `,
     ({ 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',
     html`
@@ -391,8 +391,8 @@ test('x-bind object syntax event handlers defined as functions receive the event
         <script>
             window.data = () => { return {
                 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`
         <script>
             window.data = () => { return {
@@ -467,3 +467,22 @@ test('Can extract Alpine bound data as a data prop',
         get('#2').should(haveAttribute('foo', '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'))
     }
 )
+
+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
+)

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

@@ -151,5 +151,40 @@ test('x-model with fill modifier takes input value on null or empty string',
     }
 )
 
+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]));
+    }
+);
+
 
 

+ 33 - 0
tests/cypress/integration/directives/x-on.spec.js

@@ -97,6 +97,26 @@ test('.stop modifier',
     }
 )
 
+
+test('.stop modifier with a .throttle',
+    html`
+        <div x-data="{ foo: 'bar' }">
+            <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 }">
@@ -178,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',
     html`
         <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
     }
 )
+
+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())
+        }
+    
+);

+ 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'))
     },
 )
+
+// '@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'));
+    }
+);

+ 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
 
@@ -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 haveProperty = (name, value) => el => expect(el).to.have.prop(name, value)
+
 export let haveText = text => el => expect(el).to.have.text(text)
 
 export let notHaveText = text => el => expect(el).not.to.have.text(text)

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels