Browse Source

Add ability to clone components to other elements

Caleb Porzio 5 years ago
parent
commit
19eacfebf3
9 changed files with 82 additions and 71 deletions
  1. 0 0
      dist/alpine-ie11.js
  2. 0 0
      dist/alpine-ie11.js.map
  3. 0 0
      dist/alpine.js
  4. 0 0
      dist/alpine.js.map
  5. 22 25
      src/component.js
  6. 25 8
      src/directives/show.js
  7. 6 0
      src/index.js
  8. 25 0
      test/constructor.spec.js
  9. 4 38
      test/lifecycle.spec.js

File diff suppressed because it is too large
+ 0 - 0
dist/alpine-ie11.js


File diff suppressed because it is too large
+ 0 - 0
dist/alpine-ie11.js.map


File diff suppressed because it is too large
+ 0 - 0
dist/alpine.js


File diff suppressed because it is too large
+ 0 - 0
dist/alpine.js.map


+ 22 - 25
src/component.js

@@ -7,35 +7,33 @@ import { registerModelListener } from './directives/model'
 import { registerListener } from './directives/on'
 
 export default class Component {
-    constructor(el) {
+    constructor(el, seedDataForCloning = null) {
         this.$el = el
 
         const dataAttr = this.$el.getAttribute('x-data')
         const dataExpression = dataAttr === '' ? '{}' : dataAttr
         const initExpression = this.$el.getAttribute('x-init')
-        const createdExpression = this.$el.getAttribute('x-created')
-        const mountedExpression = this.$el.getAttribute('x-mounted')
 
-        const unobservedData = saferEval(dataExpression, {})
+        this.unobservedData = seedDataForCloning ? seedDataForCloning : saferEval(dataExpression, {})
 
         /* IE11-ONLY:START */
             // For IE11, add our magic properties to the original data for access.
             // The Proxy pollyfill does not allow properties to be added after creation.
-            unobservedData.$el = null
-            unobservedData.$refs = null
-            unobservedData.$nextTick = null
+            this.unobservedData.$el = null
+            this.unobservedData.$refs = null
+            this.unobservedData.$nextTick = null
         /* IE11-ONLY:END */
 
         // Construct a Proxy-based observable. This will be used to handle reactivity.
-        this.$data = this.wrapDataInObservable(unobservedData)
+        this.$data = this.wrapDataInObservable(this.unobservedData)
 
         // 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()
+        this.unobservedData.$el = this.$el
+        this.unobservedData.$refs = this.getRefsProxy()
 
         this.nextTickStack = []
-        unobservedData.$nextTick = (callback) => {
+        this.unobservedData.$nextTick = (callback) => {
             this.nextTickStack.push(callback)
         }
 
@@ -43,7 +41,8 @@ export default class Component {
         this.showDirectiveLastElement
 
         var initReturnedCallback
-        if (initExpression) {
+        // If x-init is present AND we aren't cloning (skip x-init on clone)
+        if (initExpression && ! seedDataForCloning) {
             // 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
@@ -51,13 +50,6 @@ export default class Component {
             this.pauseReactivity = false
         }
 
-        if (createdExpression) {
-            console.warn('AlpineJS Warning: "x-created" is deprecated and will be removed in the next major version. Use "x-init" instead.')
-            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.$el)
 
@@ -70,13 +62,18 @@ export default class Component {
             // Alpine's got it's grubby little paws all over everything.
             initReturnedCallback.call(this.$data)
         }
+    }
 
-        if (mountedExpression) {
-            console.warn('AlpineJS Warning: "x-mounted" is deprecated and will be removed in the next major version. Use "x-init" (with a callback return) for the same behavior.')
-            // 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)
-        }
+    getUnobservedData() {
+        let rawData = {}
+
+        Object.keys(this.unobservedData).forEach(key => {
+            if (['$el', '$refs', '$nextTick'].includes(key)) return
+
+            rawData[key] = this.unobservedData[key]
+        })
+
+        return rawData
     }
 
     wrapDataInObservable(data) {

+ 25 - 8
src/directives/show.js

@@ -1,26 +1,43 @@
 import { transitionIn, transitionOut } from '../utils'
 
 export function handleShowDirective(component, el, value, modifiers, initialUpdate = false) {
+    const hide = () => {
+        el.style.display = 'none'
+    }
+
+    const show = () => {
+        if (el.style.length === 1 && el.style.display === 'none') {
+            el.removeAttribute('style')
+        } else {
+            el.style.removeProperty('display')
+        }
+    }
+
+    if (initialUpdate === true) {
+        if (value) {
+            show()
+        } else {
+            hide()
+        }
+        return
+    }
+
     const handle = (resolve) => {
         if (! value) {
             if ( el.style.display !== 'none' ) {
                 transitionOut(el, () => {
                     resolve(() => {
-                        el.style.display = 'none'
+                        hide()
                     })
-                }, initialUpdate)
+                })
             } else {
                 resolve(() => {})
             }
         } else {
             if ( el.style.display !== '' ) {
                 transitionIn(el, () => {
-                    if (el.style.length === 1) {
-                        el.removeAttribute('style')
-                    } else {
-                        el.style.removeProperty('display')
-                    }
-                }, initialUpdate)
+                    show()
+                })
             }
 
             // Resolve immediately, only hold up parent `x-show`s for hidin.

+ 6 - 0
src/index.js

@@ -77,6 +77,12 @@ const Alpine = {
         if (! el.__x) {
             el.__x = new Component(el)
         }
+    },
+
+    clone: function (component, newEl) {
+        if (! newEl.__x) {
+            newEl.__x = new Component(newEl, component.getUnobservedData())
+        }
     }
 }
 

+ 25 - 0
test/constructor.spec.js

@@ -254,3 +254,28 @@ test('nested components only get registered once on initialization', async () =>
 
     expect(initCount).toEqual(2)
 })
+
+test('can clone an existing component to a new element', async () => {
+    global.MutationObserver = class {
+        constructor(callback) {}
+        observe() {}
+    }
+
+    document.body.innerHTML = `
+        <h1 x-data="{ foo: 'bar' }"></h1>
+
+        <div id="insert-component-here"></div>
+    `
+
+    Alpine.start()
+
+    document.querySelector('#insert-component-here').innerHTML = `
+        <h2 x-data="{ foo: 'baz' }">
+            <span x-text="foo"></span>
+        </h2>
+    `
+
+    Alpine.clone(document.querySelector('h1').__x, document.querySelector('h2'))
+
+    expect(document.querySelector('span').innerText).toEqual('bar')
+})

+ 4 - 38
test/lifecycle.spec.js

@@ -52,43 +52,9 @@ test('x-init from data function with callback return for "x-mounted" functionali
     expect(valueB).toEqual('bar')
 })
 
-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 () => {
+test('callbacks registered within x-init 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()">
+        <div x-data="{ bar: 'baz', foo() { this.$refs.foo.addEventListener('click', () => { this.bar = 'bob' }) } }" x-init="foo()">
             <button x-ref="foo"></button>
 
             <span x-text="bar"></span>
@@ -104,9 +70,9 @@ test('callbacks registered within x-created can affect reactive data changes', a
     await wait(() => { expect(document.querySelector('span').innerText).toEqual('bob') })
 })
 
-test('callbacks registered within x-mounted can affect reactive data changes', async () => {
+test('callbacks registered within x-init callback 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()">
+        <div x-data="{ bar: 'baz', foo() { this.$refs.foo.addEventListener('click', () => { this.bar = 'bob' }) } }" x-init="() => { foo() }">
             <button x-ref="foo"></button>
 
             <span x-text="bar"></span>

Some files were not shown because too many files changed in this diff