Sfoglia il codice sorgente

Add x-modelable (#2654)

* Code

* wip

* wip
Caleb Porzio 3 anni fa
parent
commit
161170520c

+ 1 - 0
packages/alpinejs/src/directives.js

@@ -165,6 +165,7 @@ let directiveOrder = [
     'init',
     'for',
     'model',
+    'modelable',
     'transition',
     'show',
     'if',

+ 1 - 0
packages/alpinejs/src/directives/index.js

@@ -1,4 +1,5 @@
 import './x-transition'
+import './x-modelable'
 import './x-teleport'
 import './x-ignore'
 import './x-effect'

+ 26 - 0
packages/alpinejs/src/directives/x-modelable.js

@@ -0,0 +1,26 @@
+import { evaluateLater } from '../evaluator'
+import { directive } from '../directives'
+
+directive('modelable', (el, { expression }, { effect, evaluate, evaluateLater }) => {
+    let func = evaluateLater(expression)
+    let innerGet = () => { let result; func(i => result = i); return result; }
+    let evaluateInnerSet = evaluateLater(`${expression} = __placeholder`)
+    let innerSet = val => evaluateInnerSet(() => {}, { scope: { '__placeholder': val }})
+
+    let initialValue = innerGet()
+
+    // Allow packages like Livewire to hook into $modelable. Ex: `wire:model.defer`
+    if (el._x_modelable_hook) initialValue = el._x_modelable_hook(initialValue)
+
+    innerSet(initialValue)
+
+    queueMicrotask(() => {
+        if (! el._x_model) return
+    
+        let outerGet = el._x_model.get
+        let outerSet = el._x_model.set
+    
+        effect(() => innerSet(outerGet()))
+        effect(() => outerSet(innerGet()))
+    })
+})

+ 37 - 0
packages/docs/src/en/directives/modelable.md

@@ -0,0 +1,37 @@
+---
+order: 7
+title: modelable
+---
+
+# x-modelable
+
+`x-modelable` allows you to expose any value by name as the target of the `x-model` directive.
+
+Typically this feature would be used in conjunction with a backend templating framework like Laravel Blade. It's useful for abstracting away Alpine components into backend templates and exposing state to the outside through `x-model` as if it were a native input.
+
+Here's a simple example of using `x-modelable` to expose a variable for binding with `x-model`.
+
+```alpine
+<div x-data="{ number: 5 }">
+    <div x-data="{ count: 0 }" x-modelable="count" x-model="numberOfItems">
+        <button @click="count++">Increment</button>
+    </div>
+
+    Some Number: <span x-text="number"></span>
+</div>
+```
+
+<!-- START_VERBATIM -->
+<div class="demo">
+    <div x-data="{ number: 5 }">
+        <div x-data="{ count: 0 }" x-modelable="count" x-model="numberOfItems">
+            <button @click="count++">Increment</button>
+        </div>
+
+        Number: <span x-text="number"></span>
+    </div>
+</div>
+<!-- END_VERBATIM -->
+
+As you can see the outer scope property "number" is now bound to the inner scope property "count".
+

+ 66 - 0
tests/cypress/integration/directives/x-modelable.spec.js

@@ -0,0 +1,66 @@
+import { haveText, html, test } from '../../utils'
+
+test('can expose data for x-model binding',
+    html`
+        <div x-data="{ outer: 'foo' }">
+            <div x-data="{ inner: 'bar' }" x-modelable="inner" x-model="outer">
+                <h1 x-text="outer"></h1>
+                <h2 x-text="inner"></h2>
+
+                <button @click="inner = 'bob'" id="1">change inner</button>
+                <button @click="outer = 'lob'" id="2">change outer</button>
+            </div>
+        </div>
+    `,
+    ({ get }) => {
+        get('h1').should(haveText('foo'))
+        get('h2').should(haveText('foo'))
+        get('#1').click()
+        get('h1').should(haveText('bob'))
+        get('h2').should(haveText('bob'))
+        get('#2').click()
+        get('h1').should(haveText('lob'))
+        get('h2').should(haveText('lob'))
+    }
+)
+
+test('Something like Livewire can hook into x-modelable',
+    html`
+        <h1 x-data="{ value: 'bar' }" x-modelable="value" x-init="
+            () => {}; $el._x_modelable_hook = (val) => {
+                return val.toUpperCase()
+            }
+        ">
+            <span x-text="value"></span>
+        </h1>
+    `,
+    ({ get }) => {
+        get('span').should(haveText('BAR'))
+    }
+)
+
+test('x-modelable works when inside x-bind and x-model is outside',
+    html`
+        <div x-data="{ outer: 'foo', thing: {
+            ['x-modelable']: 'inner',
+        } }">
+            <div x-data="{ inner: 'bar' }" x-bind="thing" x-model="outer">
+                <h1 x-text="outer"></h1>
+                <h2 x-text="inner"></h2>
+
+                <button @click="inner = 'bob'" id="1">change inner</button>
+                <button @click="outer = 'lob'" id="2">change outer</button>
+            </div>
+        </div>
+    `,
+    ({ get }) => {
+        get('h1').should(haveText('foo'))
+        get('h2').should(haveText('foo'))
+        get('#1').click()
+        get('h1').should(haveText('bob'))
+        get('h2').should(haveText('bob'))
+        get('#2').click()
+        get('h1').should(haveText('lob'))
+        get('h2').should(haveText('lob'))
+    }
+)