Selaa lähdekoodia

Merge pull request #20 from ryangjchandler/feature/global-store-var

Add support for global `$store` variable
Ryan Chandler 5 vuotta sitten
vanhempi
commit
3e2ac5eac1

+ 18 - 0
README.md

@@ -123,6 +123,24 @@ Spruce.stores.application.name = 'Amazing Spruce Integration'
 
 This will trigger Alpine to re-evaluate your subscribed components and re-render.
 
+### Removing the need for `x-subscribe`
+
+Alpine offers a Config API. Using this API, you can enable an experimental global `$store` variable that is declared on the `window` object. This means your components do not need to manually "subscribe" to state changes:
+
+```html
+<script>
+    Spruce.config({
+        globalStoreVariable: true
+    })
+</script>
+
+<div x-data>
+    <span x-text="$store.foo.bar"></span>
+</div>
+```
+
+> **Important**: This feature is **highly unoptimized** at the moment and will actually cause all of your Alpine components on the page to re-render. This is due to the limited API that Alpine exposes to third party libraries and the `$store` variable has no simple way of knowing which element is currently retrieving data from the global store.
+
 ## Versioning
 
 This projects follow the [Semantic Versioning](https://semver.org/) guidelines. This means that there *could* be breaking changes on minor version changes, up until v1.x is reached.

+ 1 - 1
dist/spruce.js

@@ -1,2 +1,2 @@
-var t=function(t){return null==t},e=function(r,n){return Object.keys(r).forEach(function(i){t(r[i])||Object.getPrototypeOf(r[i])!==Object.prototype||(r[i]=e(r[i],n))}),new Proxy(r,{set:function(r,i,o){return t(o)||"object"!=typeof o||(o=e(o,n)),n(i,r[i]=o),!0}})},r={stores:{},subscribers:[],start:function(){try{var t=this;return Promise.resolve(new Promise(function(t){"loading"==document.readyState?document.addEventListener("DOMContentLoaded",t):t()})).then(function(){document.querySelectorAll("[x-subscribe]").forEach(function(t){t.setAttribute("x-init",function(t){var e="$store = Spruce.subscribe($el)";return t.hasAttribute("x-init")&&(e=e+"; "+t.getAttribute("x-init")),e}(t)),t.removeAttribute("x-subscribe")}),t.stores=e(t.stores,function(e,r){t.updateSubscribers(e,r)})})}catch(t){return Promise.reject(t)}},store:function(t,e){void 0===e&&(e={}),this.stores[t]||(this.stores[t]=e)},subscribe:function(t){return this.subscribers.push(t),this.stores},updateSubscribers:function(t,e){this.subscribers.forEach(function(r){void 0!==r.__x&&(r.__x.$data.spruce=[t,e])})}},n=window.deferLoadingAlpine||function(t){t()};window.deferLoadingAlpine=function(t){window.Spruce=r,window.Spruce.start(),n(t)},module.exports=r;
+var t=function(t){return null==t},e=function(r,o){return Object.keys(r).forEach(function(n){t(r[n])||Object.getPrototypeOf(r[n])!==Object.prototype||(r[n]=e(r[n],o))}),new Proxy(r,{get:function(t,e){return o.hasOwnProperty("get")&&o.get(e),t[e]},set:function(r,n,s){return t(s)||"object"!=typeof s||(s=e(s,o)),o.set(n,r[n]=s),!0}})},r={options:{globalStoreVariable:!1},stores:{},subscribers:[],start:function(){try{var t=this;return Promise.resolve(new Promise(function(t){"loading"==document.readyState?document.addEventListener("DOMContentLoaded",t):t()})).then(function(){document.querySelectorAll("[x-subscribe]").forEach(function(t){t.setAttribute("x-init",function(t){var e="$store = Spruce.subscribe($el)";return t.hasAttribute("x-init")&&(e=e+"; "+t.getAttribute("x-init")),e}(t)),t.removeAttribute("x-subscribe")}),t.stores=e(t.stores,{set:function(e,r){t.updateSubscribers(e,r)}}),t.options.globalStoreVariable&&(document.querySelectorAll("[x-data]").forEach(function(e){t.subscribers.includes(e)||t.subscribers.push(e)}),window.$store=t.stores)})}catch(t){return Promise.reject(t)}},store:function(t,e){void 0===e&&(e={}),this.stores[t]||(this.stores[t]=e)},subscribe:function(t){return this.subscribers.push(t),this.stores},updateSubscribers:function(t,e){this.subscribers.forEach(function(t){void 0!==t.__x&&(t.__x.$data.spruce=e)})},config:function(t){void 0===t&&(t={}),this.options=Object.assign(this.options,t)}},o=window.deferLoadingAlpine||function(t){t()};window.deferLoadingAlpine=function(t){window.Spruce=r,window.Spruce.start(),o(t)},module.exports=r;
 //# sourceMappingURL=spruce.js.map

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/spruce.js.map


+ 1 - 1
dist/spruce.module.js

@@ -1,2 +1,2 @@
-var t=function(t){return null==t},e=function(r,n){return Object.keys(r).forEach(function(i){t(r[i])||Object.getPrototypeOf(r[i])!==Object.prototype||(r[i]=e(r[i],n))}),new Proxy(r,{set:function(r,i,o){return t(o)||"object"!=typeof o||(o=e(o,n)),n(i,r[i]=o),!0}})},r={stores:{},subscribers:[],start:function(){try{var t=this;return Promise.resolve(new Promise(function(t){"loading"==document.readyState?document.addEventListener("DOMContentLoaded",t):t()})).then(function(){document.querySelectorAll("[x-subscribe]").forEach(function(t){t.setAttribute("x-init",function(t){var e="$store = Spruce.subscribe($el)";return t.hasAttribute("x-init")&&(e=e+"; "+t.getAttribute("x-init")),e}(t)),t.removeAttribute("x-subscribe")}),t.stores=e(t.stores,function(e,r){t.updateSubscribers(e,r)})})}catch(t){return Promise.reject(t)}},store:function(t,e){void 0===e&&(e={}),this.stores[t]||(this.stores[t]=e)},subscribe:function(t){return this.subscribers.push(t),this.stores},updateSubscribers:function(t,e){this.subscribers.forEach(function(r){void 0!==r.__x&&(r.__x.$data.spruce=[t,e])})}},n=window.deferLoadingAlpine||function(t){t()};window.deferLoadingAlpine=function(t){window.Spruce=r,window.Spruce.start(),n(t)};export default r;
+var t=function(t){return null==t},e=function(r,n){return Object.keys(r).forEach(function(o){t(r[o])||Object.getPrototypeOf(r[o])!==Object.prototype||(r[o]=e(r[o],n))}),new Proxy(r,{get:function(t,e){return n.hasOwnProperty("get")&&n.get(e),t[e]},set:function(r,o,s){return t(s)||"object"!=typeof s||(s=e(s,n)),n.set(o,r[o]=s),!0}})},r={options:{globalStoreVariable:!1},stores:{},subscribers:[],start:function(){try{var t=this;return Promise.resolve(new Promise(function(t){"loading"==document.readyState?document.addEventListener("DOMContentLoaded",t):t()})).then(function(){document.querySelectorAll("[x-subscribe]").forEach(function(t){t.setAttribute("x-init",function(t){var e="$store = Spruce.subscribe($el)";return t.hasAttribute("x-init")&&(e=e+"; "+t.getAttribute("x-init")),e}(t)),t.removeAttribute("x-subscribe")}),t.stores=e(t.stores,{set:function(e,r){t.updateSubscribers(e,r)}}),t.options.globalStoreVariable&&(document.querySelectorAll("[x-data]").forEach(function(e){t.subscribers.includes(e)||t.subscribers.push(e)}),window.$store=t.stores)})}catch(t){return Promise.reject(t)}},store:function(t,e){void 0===e&&(e={}),this.stores[t]||(this.stores[t]=e)},subscribe:function(t){return this.subscribers.push(t),this.stores},updateSubscribers:function(t,e){this.subscribers.forEach(function(t){void 0!==t.__x&&(t.__x.$data.spruce=e)})},config:function(t){void 0===t&&(t={}),this.options=Object.assign(this.options,t)}},n=window.deferLoadingAlpine||function(t){t()};window.deferLoadingAlpine=function(t){window.Spruce=r,window.Spruce.start(),n(t)};export default r;
 //# sourceMappingURL=spruce.module.js.map

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/spruce.module.js.map


+ 1 - 1
dist/spruce.umd.js

@@ -1,2 +1,2 @@
-!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.spruce=e()}(this,function(){var t=function(t){return null==t},e=function(n,r){return Object.keys(n).forEach(function(o){t(n[o])||Object.getPrototypeOf(n[o])!==Object.prototype||(n[o]=e(n[o],r))}),new Proxy(n,{set:function(n,o,i){return t(i)||"object"!=typeof i||(i=e(i,r)),r(o,n[o]=i),!0}})},n={stores:{},subscribers:[],start:function(){try{var t=this;return Promise.resolve(new Promise(function(t){"loading"==document.readyState?document.addEventListener("DOMContentLoaded",t):t()})).then(function(){document.querySelectorAll("[x-subscribe]").forEach(function(t){t.setAttribute("x-init",function(t){var e="$store = Spruce.subscribe($el)";return t.hasAttribute("x-init")&&(e=e+"; "+t.getAttribute("x-init")),e}(t)),t.removeAttribute("x-subscribe")}),t.stores=e(t.stores,function(e,n){t.updateSubscribers(e,n)})})}catch(t){return Promise.reject(t)}},store:function(t,e){void 0===e&&(e={}),this.stores[t]||(this.stores[t]=e)},subscribe:function(t){return this.subscribers.push(t),this.stores},updateSubscribers:function(t,e){this.subscribers.forEach(function(n){void 0!==n.__x&&(n.__x.$data.spruce=[t,e])})}},r=window.deferLoadingAlpine||function(t){t()};return window.deferLoadingAlpine=function(t){window.Spruce=n,window.Spruce.start(),r(t)},n});
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.spruce=e()}(this,function(){var t=function(t){return null==t},e=function(n,r){return Object.keys(n).forEach(function(o){t(n[o])||Object.getPrototypeOf(n[o])!==Object.prototype||(n[o]=e(n[o],r))}),new Proxy(n,{get:function(t,e){return r.hasOwnProperty("get")&&r.get(e),t[e]},set:function(n,o,i){return t(i)||"object"!=typeof i||(i=e(i,r)),r.set(o,n[o]=i),!0}})},n={options:{globalStoreVariable:!1},stores:{},subscribers:[],start:function(){try{var t=this;return Promise.resolve(new Promise(function(t){"loading"==document.readyState?document.addEventListener("DOMContentLoaded",t):t()})).then(function(){document.querySelectorAll("[x-subscribe]").forEach(function(t){t.setAttribute("x-init",function(t){var e="$store = Spruce.subscribe($el)";return t.hasAttribute("x-init")&&(e=e+"; "+t.getAttribute("x-init")),e}(t)),t.removeAttribute("x-subscribe")}),t.stores=e(t.stores,{set:function(e,n){t.updateSubscribers(e,n)}}),t.options.globalStoreVariable&&(document.querySelectorAll("[x-data]").forEach(function(e){t.subscribers.includes(e)||t.subscribers.push(e)}),window.$store=t.stores)})}catch(t){return Promise.reject(t)}},store:function(t,e){void 0===e&&(e={}),this.stores[t]||(this.stores[t]=e)},subscribe:function(t){return this.subscribers.push(t),this.stores},updateSubscribers:function(t,e){this.subscribers.forEach(function(t){void 0!==t.__x&&(t.__x.$data.spruce=e)})},config:function(t){void 0===t&&(t={}),this.options=Object.assign(this.options,t)}},r=window.deferLoadingAlpine||function(t){t()};return window.deferLoadingAlpine=function(t){window.Spruce=n,window.Spruce.start(),r(t)},n});
 //# sourceMappingURL=spruce.umd.js.map

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/spruce.umd.js.map


