Closed 7ammer closed 4 years ago
I guess I'm trying to follow this Vuejs pattern "Simple-State-Management-from-Scratch": https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
Yeah, that pattern does not work in Alpine.
I imagine Vue3 won't support that pattern (because of the switch to a Proxy-based reactivity core like Alpine).
Consider this example:
<script> const state = { count: 0 } </script>
<button x-data="{ shared: state }" @click="shared.count++" x-text="shared.count"></button>
<button x-data="{ shared: state }" @click="shared.count++" x-text="shared.count"></button>
Clicking one button doesn't change the other, but if you click the first one 3 times, then click the next button, the number will change from 0 to 4 instantly.
What's happening here is that the state IS actually shared, just mutations from one component don't trigger a refresh in the other.
There may be some simple way to enable this behavior.
Open to ideas.
Thanks!
Open to ideas.
I'm deep into the "I should probably not comment on stuff I don't know too much about" land here, but:
<script> const state = { count: 0 } </script>
<button x-data="{ shared: state }" @click="shared.count++" x-text="shared.count"></button>
<button x-data="{ shared: state }" @click="shared.count++" x-text="shared.count"></button>
There may be some simple way to enable this behavior. Open to ideas.
I have a workaround like this : https://codepen.io/fergushaga/pen/MWwrgJQ
<div x-data="count()" x-trigger="inc-count">
<button x-text="shared.count" x-on:click="incCount()"></button>
</div>
<div x-data="count()" x-trigger="inc-count">
<button x-text="shared.count" x-on:click="incCount()"></button>
</div>
<script>
const count1 = new AlpineData('inc-count', 'setCount')
const state = { count: 0 }
function count(){
return{
shared: state,
setCount(){
this.shared = count1.get()
},
incCount(){
state.count++
count1.set(state)
}
}
}
</script>
Originally I built this helper class for my existing project that use bootstrap 4 modal. I want to pass data from outside x-data into the modal that has a x-for and trigger the reactivity everytime the modal opened.
I'm not sure this is the correct use case for Alpine. I believe the correct use case would be building the model component itself with Alpine and encapsulating all of the logic there. Again, Alpine is designed to be sprinkled into your existing markup that is JavaScript free, whereas Bootstrap is controlling the model JavaScript in your case.
@ryangjchandler yes I'm aware of that, but consider this case :
I have a table generated by server. Inside the table there is a link and open a modal containg the detail of clicked item by fetching data from server, process it, and render it. Even if I can build the modal myself, how can I achieve reactivity / re-render elements inside the modal everytime i click the link.
That's the original idea of my helper class. If there is a cleaner and preferred way to do this with alpine, I would love to implement it
In this case, you probably want 2 different alpine.js components.
1 for the link (with 1 instance per rendered line I guess)
1 for the modal
The link component can dispatch a custom event that the modal can listen for.
The custom event contains all the data the modal needs to render.
Would that work?
Unless I'm missing something, yes that would work if :
The data is already available before I click the link / before dispatching
$dispatch can await async data fetch
dispatching custom event can be programmatically triggered.
1 and 3 are true
1
Is true since you're inlining data in the template
3
Is true because Alpine.js provides $dispatch in both methods and the template
What's with 2? Can you fetch the async data and then trigger the event? Or trigger a "open" even then a "data-ready" event?
<div x-data="{a: 'a'}">
<div x-data="{b: 'b'}">
<p x-text="a">character</p>
</div>
</div>
As I know Alpine creates a component for every element that contains x-data attribute but Why I can not access parent component state from child component something like above code!!?
The data lives on the component instance in which it's set, the child component doesn't have the parent component in scope.
Can you fetch the async data and then trigger the event?
I can't, because like I said before, my data isn't ready. I need to fetch it from server. Is there a way to do that?
<a x-on:click="$dispatch('item-details', fetchData())">item</a>
This will return undefined
Right, in this case, you could actually do
<a x-on:click="fetchData().then((info) => $dispatch('item-details', info))">item</a>
I've got a PR prepared for some internal events that get fired too. For example, after the data has been loaded and observation has start, as well as events fire after data updates and mutations.
Right, in this case, you could actually do
<a x-on:click="fetchData().then((info) => $dispatch('item-details', info))">item</a>
yes, this is what I mean, I have to change a lot of things in my fetchData function to accommodate this.
It would be nice if $dispatch can await async, or I can execute dispatch from my fetchData. Just hoping tho, nevermind it. Actually alpine has made my life so much easier :)
I don't think dispatch being promise-aware is a core thing Alpine should do since there's a userland workaround.
I didn't see anyone here mentioning x-init
, i'm using it to capture the reactive object created by Alpine:
<div x-data="myData()" x-init="myInit()"></div>
var myObject;
function myData() {
return {
property: 'value'
};
}
function myInit() {
myObject = this; // here you get the reference and you can save it
}
@rhengles can you explain how you would use the oobject provided by x-init
to share the state?
@ockam - since the x-init
hook has access to the $data
variable that Alpine uses to proxy any changes to data, you could store it in another variable and modify it directly. JavaScript passes objects and variables by memory address / reference, so any changes affect the original point in memory.
@ryangjchandler thanks for the precision.
Here’s what I came up with (note that this example use a bundler):
import 'alpinejs'
/* modal management */
window.modalState
window.modal = function() {
return {
show: false,
isModalOpen() { return this.show === true },
openModal() {
this.show = true
const body = document.body
body.style.position = 'fixed'
body.style.top = 0
},
closeModal() {
const body = document.body
body.style.position = ''
this.show = false
},
modalInit() {
window.modalState = this
}
}
}
// used outside of modal scope to trigger it
window.remoteModal = function() {
return {
openModal() {
window.modalState.show = true
}
}
}
Yeah, does this work nicely? P.s. I'm working on a global state management layer for Alpine at the moment that weighs in at less than 1kb, my GitHub Sponsors can get early access. I'll make it OS once I've reached 10 subscribers ;)
Works perfectly.
I wonder if there’s a performance penalty related to the amount of content of your scope?
That could have been solved simply by putting my modal state higher up in the DOM, but I have a feeling this is not a good idea. Maybe I’m wrong...
Works perfectly.
I wonder if there’s a performance penalty related to the amount of content of your scope?
That could have been solved simply by putting my modal state higher up in the DOM, but I have a feeling this is not a good idea. Maybe I’m wrong...
Alpine walks the whole DOM below the element with x-data
on render so yeah there is a performance impact.
Current state of state management in Alpine, you can give a go with the answers here or @ryangjchandler 's Spruce, I also believe reactive stores might make it into Alpine v3.
Closing this since it's not going to be in scope for v2 and there's a workaround: use https://github.com/ryangjchandler/spruce
Feel free to reopen or start a discussion 😄
I've been playing with a composable architecture for handling global state and created a little PoC with Alpine. It's still early stages and I'm still getting my head around the best approach for structure and organization... But I got an MVP working. If you want to see the full thing check out this CodeSandbox, but the gist of it is:
const todos = state([]);
const addTodo = mutator(todos, (list, todo) => list.concat([todo]));
const completed = computed(todos, list =>
list.filter(todo => todo.completed)
);
window.$stores = initStores({
completed
});
On the Alpine side:
<div x-data="{}" x-store="completed" x-show="$completed.length > 0">
<h2>Complete</h2>
<ul>
<template x-for="todo in $completed">
<li x-text="todo.name" style="text-decoration: line-through"></li>
</template>
</ul>
</div>
@olavoasantos if you're interested, you should check out Spruce - it's a global state management library for Alpine. https://github.com/ryangjchandler/spruce
@ryangjchandler I did take a look! Great job.
I actually learn by doing stuff haha I made this little state lib which is framework agnostic. Now I'm playing around trying to integrate it with different frameworks to see how far I can take it.
@ryangjchandler I did take a look! Great job.
I actually learn by doing stuff haha I made this little state lib which is framework agnostic. Now I'm playing around trying to integrate it with different frameworks to see how far I can take it.
Nice work - I like the approach, very functional. My aim with Spruce was to make it almost like an Alpine component, where you can mix and match data properties and functions within the state objects, but your approach which is similar to VueX definitely works for some!
Things are now done with Alpine.store()
and $store
, I guess. :)
If I have some javascript like so (note the shared state object):
Can someone help explain why something like this works:
but this doesn't
Here is a working example: https://codepen.io/7ammer/pen/MWwOZVp
I've noticed that 'mySharedState' is shared but the second example is not reactive when the value changes.