瀏覽代碼

Merge branch 'main' of https://github.com/alpinejs/alpine into jlb/combobox

Simone Todaro 2 年之前
父節點
當前提交
d4534b7a82

+ 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
packages/alpinejs/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "alpinejs",
     "name": "alpinejs",
-    "version": "3.10.5",
+    "version": "3.11.1",
     "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, extractProp } 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)

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

+ 6 - 2
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
@@ -62,7 +63,10 @@ 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()))
     })
     })
 
 
@@ -127,7 +131,7 @@ function getInputValue(el, modifiers, event, currentValue) {
         // 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) {
         if (event instanceof CustomEvent && event.detail !== undefined) {
-            return event.detail || event.target.value
+            return typeof event.detail != 'undefined' ? event.detail : event.target.value
         } else if (el.type === 'checkbox') {
         } 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)) {

+ 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

@@ -41,11 +41,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 evaluator
     return evaluator
     return tryCatch.bind(null, el, expression, evaluator)
     return tryCatch.bind(null, el, expression, evaluator)

+ 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
 }
 }

+ 1 - 1
packages/collapse/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/collapse",
     "name": "@alpinejs/collapse",
-    "version": "3.10.5",
+    "version": "3.11.1",
     "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.11.1-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 }) => {

+ 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.
 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.11.1/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', () => {

+ 1 - 1
packages/focus/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/focus",
     "name": "@alpinejs/focus",
-    "version": "3.10.5",
+    "version": "3.11.1",
     "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": {

+ 1 - 1
packages/intersect/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/intersect",
     "name": "@alpinejs/intersect",
-    "version": "3.10.5",
+    "version": "3.11.1",
     "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.11.1",
     "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": {

+ 1 - 1
packages/morph/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/morph",
     "name": "@alpinejs/morph",
-    "version": "3.10.5",
+    "version": "3.11.1",
     "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": {

+ 1 - 1
packages/persist/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/persist",
     "name": "@alpinejs/persist",
-    "version": "3.10.5",
+    "version": "3.11.1",
     "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": {

+ 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'))
+    }
+)

+ 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())
+    }
+)

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

+ 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?