+ 23 - 3
src/index.js

@@ -2,6 +2,10 @@ import { domReady, buildInitExpression } from './utils'
 import { createObservable } from './observable'
 
 const Spruce = {
+    options: {
+        globalStoreVariable: false,
+    },
+
     stores: {},
 
     subscribers: [],
@@ -14,9 +18,21 @@ const Spruce = {
             el.removeAttribute('x-subscribe')
         })
 
-        this.stores = createObservable(this.stores, (key, value) => {
-            this.updateSubscribers(key, value)
+        this.stores = createObservable(this.stores, {
+            set: (key, value) => {
+                this.updateSubscribers(key, value)
+            }
         })
+
+        if (this.options.globalStoreVariable) {
+            document.querySelectorAll('[x-data]').forEach(el => {
+                if (! this.subscribers.includes(el)) {
+                    this.subscribers.push(el)
+                }
+            })
+            
+            window.$store = this.stores
+        }
     },
 
     store: function (name, state = {}) {
@@ -34,9 +50,13 @@ const Spruce = {
     updateSubscribers(key, value) {
         this.subscribers.forEach(el => {
             if (el.__x !== undefined) {
-                el.__x.$data.spruce = [key, value]
+                el.__x.$data.spruce = value
             }
         })
+    },
+
+    config(options = {}) {
+        this.options = Object.assign(this.options, options)
     }
 }
 

+ 11 - 4
src/observable.js

@@ -1,19 +1,26 @@
 import { isNullOrUndefined } from './utils'
 
-export const createObservable = (target, callback) => {
+export const createObservable = (target, callbacks) => {
     Object.keys(target).forEach(key => {
         if (! isNullOrUndefined(target[key]) && Object.getPrototypeOf(target[key]) === Object.prototype) {
-            target[key] = createObservable(target[key], callback)
+            target[key] = createObservable(target[key], callbacks)
         }
     })
 
     return new Proxy(target, {
+        get(target, key) {
+            if (callbacks.hasOwnProperty('get')) {
+                callbacks.get(key)
+            }
+            
+            return target[key]
+        },
         set(target, key, value) {
             if (! isNullOrUndefined(value) && typeof value === 'object') {
-                value = createObservable(value, callback)
+                value = createObservable(value, callbacks)
             }
 
-            callback(key, target[key] = value)
+            callbacks.set(key, target[key] = value)
 
             return true
         }

+ 60 - 0
tests/global-store.spec.js

@@ -0,0 +1,60 @@
+import Alpine from 'alpinejs'
+import Spruce from '../dist/spruce'
+import { waitFor } from '@testing-library/dom'
+
+beforeEach(() => {
+    Spruce.subscribers = []
+})
+
+beforeAll(() => {
+    window.Spruce = Spruce
+})
+
+test('$store > is available as global object', async () => {
+    Spruce.config({ globalStoreVariable: true })
+
+    expect(Spruce.options.globalStoreVariable).toBeTruthy()
+
+    await Spruce.start()
+
+    expect(window.$store).toEqual(Spruce.stores)
+})
+
+test('$store > can be used inside of component without subscribing', async () => {
+    document.body.innerHTML = `
+        <div x-data>
+            <span x-text="$store.foo.bar"></span>
+        </div>
+    `
+
+    Spruce.store('foo', { bar: 'car' })
+
+    await Spruce.start()
+    Alpine.start()
+
+    await waitFor(() => {
+        expect(document.querySelector('span').innerText).toEqual('car')
+    })
+})
+
+test('$store > modifying store value will trigger component re-render', async () => {
+    document.body.innerHTML = `
+        <div x-data>
+            <span x-text="$store.foo.bar"></span>
+            <button @click="$store.foo.bar = 'boo'"></button>
+        </div>
+    `
+
+    Spruce.store('foo', { bar: 'car' })
+
+    await Spruce.start()
+    Alpine.start()
+
+    expect(document.querySelector('span').innerText).toEqual('car')
+
+    document.querySelector('button').click()
+
+    await waitFor(() => {
+        expect(document.querySelector('span').innerText).toEqual('boo')
+    })
+})

+ 6 - 4
tests/observable.spec.js

@@ -5,7 +5,7 @@ test('createObservable > successfully wraps object', () => {
         foo: 'bar'
     }
 
-    let observable = createObservable(target, () => {})
+    let observable = createObservable(target, { get: () => {} })
 
     expect(observable.foo).toEqual('bar')
 })
@@ -19,7 +19,7 @@ test('createObservable > can access deeply nested props', () => {
         }
     }
 
-    let observable = createObservable(target, () => {})
+    let observable = createObservable(target, { get: () => {} })
 
     expect(observable.foo.bar.baz).toEqual('bob')
 })
@@ -31,8 +31,10 @@ test('createObservable > will run callback on set trap', () => {
 
     let fixture = 0
 
-    let observable = createObservable(target, () => {
-        fixture = 100
+    let observable = createObservable(target, {
+        set: () => {
+            fixture = 100
+        }
     })
 
     observable.foo = 'bob'

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä