Przeglądaj źródła

Merge branch 'master' of github.com:lancebutler2/alpine into update-readme

Lance Butler II 5 lat temu
rodzic
commit
53d739d325
12 zmienionych plików z 223 dodań i 77 usunięć
  1. 33 12
      README.md
  2. 0 0
      dist/alpine.js
  3. 0 0
      dist/alpine.js.map
  4. 1 1
      package.json
  5. 74 41
      src/component.js
  6. 1 1
      src/utils.js
  7. 13 1
      test/data.spec.js
  8. 22 0
      test/el.spec.js
  9. 0 18
      test/init.spec.js
  10. 76 0
      test/lifecycle.spec.js
  11. 1 1
      test/model.spec.js
  12. 2 2
      test/on.spec.js

+ 33 - 12
README.md

@@ -14,7 +14,7 @@ Think of it like [Tailwind](https://tailwindcss.com/) for JavaScript.
 
 
 **From CDN:** Add the following script to the end of your `<head>` section.
 **From CDN:** Add the following script to the end of your `<head>` section.
 ```html
 ```html
-<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v1.5.0/dist/alpine.js" defer></script>
+<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v1.6.0/dist/alpine.js" defer></script>
 ```
 ```
 
 
 That's it. It will initialize itself.
 That's it. It will initialize itself.
@@ -84,12 +84,13 @@ You can even use it for non-trivial things:
 
 
 ## Learn
 ## Learn
 
 
-There are 12 directives available to you:
+There are 13 directives available to you:
 
 
 | Directive
 | Directive
 | --- |
 | --- |
 | [`x-data`](#x-data) |
 | [`x-data`](#x-data) |
-| [`x-init`](#x-init) |
+| [`x-created`](#x-created) |
+| [`x-mounted`](#x-mounted) |
 | [`x-show`](#x-show) |
 | [`x-show`](#x-show) |
 | [`x-bind`](#x-bind) |
 | [`x-bind`](#x-bind) |
 | [`x-on`](#x-on) |
 | [`x-on`](#x-on) |
@@ -101,10 +102,11 @@ There are 12 directives available to you:
 | [`x-transition`](#x-transition) |
 | [`x-transition`](#x-transition) |
 | [`x-cloak`](#x-cloak) |
 | [`x-cloak`](#x-cloak) |
 
 
-And 2 magic objects/functions:
+And 3 magic properties:
 
 
-| Magic Object/Functions
+| Magic Properties
 | --- |
 | --- |
+| [`$el`](#el) |
 | [`$refs`](#refs) |
 | [`$refs`](#refs) |
 | [`$nextTick`](#nexttick) |
 | [`$nextTick`](#nexttick) |
 
 
@@ -155,12 +157,21 @@ You can also mix-in multiple data objects using object destructuring:
 
 
 ---
 ---
 
 
-### `x-init`
-**Example:** `<div x-data="{ foo: 'bar' }" x-init="foo = 'baz"></div>`
+### `x-created`
+**Example:** `<div x-data="{ foo: 'bar' }" x-created="foo = 'baz"></div>`
 
 
-**Structure:** `<div x-data="..." x-init="[expression]"></div>`
+**Structure:** `<div x-data="..." x-created="[expression]"></div>`
 
 
-`x-init` runs an expression (with the initial data object in scope) when a component is initialized.
+`x-created` runs an expression when a component is initialized, but BEFORE the component initializes the DOM.
+
+---
+
+### `x-mounted`
+**Example:** `<div x-data"{ foo: 'bar' }" x-mounted="foo = 'baz"></div>`
+
+**Structure:** `<div x-data="..." x-mounted="[expression]"></div>`
+
+`x-mounted` runs an expression when a component is initialized, and AFTER the component initializes the DOM.
 
 
 ---
 ---
 
 
@@ -367,10 +378,20 @@ These behave exactly like VueJs's transition directives, except they have differ
 </style>
 </style>
 ```
 ```
 
 
-### Magic Objects/Functions
+### Magic Properties
 
 
 ---
 ---
 
 
+### `$el`
+**Example:**
+```html
+<div x-data>
+    <button @click="$el.innerHTML = 'foo'">Replace me with "foo"</button>
+</div>
+```
+
+`$el` is a magic property that can be used to retreive the root component DOM node.
+
 ### `$refs`
 ### `$refs`
 **Example:**
 **Example:**
 ```html
 ```html
@@ -379,7 +400,7 @@ These behave exactly like VueJs's transition directives, except they have differ
 <button x-on:click="$refs.foo.innerText = 'bar'">
 <button x-on:click="$refs.foo.innerText = 'bar'">
 ```
 ```
 
 
-`$refs` is a magic object that can be used to retreive DOM elements marked with `x-ref` inside the component. This is useful when you need to manually manipulate DOM elements.
+`$refs` is a magic property that can be used to retreive DOM elements marked with `x-ref` inside the component. This is useful when you need to manually manipulate DOM elements.
 
 
 ---
 ---
 
 
@@ -397,4 +418,4 @@ These behave exactly like VueJs's transition directives, except they have differ
 </div>
 </div>
 ```
 ```
 
 
-`$nextTick` is a magic function that allows you to only execute a given expression AFTER Alpine has made it's reactive DOM updates. This is useful for times you want to interact with the DOM state AFTER it's reflected any data updates you've made.
+`$nextTick` is a magic property that allows you to only execute a given expression AFTER Alpine has made it's reactive DOM updates. This is useful for times you want to interact with the DOM state AFTER it's reflected any data updates you've made.

Plik diff jest za duży
+ 0 - 0
dist/alpine.js


Plik diff jest za duży
+ 0 - 0
dist/alpine.js.map


+ 1 - 1
package.json

@@ -1,7 +1,7 @@
 {
 {
   "main": "dist/alpine.js",
   "main": "dist/alpine.js",
   "name": "alpinejs",
   "name": "alpinejs",
-  "version": "1.5.0",
+  "version": "1.6.0",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
     "url": "git://github.com/alpinejs/alpine.git"
     "url": "git://github.com/alpinejs/alpine.git"

+ 74 - 41
src/component.js

@@ -2,52 +2,60 @@ import { walkSkippingNestedComponents, kebabCase, saferEval, saferEvalNoReturn,
 
 
 export default class Component {
 export default class Component {
     constructor(el) {
     constructor(el) {
-        this.el = el
+        this.$el = el
 
 
-        // For $nextTick().
-        this.tickStack = []
-        this.collectingTickCallbacks = false
+        const dataAttr = this.$el.getAttribute('x-data')
+        const dataExpression = dataAttr === '' ? '{}' : dataAttr
+        const createdExpression = this.$el.getAttribute('x-created')
+        const mountedExpression = this.$el.getAttribute('x-mounted')
 
 
-        const rawData = saferEval(this.el.getAttribute('x-data'), {})
+        const unobservedData = saferEval(dataExpression, {})
 
 
-        rawData.$refs =  this.getRefsProxy()
+        // Construct a Proxy-based observable. This will be used to handle reactivity.
+        this.$data = this.wrapDataInObservable(unobservedData)
 
 
-        rawData.$nextTick =  (callback) => {
+        // Walk through the raw data and set the "this" context of any functions
+        // to the observable, so data manipulations are reactive.
+        Object.keys(unobservedData).forEach(key => {
+            if (typeof unobservedData[key] === 'function') {
+                unobservedData[key] = unobservedData[key].bind(this.$data)
+            }
+        })
+
+        // After making user-supplied data methods reactive, we can now add
+        // our magic properties to the original data for access.
+        unobservedData.$el = this.$el
+        unobservedData.$refs = this.getRefsProxy()
+        unobservedData.$nextTick = (callback) => {
             this.delayRunByATick(callback)
             this.delayRunByATick(callback)
         }
         }
 
 
-        this.runXInit(this.el.getAttribute('x-init'), rawData)
+        // For $nextTick().
+        this.tickStack = []
+        this.collectingTickCallbacks = false
 
 
-        this.data = this.wrapDataInObservable(rawData)
+        if (createdExpression) {
+            // 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
+            saferEvalNoReturn(this.$el.getAttribute('x-created'), this.$data)
+            this.pauseReactivity = false
+        }
 
 
+        // Register all our listeners and set all our attribute bindings.
         this.initializeElements()
         this.initializeElements()
 
 
+        // Use mutation observer to detect new elements being added within this component at run-time.
+        // Alpine's just so darn flexible amirite?
         this.listenForNewElementsToInitialize()
         this.listenForNewElementsToInitialize()
-    }
 
 
-    delayRunByATick(callback) {
-        if (this.collectingTickCallbacks) {
-            this.tickStack.push(callback)
-        } else {
-            callback()
+        if (mountedExpression) {
+            // Run an "x-mounted" hook to allow the user to do stuff after
+            // Alpine's got it's grubby little paws all over everything.
+            saferEvalNoReturn(mountedExpression, this.$data)
         }
         }
     }
     }
 
 
-    startTick() {
-        this.collectingTickCallbacks = true
-    }
-
-    clearAndEndTick() {
-        this.tickStack.forEach(callable => callable())
-        this.tickStack = []
-
-        this.collectingTickCallbacks = false
-    }
-
-    runXInit(initExpression, rawData) {
-        initExpression && saferEvalNoReturn(initExpression, rawData)
-    }
-
     wrapDataInObservable(data) {
     wrapDataInObservable(data) {
         this.concernedData = []
         this.concernedData = []
 
 
@@ -59,6 +67,9 @@ export default class Component {
 
 
                 const setWasSuccessful = Reflect.set(obj, property, value)
                 const setWasSuccessful = Reflect.set(obj, property, value)
 
 
+                // Don't react to data changes for cases like the `x-created` hook.
+                if (self.pauseReactivity) return
+
                 if (self.concernedData.indexOf(propertyName) === -1) {
                 if (self.concernedData.indexOf(propertyName) === -1) {
                     self.concernedData.push(propertyName)
                     self.concernedData.push(propertyName)
                 }
                 }
@@ -68,17 +79,21 @@ export default class Component {
                 return setWasSuccessful
                 return setWasSuccessful
             },
             },
             get(target, key) {
             get(target, key) {
+                // This is because there is no way to do something like `typeof foo === 'Proxy'`.
                 if (key === 'isProxy') return true
                 if (key === 'isProxy') return true
 
 
                 // If the property we are trying to get is a proxy, just return it.
                 // If the property we are trying to get is a proxy, just return it.
+                // Like in the case of $refs
                 if (target[key] && target[key].isProxy) return target[key]
                 if (target[key] && target[key].isProxy) return target[key]
 
 
+                // If accessing a nested property, retur this proxy recursively.
                 if (typeof target[key] === 'object' && target[key] !== null) {
                 if (typeof target[key] === 'object' && target[key] !== null) {
                     const propertyName = keyPrefix ? `${keyPrefix}.${key}` : key
                     const propertyName = keyPrefix ? `${keyPrefix}.${key}` : key
 
 
                     return new Proxy(target[key], proxyHandler(propertyName))
                     return new Proxy(target[key], proxyHandler(propertyName))
                 }
                 }
 
 
+                // If none of the above, just return the flippin' value. Gawsh.
                 return target[key]
                 return target[key]
             }
             }
         })
         })
@@ -86,8 +101,27 @@ export default class Component {
         return new Proxy(data, proxyHandler())
         return new Proxy(data, proxyHandler())
     }
     }
 
 
+    delayRunByATick(callback) {
+        if (this.collectingTickCallbacks) {
+            this.tickStack.push(callback)
+        } else {
+            callback()
+        }
+    }
+
+    startTick() {
+        this.collectingTickCallbacks = true
+    }
+
+    clearAndEndTick() {
+        this.tickStack.forEach(callable => callable())
+        this.tickStack = []
+
+        this.collectingTickCallbacks = false
+    }
+
     initializeElements() {
     initializeElements() {
-        walkSkippingNestedComponents(this.el, el => {
+        walkSkippingNestedComponents(this.$el, el => {
             this.initializeElement(el)
             this.initializeElement(el)
         })
         })
     }
     }
@@ -154,7 +188,7 @@ export default class Component {
     }
     }
 
 
     listenForNewElementsToInitialize() {
     listenForNewElementsToInitialize() {
-        const targetNode = this.el
+        const targetNode = this.$el
 
 
         const observerOptions = {
         const observerOptions = {
             childList: true,
             childList: true,
@@ -165,14 +199,14 @@ export default class Component {
         const observer = new MutationObserver((mutations) => {
         const observer = new MutationObserver((mutations) => {
             for (let i=0; i < mutations.length; i++){
             for (let i=0; i < mutations.length; i++){
                 // Filter out mutations triggered from child components.
                 // Filter out mutations triggered from child components.
-                if (! mutations[i].target.closest('[x-data]').isSameNode(this.el)) return
+                if (! mutations[i].target.closest('[x-data]').isSameNode(this.$el)) return
 
 
                 if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'x-data') {
                 if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'x-data') {
                     const rawData = saferEval(mutations[i].target.getAttribute('x-data'), {})
                     const rawData = saferEval(mutations[i].target.getAttribute('x-data'), {})
 
 
                     Object.keys(rawData).forEach(key => {
                     Object.keys(rawData).forEach(key => {
-                        if (this.data[key] !== rawData[key]) {
-                            this.data[key] = rawData[key]
+                        if (this.$data[key] !== rawData[key]) {
+                            this.$data[key] = rawData[key]
                         }
                         }
                     })
                     })
                 }
                 }
@@ -215,7 +249,7 @@ export default class Component {
 
 
         this.startTick()
         this.startTick()
 
 
-        debounce(walkThenClearDependancyTracker, 5)(this.el, function (el) {
+        debounce(walkThenClearDependancyTracker, 5)(this.$el, function (el) {
             getXAttrs(el).forEach(({ type, value, expression }) => {
             getXAttrs(el).forEach(({ type, value, expression }) => {
                 if (! actionByDirectiveType[type]) return
                 if (! actionByDirectiveType[type]) return
 
 
@@ -232,7 +266,7 @@ export default class Component {
         var rightSideOfExpression = ''
         var rightSideOfExpression = ''
         if (el.type === 'checkbox') {
         if (el.type === 'checkbox') {
             // If the data we are binding to is an array, toggle it's value inside the array.
             // If the data we are binding to is an array, toggle it's value inside the array.
-            if (Array.isArray(this.data[dataKey])) {
+            if (Array.isArray(this.$data[dataKey])) {
                 rightSideOfExpression = `$event.target.checked ? ${dataKey}.concat([$event.target.value]) : ${dataKey}.filter(i => i !== $event.target.value)`
                 rightSideOfExpression = `$event.target.checked ? ${dataKey}.concat([$event.target.value]) : ${dataKey}.filter(i => i !== $event.target.value)`
             } else {
             } else {
                 rightSideOfExpression = `$event.target.checked`
                 rightSideOfExpression = `$event.target.checked`
@@ -330,7 +364,7 @@ export default class Component {
             }
             }
         })
         })
 
 
-        const proxiedData = new Proxy(this.data, proxyHandler())
+        const proxiedData = new Proxy(this.$data, proxyHandler())
 
 
         const result = saferEval(expression, proxiedData)
         const result = saferEval(expression, proxiedData)
 
 
@@ -341,7 +375,7 @@ export default class Component {
     }
     }
 
 
     evaluateCommandExpression(expression, extraData) {
     evaluateCommandExpression(expression, extraData) {
-        saferEvalNoReturn(expression, this.data, extraData)
+        saferEvalNoReturn(expression, this.$data, extraData)
     }
     }
 
 
     updateTextValue(el, value) {
     updateTextValue(el, value) {
@@ -366,7 +400,6 @@ export default class Component {
                 }
                 }
             }, initialUpdate)
             }, initialUpdate)
         }
         }
-
     }
     }
 
 
     updatePresence(el, expressionResult) {
     updatePresence(el, expressionResult) {
@@ -464,7 +497,7 @@ export default class Component {
 
 
                 // We can't just query the DOM because it's hard to filter out refs in
                 // We can't just query the DOM because it's hard to filter out refs in
                 // nested components.
                 // nested components.
-                walkSkippingNestedComponents(self.el, el => {
+                walkSkippingNestedComponents(self.$el, el => {
                     if (el.hasAttribute('x-ref') && el.getAttribute('x-ref') === property) {
                     if (el.hasAttribute('x-ref') && el.getAttribute('x-ref') === property) {
                         ref = el
                         ref = el
                     }
                     }

+ 1 - 1
src/utils.js

@@ -60,7 +60,7 @@ export function saferEval(expression, dataContext, additionalHelperVariables = {
 }
 }
 
 
 export function saferEvalNoReturn(expression, dataContext, additionalHelperVariables = {}) {
 export function saferEvalNoReturn(expression, dataContext, additionalHelperVariables = {}) {
-    return (new Function(['$data', ...Object.keys(additionalHelperVariables)], `with($data) { ${expression} }`))(
+    return (new Function(['dataContext', ...Object.keys(additionalHelperVariables)], `with(dataContext) { ${expression} }`))(
         dataContext, ...Object.values(additionalHelperVariables)
         dataContext, ...Object.values(additionalHelperVariables)
     )
     )
 }
 }

+ 13 - 1
test/data.spec.js

@@ -14,7 +14,19 @@ test('data manipulated on component object is reactive', async () => {
 
 
     Alpine.start()
     Alpine.start()
 
 
-    document.querySelector('div').__x.data.foo = 'baz'
+    document.querySelector('div').__x.$data.foo = 'baz'
 
 
     await wait(() => { expect(document.querySelector('span').innerText).toEqual('baz') })
     await wait(() => { expect(document.querySelector('span').innerText).toEqual('baz') })
 })
 })
+
+test('x-data attribute value is optional', async () => {
+    document.body.innerHTML = `
+        <div x-data>
+            <span x-text="'foo'"></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('span').innerText).toEqual('foo')
+})

+ 22 - 0
test/el.spec.js

@@ -0,0 +1,22 @@
+import Alpine from 'alpinejs'
+import { wait } from '@testing-library/dom'
+
+global.MutationObserver = class {
+    observe() {}
+}
+
+test('$el', async () => {
+    document.body.innerHTML = `
+        <div x-data>
+            <button @click="$el.innerHTML = 'foo'"></button>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('div').innerHTML).not.toEqual('foo')
+
+    document.querySelector('button').click()
+
+    await wait(() => { expect(document.querySelector('div').innerHTML).toEqual('foo') })
+})

+ 0 - 18
test/init.spec.js

@@ -1,18 +0,0 @@
-import Alpine from 'alpinejs'
-import { wait } from '@testing-library/dom'
-
-global.MutationObserver = class {
-    observe() {}
-}
-
-test('x-init', async () => {
-    document.body.innerHTML = `
-        <div x-data="{ foo: 'bar' }" x-init="foo = 'baz'">
-            <span x-text="foo"></span>
-        </div>
-    `
-
-    Alpine.start()
-
-    expect(document.querySelector('span').innerText).toEqual('baz')
-})

+ 76 - 0
test/lifecycle.spec.js

@@ -0,0 +1,76 @@
+import Alpine from 'alpinejs'
+import { wait } from '@testing-library/dom'
+
+global.MutationObserver = class {
+    observe() {}
+}
+
+test('x-created', async () => {
+    var spanValue
+    window.setSpanValue = (el) => {
+        spanValue = el.innerHTML
+    }
+
+    document.body.innerHTML = `
+        <div x-data="{ foo: 'bar' }" x-created="window.setSpanValue($refs.foo)">
+            <span x-text="foo" x-ref="foo">baz</span>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(spanValue).toEqual('baz')
+})
+
+test('x-mounted', async () => {
+    var spanValue
+    window.setSpanValue = (el) => {
+        spanValue = el.innerText
+    }
+
+    document.body.innerHTML = `
+        <div x-data="{ foo: 'bar' }" x-mounted="window.setSpanValue($refs.foo)">
+            <span x-text="foo" x-ref="foo">baz</span>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(spanValue).toEqual('bar')
+})
+
+test('callbacks registered within x-created can affect reactive data changes', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ bar: 'baz', foo() { this.$refs.foo.addEventListener('click', () => { this.bar = 'bob' }) } }" x-created="foo()">
+            <button x-ref="foo"></button>
+
+            <span x-text="bar"></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('span').innerText).toEqual('baz')
+
+    document.querySelector('button').click()
+
+    await wait(() => { expect(document.querySelector('span').innerText).toEqual('bob') })
+})
+
+test('callbacks registered within x-mounted can affect reactive data changes', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ bar: 'baz', foo() { this.$refs.foo.addEventListener('click', () => { this.bar = 'bob' }) } }" x-mounted="foo()">
+            <button x-ref="foo"></button>
+
+            <span x-text="bar"></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('span').innerText).toEqual('baz')
+
+    document.querySelector('button').click()
+
+    await wait(() => { expect(document.querySelector('span').innerText).toEqual('bob') })
+})

+ 1 - 1
test/model.spec.js

@@ -58,7 +58,7 @@ test('x-model casts value to number if number modifier is present', async () =>
 
 
     fireEvent.input(document.querySelector('input'), { target: { value: '123' }})
     fireEvent.input(document.querySelector('input'), { target: { value: '123' }})
 
 
-    await wait(() => { expect(document.querySelector('[x-data]').__x.data.foo).toEqual(123) })
+    await wait(() => { expect(document.querySelector('[x-data]').__x.$data.foo).toEqual(123) })
 })
 })
 
 
 test('x-model trims value if trim modifier is present', async () => {
 test('x-model trims value if trim modifier is present', async () => {

+ 2 - 2
test/on.spec.js

@@ -54,12 +54,12 @@ test('.stop modifier', async () => {
 
 
     Alpine.start()
     Alpine.start()
 
 
-    expect(document.querySelector('div').__x.data.foo).toEqual('bar')
+    expect(document.querySelector('div').__x.$data.foo).toEqual('bar')
 
 
     document.querySelector('span').click()
     document.querySelector('span').click()
 
 
     await wait(() => {
     await wait(() => {
-        expect(document.querySelector('div').__x.data.foo).toEqual('baz')
+        expect(document.querySelector('div').__x.$data.foo).toEqual('baz')
     })
     })
 })
 })
 
 

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików