Explorar el Código

Enable `x-model` CustomEvent listening on .detail

Caleb Porzio hace 5 años
padre
commit
c31add7d1a
Se han modificado 8 ficheros con 95 adiciones y 25 borrados
  1. 27 0
      README.md
  2. 0 0
      dist/alpine.js
  3. 0 0
      dist/alpine.js.map
  4. 1 0
      rollup.config.js
  5. 1 1
      src/component.js
  6. 30 24
      src/directives/model.js
  7. 16 0
      test/lifecycle.spec.js
  8. 20 0
      test/model.spec.js

+ 27 - 0
README.md

@@ -108,6 +108,7 @@ And 3 magic properties:
 | --- |
 | [`$el`](#el) |
 | [`$refs`](#refs) |
+| [`$dispatch`](#dispatch) |
 | [`$nextTick`](#nexttick) |
 
 ### Directives
@@ -415,6 +416,32 @@ These behave exactly like VueJs's transition directives, except they have differ
 
 ---
 
+### `$dispatch`
+**Example:**
+```html
+<div @custom-event="console.log($event.detail.foo)">
+    <button @click="$dispatch('custom-event', { foo: 'bar' })">
+    <!-- When clicked, will console.log "bar" -->
+</div>
+```
+
+`$dispatch` is a shortcut for creating a `CustomEvent` and dispatching it using `.dispatchEvent()` internally. There are lots of good use cases for passing data around and between components using custom events. [Read here](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events) for more information on the underlying `CustomEvent` system in browsers.
+
+You will notice that any data passed as the second parameter to `$dispatch('some-event', { some: 'data' })`, becomes available through the new events "detail" property: `$event.detail.some`. Attaching custom event data to the `.detail` property is standard practice for `CustomEvent`s in browsers. [Read here](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail) for more info.
+
+You can also use `$dispatch()` to trigger data updates for `x-model` bindings. For example:
+
+```html
+<div x-data="{ foo: 'bar' }">
+    <span x-model="foo">
+        <button @click="$dispatch('input', 'baz')">
+        <!-- After the button is clicked, `x-model` will catch the bubbling "input" event, and update foo to "baz". -->
+    </span>
+</div>
+```
+
+---
+
 ### `$nextTick`
 **Example:**
 ```html

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
dist/alpine.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
dist/alpine.js.map


+ 1 - 0
rollup.config.js

@@ -15,6 +15,7 @@ export default {
         resolve(),
         filesize(),
         terser({
+            mangle: false,
             compress: {
                 drop_debugger: false,
             },

+ 1 - 1
src/component.js

@@ -36,7 +36,7 @@ export default class Component {
             // We want to allow data manipulation, but not trigger DOM updates just yet.
             // We haven't even initialized the elements with their Alpine bindings. I mean c'mon.
             this.pauseReactivity = true
-            initReturnedCallback = saferEval(this.$el.getAttribute('x-init'), this.$data)
+            initReturnedCallback = this.evaluateReturnExpression(this.$el, initExpression)
             this.pauseReactivity = false
         }
 

+ 30 - 24
src/directives/model.js

@@ -1,6 +1,6 @@
 import { registerListener } from './on'
 
-export function registerModelListener(component, el, modifiers, expression, extraVars = {}) {
+export function registerModelListener(component, el, modifiers, expression, extraVars) {
     // If the element we are binding to is a select, a radio, or checkbox
     // we'll listen for the change event instead of the "input" event.
     var event = (el.tagName.toLowerCase() === 'select')
@@ -8,36 +8,42 @@ export function registerModelListener(component, el, modifiers, expression, extr
         || modifiers.includes('lazy')
         ? 'change' : 'input'
 
-    const listenerExpression = modelListenerExpression(component, el, modifiers, expression)
+    const listenerExpression = `${expression} = rightSideOfExpression($event, ${expression})`
 
-    registerListener(component, el, event, modifiers, listenerExpression, extraVars)
-}
-
-function modelListenerExpression(component, el, modifiers, dataKey) {
-    var rightSideOfExpression = ''
-    if (el.type === 'checkbox') {
-        // If the data we are binding to is an array, toggle it's value inside the array.
-        if (Array.isArray(component.$data[dataKey])) {
-            rightSideOfExpression = `$event.target.checked ? ${dataKey}.concat([$event.target.value]) : ${dataKey}.filter(i => i !== $event.target.value)`
-        } else {
-            rightSideOfExpression = `$event.target.checked`
+    registerListener(component, el, event, modifiers, listenerExpression, () => {
+        return {
+            ...extraVars(),
+            rightSideOfExpression: generateModelAssignmentFunction(el, modifiers, expression),
         }
-    } else if (el.tagName.toLowerCase() === 'select' && el.multiple) {
-        rightSideOfExpression = modifiers.includes('number')
-            ? 'Array.from($event.target.selectedOptions).map(option => { return parseFloat(option.value || option.text) })'
-            : 'Array.from($event.target.selectedOptions).map(option => { return option.value || option.text })'
-    } else {
-        rightSideOfExpression = modifiers.includes('number')
-            ? 'parseFloat($event.target.value)'
-            : (modifiers.includes('trim') ? '$event.target.value.trim()' : '$event.target.value')
-    }
+    })
+}
 
+function generateModelAssignmentFunction(el, modifiers, expression) {
     if (el.type === 'radio') {
         // Radio buttons only work properly when they share a name attribute.
         // People might assume we take care of that for them, because
         // they already set a shared "x-model" attribute.
-        if (! el.hasAttribute('name')) el.setAttribute('name', dataKey)
+        if (! el.hasAttribute('name')) el.setAttribute('name', expression)
     }
 
-    return `${dataKey} = ${rightSideOfExpression}`
+    return (event, currentValue) => {
+        if (event instanceof CustomEvent) {
+            return event.detail
+        } else if (el.type === 'checkbox') {
+            // If the data we are binding to is an array, toggle it's value inside the array.
+            if (Array.isArray(currentValue)) {
+                return event.target.checked ? currentValue.concat([event.target.value]) : currentValue.filter(i => i !== event.target.value)
+            } else {
+                return event.target.checked
+            }
+        } else if (el.tagName.toLowerCase() === 'select' && el.multiple) {
+            return modifiers.includes('number')
+                ? Array.from(event.target.selectedOptions).map(option => { return parseFloat(option.value || option.text) })
+                : Array.from(event.target.selectedOptions).map(option => { return option.value || option.text })
+        } else {
+            return modifiers.includes('number')
+                ? parseFloat(event.target.value)
+                : (modifiers.includes('trim') ? event.target.value.trim() : event.target.value)
+        }
+    }
 }

+ 16 - 0
test/lifecycle.spec.js

@@ -121,3 +121,19 @@ test('callbacks registered within x-mounted can affect reactive data changes', a
 
     await wait(() => { expect(document.querySelector('span').innerText).toEqual('bob') })
 })
+
+test('x-init is capable of dispatching an event', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ foo: 'bar' }" @update-foo="foo = $event.detail.foo">
+            <div x-data x-init="$dispatch('update-foo', { foo: 'baz' })"></div>
+
+            <span x-text="foo"></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    await wait(() => {
+        expect(document.querySelector('span').innerText).toEqual('baz')
+    })
+})

+ 20 - 0
test/model.spec.js

@@ -262,3 +262,23 @@ test('x-model undefined nested model key defaults to empty string', async () =>
         expect(document.querySelector('span').innerText).toEqual('bar')
     })
 })
+
+test('x-model can listen for custom input event dispatches', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ foo: 'bar' }" x-model="foo">
+            <button @click="$dispatch('input', 'baz')"></button>
+
+            <span x-text="foo"></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('span').innerText).toEqual('bar')
+
+    document.querySelector('button').click()
+
+    await wait(() => {
+        expect(document.querySelector('span').innerText).toEqual('baz')
+    })
+})

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio