소스 검색

Merge pull request #1 from alpinejs/master

Add $el, change x-init to x-created and x-mounted, and improve reacti…
Lance Butler II 5 년 전
부모
커밋
7e423376bb
12개의 변경된 파일223개의 추가작업 그리고 77개의 파일을 삭제
  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.
 ```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.
@@ -84,12 +84,13 @@ You can even use it for non-trivial things:
 
 ## Learn
 
-There are 12 directives available to you:
+There are 13 directives available to you:
 
 | Directive
 | --- |
 | [`x-data`](#x-data) |
-| [`x-init`](#x-init) |
+| [`x-created`](#x-created) |
+| [`x-mounted`](#x-mounted) |
 | [`x-show`](#x-show) |
 | [`x-bind`](#x-bind) |
 | [`x-on`](#x-on) |
@@ -101,10 +102,11 @@ There are 12 directives available to you:
 | [`x-transition`](#x-transition) |
 | [`x-cloak`](#x-cloak) |
 
-And 2 magic objects/functions:
+And 3 magic properties:
 
-| Magic Object/Functions
+| Magic Properties
 | --- |
+| [`$el`](#el) |
 | [`$refs`](#refs) |
 | [`$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>
 ```
 
-### 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`
 **Example:**
 ```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'">
 ```
 
-`$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>
 ```
 
-`$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.

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
dist/alpine.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
dist/alpine.js.map


+ 1 - 1
package.json

@@ -1,7 +1,7 @@
 {
   "main": "dist/alpine.js",
   "name": "alpinejs",
-  "version": "1.5.0",
+  "version": "1.6.0",
   "repository": {
     "type": "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 {
     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.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()
 
+        // Use mutation observer to detect new elements being added within this component at run-time.
+        // Alpine's just so darn flexible amirite?
         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) {
         this.concernedData = []
 
@@ -59,6 +67,9 @@ export default class Component {
 
                 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) {
                     self.concernedData.push(propertyName)
                 }
@@ -68,17 +79,21 @@ export default class Component {
                 return setWasSuccessful
             },
             get(target, key) {
+                // This is because there is no way to do something like `typeof foo === 'Proxy'`.
                 if (key === 'isProxy') return true
 
                 // 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 accessing a nested property, retur this proxy recursively.
                 if (typeof target[key] === 'object' && target[key] !== null) {
                     const propertyName = keyPrefix ? `${keyPrefix}.${key}` : key
 
                     return new Proxy(target[key], proxyHandler(propertyName))
                 }
 
+                // If none of the above, just return the flippin' value. Gawsh.
                 return target[key]
             }
         })
@@ -86,8 +101,27 @@ export default class Component {
         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() {
-        walkSkippingNestedComponents(this.el, el => {
+        walkSkippingNestedComponents(this.$el, el => {
             this.initializeElement(el)
         })
     }
@@ -154,7 +188,7 @@ export default class Component {
     }
 
     listenForNewElementsToInitialize() {
-        const targetNode = this.el
+        const targetNode = this.$el
 
         const observerOptions = {
             childList: true,
@@ -165,14 +199,14 @@ export default class Component {
         const observer = new MutationObserver((mutations) => {
             for (let i=0; i < mutations.length; i++){
                 // 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') {
                     const rawData = saferEval(mutations[i].target.getAttribute('x-data'), {})
 
                     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()
 
-        debounce(walkThenClearDependancyTracker, 5)(this.el, function (el) {
+        debounce(walkThenClearDependancyTracker, 5)(this.$el, function (el) {
             getXAttrs(el).forEach(({ type, value, expression }) => {
                 if (! actionByDirectiveType[type]) return
 
@@ -232,7 +266,7 @@ export default class Component {
         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(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)`
             } else {
                 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)
 
@@ -341,7 +375,7 @@ export default class Component {
     }
 
     evaluateCommandExpression(expression, extraData) {
-        saferEvalNoReturn(expression, this.data, extraData)
+        saferEvalNoReturn(expression, this.$data, extraData)
     }
 
     updateTextValue(el, value) {
@@ -366,7 +400,6 @@ export default class Component {
                 }
             }, initialUpdate)
         }
-
     }
 
     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
                 // nested components.
-                walkSkippingNestedComponents(self.el, el => {
+                walkSkippingNestedComponents(self.$el, el => {
                     if (el.hasAttribute('x-ref') && el.getAttribute('x-ref') === property) {
                         ref = el
                     }

+ 1 - 1
src/utils.js

@@ -60,7 +60,7 @@ export function saferEval(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)
     )
 }

+ 13 - 1
test/data.spec.js

@@ -14,7 +14,19 @@ test('data manipulated on component object is reactive', async () => {
 
     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') })
 })
+
+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' }})
 
-    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 () => {

+ 2 - 2
test/on.spec.js

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

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.