Browse Source

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

Simone Todaro 2 years ago
parent
commit
d4534b7a82

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

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

+ 1 - 1
packages/alpinejs/package.json

@@ -1,6 +1,6 @@
 {
     "name": "alpinejs",
-    "version": "3.10.5",
+    "version": "3.11.1",
     "description": "The rugged, minimal JavaScript framework",
     "homepage": "https://alpinejs.dev",
     "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 { throttle } from './utils/throttle'
 import { setStyles } from './utils/styles'
-import { entangle } from './entangle'
 import { nextTick } from './nextTick'
 import { walk } from './utils/walk'
 import { plugin } from './plugin'
@@ -53,7 +52,6 @@ let Alpine = {
     setStyles, // INTERNAL
     mutateDom,
     directive,
-    entangle,
     throttle,
     debounce,
     evaluate,

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

@@ -2,7 +2,7 @@ import { effect, release, overrideEffect } from "./reactivity"
 import { initTree, isRoot } from "./lifecycle"
 import { walk } from "./utils/walk"
 
-let isCloning = false
+export let isCloning = false
 
 export function skipDuringClone(callback, fallback = () => {}) {
     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 })
 
-    if (data === undefined) data = {}
+    if (data === undefined || data === true) data = {}
 
     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 on from '../utils/on'
 import { warn } from '../utils/warn'
+import { isCloning } from '../clone'
 
 directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
     let scopeTarget = el
@@ -62,7 +63,10 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
         || modifiers.includes('lazy')
             ? '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()))
     })
 
@@ -127,7 +131,7 @@ function getInputValue(el, modifiers, event, currentValue) {
         // 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 event.detail || event.target.value
+            return typeof event.detail != 'undefined' ? 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)) {

+ 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 }) {
     let firstRun = true
-    let outerHash, innerHash
+    let outerHash, innerHash, outerHashLatest, innerHashLatest
 
     let reference = effect(() => {
         let outer, inner

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

@@ -41,11 +41,9 @@ export function normalEvaluator(el, expression) {
 
     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 tryCatch.bind(null, el, expression, evaluator)

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

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

+ 1 - 1
packages/collapse/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@alpinejs/collapse",
-    "version": "3.10.5",
+    "version": "3.11.1",
     "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.10.5-revision.1",
+    "version": "3.11.1-revision.1",
     "description": "The documentation for Alpine",
     "author": "Caleb Porzio",
     "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".
 
-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>
 ## 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>
 ### 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
 
 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
 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.
 
 ```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.

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

@@ -87,7 +87,7 @@ document.addEventListener('alpine:init', () => {
 <a name="alpine-initialized"></a>
 ### `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
 document.addEventListener('alpine:initialized', () => {

+ 1 - 1
packages/focus/package.json

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

+ 1 - 1
packages/intersect/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@alpinejs/intersect",
-    "version": "3.10.5",
+    "version": "3.11.1",
     "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.10.5",
+    "version": "3.11.1",
     "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.10.5",
+    "version": "3.11.1",
     "description": "Diff and patch a block of HTML on a page with an HTML template",
     "homepage": "https://alpinejs.dev/plugins/morph",
     "repository": {

+ 1 - 1
packages/persist/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@alpinejs/persist",
-    "version": "3.10.5",
+    "version": "3.11.1",
     "description": "Persist Alpine data across page loads",
     "homepage": "https://alpinejs.dev/plugins/persist",
     "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'))
     }
 )
+
+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())
     }
 )
+
+// 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'
 
-test('can entangle to getter/setter pairs',
+test.skip('can entangle to getter/setter pairs',
     [html`
     <div x-data="{ outer: 'foo' }">
         <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`
         <div x-data="{ outer: 'foo' }">
             <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'
 
-// Test persistant peice of layout
+// Test persistent piece of layout
 // Handle non-origin links and such
 // Handle 404
 // Middle/command click link in new tab works?