-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Nested components cannot access external data #49
Comments
Hey @SimoTod, Great question. This is definitely something that should be on the radar. Inter-component communication is a common need. I suppose I've wanted to nail the core before adding this type of feature because it will increase the complexity of the project and therefore the issues, etc... I would love to keep this conversation going though and start discussing potential APIs for this. Here are a couple routes we could go off the top of my head:
or something like that
C) Accessing data through simply passing down scope like you mentioned:
Let's keep the conversation going. Thanks! |
Good points @calebporzio. Option A feels similar to the approach React and other frameworks take but, if I didn't have any experience, it would be an additional learning friction. It also forces a dev to pass down the variables if there are multiple nested levels, polluting the DOM. It probably makes easier to implement the "reactivity" part, though. Option C feels more natural to me and it kinda match the scope rules in javascript but we wouldn't be able to use variables from the parent scope if a variable in the current scope has the same name. Option B requires dev to know about the parent rule, I feel it can be easily forgotten, leading to bugs. My preference, without any tech consideration, would be C + support for the B syntax to access the parent scopes when names clash. What are your thoughts? |
Leaving a couple of considerations for when you get back online: |
+1 on |
Also, another issue is referring to the same thing: #21 |
Hi. I'm the author of the last comment in #21. My use case is here: https://codepen.io/magicroundabout/pen/KKwXbME Amazing to see this progressing. Great work, @SimoTod I confess my JS isn't great and I've not looked at the Alpine source, so I'm (currently) unaware of the technical challenges of doing this. I'm curious about the While reading the Vue docs linked above I realised that using So your example would be:
And my button example would be something like:
This is starting to get a bit verbose. But still better than writing handlers in vanilla Javascript. |
Hi @rosswintle Thanks for your feedback. This is the way I implemented it: <div x-data="{ foo: 'bar' }">
<div> <!-- this is not an alpine component, so $parent will ignore it-->
<div x-data="{}">
<span x-text="$parent.foo"></span>
</div>
</div>
</div> Each component, unless it's the root component, will have another <div x-data="{ foo: 'bar' }">
<div x-data="{}">
<div x-data="{}">
<span x-text="$parent.$parent.foo"></span>
</div>
</div>
</div> I don't think This is how your code would look like if the PR goes through: https://codepen.io/SimoTod/pen/jOEZpKy?editors=1111 I initially tried to implement the "magic" inheritance.
<div x-data="{ foo: 'bar' }">
<div x-data="{ foo: 'bar' }">
<span x-text="foo"></span>
<!-- I can't access the parent foo -->
</div>
<div>
<div x-data="{ foo: 'bar' }">
<div x-data="">
<button x-on:click="bob = 'baz'"></button> <!-- This sets the property on the child scope -->
<button x-on:click="foo = 'bar'"></button> <!-- The behaviour of this needs to be defined and it will be ambigous. It could either set the property on the child scope (but if another element on the parent level was using the same property, they will show different values from this point on) or it could set the property on the first valid parent scope (but it would be inconsistent with the other setter) -->
</div>
<div> For these reasons, I think '$parent' would be a nice compromise but I'm open to try other options. |
First of all, I think you're a blimmin genius, and a very generous and gracious one at that too! 👏
Nice. This is probably flexible enough for me.
Ah right. I missed that vital part of the Vue docs that I referenced. I assumed $refs was some global thing and you could refer to anything throughout the whole DOM. But "you can assign a reference ID to the child component using the ref attribute"
Sure. It's hard for an end-user (me) to discuss the possibilities without fully understanding the internals. Thanks for bearing with me!
For your example 1: This is fine. This is how I expect "scope" to work. Local scope overrides global. I think this is how I would expect it to work. If you want access to the parent you need to name things better! For your example 2: I'm actually surprised that you can assign a NEW variable/property in the click handler. My mental model of this (which, admittedly, is clearly incorrect) is that you have to declare the variables/properties in the Summary:
ALSO: Documentation is key here. Caleb's use of "component scope" made me think of regular programming scopes (as I clearly don't yet have a decent understanding of "components"). I think that whatever we do the documentation needs to be clarified to document the limitations. @calebporzio Are you happy for me to suggest some docs clarifications? |
Hi @rosswintle, I agreed with you on point 1. It's probably just me being overzealous. About point 2, I can see where you're coming from. You can create a new variable in a click handler: If you try to use a variable that does not exists, you won't get any errors and if you inspect the DOM, you can see that it actually creates a new variable in the current scope. So, in your mental model, anything in the x-data will act as a let declaration and the rest will be a normal assignment. For example <div x-data="{'foo': 'bar'}">
<div x-data="{'foo': 'baz'}">
<span x-text="foo"></span>
</div>
<span x-text="foo"></span>
</div> Would translate to {
let foo = 'bar';
{
let foo = 'baz';
console.log(foo); <!-- baz -->
}
console.log(foo); <!-- bar -->
} <div x-data="{'foo': 'bar'}">
<div x-data="{}">
<span x-text="foo"></span> <!-- bar -->
</div>
<span x-text="foo"></span> <!-- bar -->
</div> Would translate to {
let foo = 'bar';
{
console.log(foo); //bar
}
console.log(foo); //bar
} <div x-data="{'foo': 'bar'}">
<div x-data="{}">
<button x-on:click="foo = ''baz"></span>
<span x-text="foo"></span> <!-- baz -->
</div>
<span x-text="foo"></span> <!-- baz -->
</div> Would translate to {
let foo = 'bar';
{
foo = 'baz';
console.log(foo); //baz
}
console.log(foo); //baz
} Is that correct? The issue is that <div x-data="{}">
<div x-data="{}">
<button x-on:click="foo = ''bob"></span>
<span x-text="foo"></span> <!-- bob -->
</div>
<span x-text="foo"></span> <!-- error undefined variable -->
</div> Would not translate to {
{
foo = 'bob';
console.log(foo); //bob
}
console.log(foo); //bob
} since it that case javascript declares a variable in the global scope rather than the nested one. If we want to keep it consistent, we could declare the new variable in the root scope but it will always be an opinionated decision while explicitly using $parent would remove ambiguities. Probably it's more @calebporzio 's call since it's the author of the framework. I can update the PR to implement the other behaviour if we feel it would be better. 👍 |
Yes. I think you nailed it. I'd probably sum up my position as:
I'd leave your I guess implementing It was AWESOME working this through with you - I really hope you get something merged in! 👏 |
There's one use case that's not covered by <div x-data={ foo: 'bar' }>
<div x-data="{ isOpen: false }" class="collapse" x-bind:class="{ 'is-open': isOpen }">
<span x-text="$parent.foo"></span>
</div>
</div> On first glance it may seem that In Vue this is tackled via slots and scoped slots:
In Stimulus this is solved by controller namespacing: <div data-controller="outer_controller">
<div data-controller="inner_controller">
<button data-action="outer_controller#click()">...</button>
<button data-action="inner_controller#click()">...</button>
<div data-target="outer_controller.someTarget">
...
</div>
</div>
</div> Something close to Stimulus's approach would be optimal, IMO. Along the lines of: <div x-data.outer={ foo: 'bar' }>
<div x-data.inner="{ isOpen: false }" class="collapse" x-bind:class="{ 'is-open': isOpen }">
<span x-text="outer.foo"></span>
<span x-text="inner.isOpen"></span>
</div>
</div> |
Thanks for your feedback @panda-madness Do you mean wrapping the span tag with another tag with a x-data attribute (new scope)? The stymulus approach seems quite verbose. What does it happen if you have 3-4 levels? Do you need to pick a name for each of those scopes? Is it the fact that you need to update the alpine attributes in case you change the scopes that you don't like? In that case, maybe the approach without '$parent' would work better. If I didn't understand correctly, could you pleased expand a bit more with other exemples where it would be broken? If you have another PR that would fit better, feel free to submit it. I'm not precious about this PR, I just would like this feature to be available. :) |
Yes, that's exactly what I mean. Ideally one would want to extract simple things like collapses, dropdowns and whatnot into reusable components. A laravel example: // components/dropdown.blade.php
<div x-data={ isOpen: false }>
<button @click="isOpen = !isOpen">Toggle</button>
<div x-show="isOpen">
{{ $slot }}
</div>
</div>
// somewhere else
<div x-data="{ foo: 'bar' }">
@component('components.dropdown')
<div x-text="foo"></div>
@endcomponent
</div> However this is not practical if
Yes, you would need to pick a name for each scope. Personally I don't find it very verbose, but that's a matter of preference. In any case the flexibility it opens up is worth it, I think. |
The decoupling thing is surely a valid point. The current way Alpine builds scopes makes a bit harder to support named scopes (I assume that the name bit would be optional and people that doesn't need it could use the standard I'm still leaning a bit towards a "magic" approach where the inner scope can access the parent scope simply by using the name of the variable in the parent scope without any prefix but I appreciate that we would control on which of the parent scope the variable comes from, especially using reusable component like in laravel. |
I tried a different approach and I have another version supporting access to the parent scope without any prefix. For example <div x-data="{foo: 'bar'}">
<div x-data="{}">
<span x-text="foo"></span> <!-- this will print bar -->
</div>
</div> It would partially fix your problem since it won't depend on the number of scopes between your span tag and the component defining An alternative approach would be to keep the $parent implementation and, like Vue does, to add a $root magic property to access the root scope. Your example would then use Any thoughts? Both approaches can be improved adding something like named scopes later on. |
@SimoTod in practice this is effectively the same as named scopes, since I could scope it like so: <div x-data="{ outer_scope: { foo: 'bar' } }">
<div x-data="{ inner_scope: { foo: 'baz' } }">
<span x-text="outer_scope.foo"></span> <!-- this will print bar -->
</div>
</div> I would be satisfied with this solution. |
@panda-madness I've updated those 2 pens to demo it: Here's the code: https://github.com/alpinejs/alpine/compare/master...SimoTod:feature/parent-scope-access?expand=1 I haven't sent a PR yet since I don't know if it's the direction @calebporzio wants to take and maybe there will be further feedback. |
One thought on the Interested to hear any ideas on how we could do this given the current architecture. |
Yeah, it requires to update all the children at the minute because a parent component doesn't know which children use the updated param. Not sure, maybe a child could keep track of the parent params it uses and it could ignore a refresh call if nothing relevant has changed but it's not trivial to implement, especially if there are intermediate components. I can't think of a smarter solution right now. The "props-like" approach would mitigate this issue and make the list of params to track easier to implement but with a big (IMO) trade-off: it will lead to a really verbose HTML when multiple levels are involved and, for example, it would break the example posted earlier in this thread. // components/dropdown.blade.php
<div x-data={ isOpen: false }>
<button @click="isOpen = !isOpen">Toggle</button>
<div x-show="isOpen">
{{ $slot }}
</div>
</div>
// somewhere else
<div x-data="{ foo: 'bar' }">
@component('components.dropdown')
<div x-text="foo"></div>
@endcomponent
</div>``` |
I'm relatively new to this so please bear that in mind. I like the idea of nested scopes (Option C above) - if feels familiar and natural - but wouldn't there need to be some sort of barrier in the html which prevents further searches up the x-data/scope chain. I worry that without the barrier, it would be easy to create hard-to-find bugs. For example, I create an x-data component in a partial in my server-side template engine and I accidentally include a reference to an undefined variable. If I use the partial in several places, it may work as expected or may not, depending on the context in which it is used. With a barrier, we see the error every time. Taking @SimoTod's example above: <div x-data="{}">
<div x-data="{}">
<button x-on:click="foo = ''bob"></span>
<span x-text="foo"></span> <!-- bob -->
</div>
<span x-text="foo"></span> <!-- May or may not work depending on surrounding scope-->
</div> would become: <div x-barrier >
<div x-data="{}">
<div x-data="{}">
<button x-on:click="foo = ''bob"></span>
<span x-text="foo"></span> <!-- bob -->
</div>
<span x-text="foo"></span> <!-- error undefined variable -->
</div>
</div> and would reliably log an error as the scope search stops at the x-barrier. (BTW I don't like the name x-barrier - it's just for illustration). I don't know if this is feasible or not in the Alpine codebase, it just feels like a more comfortable api. |
Just an FYI for those following, this is discussed towards the end of the Full Stack Radio podcast episode where Caleb discusses Alpine. Worth a listen: http://www.fullstackradio.com/132 Summary:
Worth a listen if you're interested in this issue. |
Just listened to the podcast. I feel that the magic inheritance would make a big difference, it feels more natural. I'm glad Adam voted for it. :) It's fully working and performance doesn't seem bad (I didn't have a proper benchmark though). |
I also came here from the podcast (and I really like what I've seen so far). If you want my 50 cents on this (very important issue): I think one clue lives in "component composability" without each component having to know where it lives in the component tree. I'm having a hard time wrapping my head around constructs like So for me, that leaves A (prop system) and C (passing down scope). I love the simplicity of C, and I assume it should be possible if needed (in a future version) to somehow restrict the scope by some contract in the child component (aka props): |
I really like this idea of having a contract for child component(making things a lot more explicit). Basically C but with some of the explicitness of A. Remind me of closure definition in Rust and other languages: This would also solve the hard to trace error that @stuartpullinger raised |
@calebporzio Where are we with this one? Would you need any help making it happen? What about @SimoTod's solution? I have a for loop with components inside that have an open/close state so I need to set the x-data property for all of them individually while having access to the parent scope. I really need this so I'm willing to help. |
C) would effectively dump everything into a single global namespace. It’s really not suitable for composing components. Suppose I have multiple, nested AJAX widgets. I can’t reuse obvious names like I think a Vue-like solution would be much better than creating a single global namespace. |
C would created nested namespaces in reality. The local would also have precedence so if 2 components use the same variable name, you won't be able to access the parent one. |
One advantage of explicit x-props (A) is that a child cannot modify the parent's x-data, making for a top-down data flow (events/callbacks up, data down). I wouldn't want a child component that I include somewhere down the tree changing my x-data. Also it wouldn't be clear who "owns" the data/what is the source of truth -- if a child changes x-data, it might not expect the data to revert to parent's value on next render. Another plus is that a component won't behave differently based on where you place it because it would start seeing different ancestors' scopes. |
That point convinced me... |
Many devs would probably expect to be able to change the parent data from the children, though (a sort of shared state). |
@SimoTod yep, I'm coming from a React background where props are read-only. But you can use them to initialize state. What devs expect is an interesting discussion to have, maybe it can be done using some simple examples: Example 1:
Here I think most would expect that the child owns its own foo and child changing foo doesn't change parent's and vice versa. Example 2, if we don't use x-props but instead evaluate x-data in parent's context:
I'd expect that the child still owns its own foo, but it's not clear what happens when parent's foo changes - does it overwrite the child's, losing any changes? Or does child foo only get initialized with parent foo once, and if so, how does the child ever see the latest/greatest foo from parent? Example 3, using option A (tentative implementation in my PR).
x-data can be initialized with props, but is still owned/visible only to the component. x-props are always latest evaluated in parent's context. Example 4: the child can just see the parent's (or all ancestor's) foo (option C) - imo, should not be allowed:
Here, what the child sees depends on where you place it. It's not clear what happens when the child changes foo: does it modify its local copy, and if so does it get overwritten later by the parent? Or does it modify the parent's, which IMO is even worse since including third-party components as children could modify your own component's data. |
@SimoTod forgot to mention that my PR borrowed some docs, tests and examples from your $parent PR so thanks :) |
In my head, option C would modify the parent. Whether it's right or not, it's part of this discussion. The prop system in react is also dictated by performance and complexity issue. |
Btw, if I remember correctly, Caleb liked the prop solution at the very beginning. |
some more brainstorming on option C, using React's Context idea to enable children components to see data without explicit prop passing.
|
It doesn't read as nice as the other options. Hahaha, I'm never happy. I think i can cope with a prop system, it would be good if I could have "transparent" components though. <div x-data="{foo: 'bar'}">
<div x-data="{open: false}" x-iamaghost>
<div x-data="{bar: $props.foo}" x-props="{foo: foo}">
...
<\div>
<\div>
<\div> |
Today was my first time using alpine.js and I immediately came across this issue. My gut reaction was it should work like option C with any disambiguation handled in the manner suggested by @panda-madness i.e.
Just my two cents worth as someone learning the framework with no preconceptions. |
Just gonna throw a car on the idea train. The similarity to React Contexts @nyura123 mentioned sits well with me, since it's attempting to solve the prop drilling that's inevitable with any I'd suggest a different syntax, though, which would follow the <div x-data="{foo: 'bar'}" x-context="fooStore">
<div x-data="{open: false}">
<div x-data="{bar: 'baz'}">
<span x-text={bar}></span> <!-- "baz" -->
<span x-text={open}></span> <!-- undefined -->
<span x-text={$contexts.fooStore.foo}></span> <!-- "bar" -->
<\div>
<\div>
<\div> One tradeoff I see with ☝️ is the implicit access all descendants have to any context above. That may be a win for simple cases; but if the goal is to get to reusable agnostic components, it feels dangerous to allow any descendant access to everything above it (especially if descendants can mutate values). The way React avoids this is that components have to explicitly "consume" a context. I think to Alpine, that syntax would probably look something like: <div x-data="{foo: 'bar'}" x-context:provider="fooStore">
<div x-data="{open: false}">
<div x-data="{bar: 'baz'}" x-context:consumer="fooStore"> <!-- maybe support arrays -->
<span x-text={bar}></span> <!-- "baz" -->
<span x-text={open}></span> <!-- undefined -->
<span x-text={$contexts.fooStore.foo}></span> <!-- "bar" -->
<\div>
<div x-data="{bar: 'baz'}">
<span x-text={bar}></span> <!-- "baz" -->
<span x-text={open}></span> <!-- undefined -->
<span x-text={$contexts.fooStore.foo}></span> <!-- undefined -->
<\div>
<\div>
<\div> The consumption pattern definitely protects a some 3rd party descendant from accidentally, or maliciously, manipulating generically named stores. Just a thought. Also, apologies if my syntax suggestion is way off base; I've only been experimenting with Alpine for a week or so. Loving it so far, though! Thanks for all the work! |
Hi @torshakm At the moment, communication between components follows a publisher / subscriber approach where a component dispatches an event and another component sets a listener. Example 1 (separate components) <div x-data="{val: 'value'}" @myevent.window="val = $event.detail.newvalue">
<span x-text="val"></span>
</div>
<div x-data="{something: 'somethingelse'}">
<a @click="$dispatch('myevent', {newvalue: 'value2'})">Click me</a>
</div> Since these 2 components are independent and events only travels up the DOM, they need to communicate through the global scope so listeners need to use the window modifier in order to work. Example 2 (nested components) <div x-data="{val: 'value'}" @myevent="val = $event.detail.newvalue">
<span x-text="val"></span>
<div x-data="{something: 'somethingelse'}">
<a @click="$dispatch('myevent', {newvalue: 'value2'})">Click me</a>
</div>
</div> In this case, evente will naturally bubbles up to the parent component so it will work without using the Generic considerations About tabs <div class="tablist" x-data="{selected: 'one'}">
<!-- Your tab selectors -->
<a @click="selected = 'one'" :class="selected == 'one' ? 'selected' : ''">Tab one</a>
<a @click="selected = 'two'" :class="selected == 'two' ? 'selected' : ''">Tab two</a>
<!-- First tab. Note the nested component is inside the tab element -->
<div class="tab" x-show="selected == 'one'">
<div x-data="{something: 'something'}">
<span x-text="something" ></span>
</div>
</div>
<!-- Second tab. Note the nested component is inside the tab element -->
<div class="tab" x-show="selected == 'two'">
<div x-data="{something: 'somethingelse'}">
<span x-text="something"></span>
</div>
</div>
</div> I hope it helps. |
Closing for now since it's not in scope for v2. |
Not sure if it fits here but I think I need something similar. I have a "modal component" (via Laravel components) that I want to have its "own scope"
What should I do? Thanks. |
Change |
@carlmjohnson That's not the point. "openModal" is a "global" function and maybe it's not the best example to explain what I want. This won't work neither (I guess it is in the comments too)
|
Lots of suggested solutions but this doesn't seem to have a solid solution and is closed? What's the status of this? In development? Shelved? |
See #49 (comment) |
Thanks @SimoTod I never knew about Spruce, super handy! I figured it'll be addressed in V3. Thanks. |
The idea is that Alpine V3 might provide global stores (which you currently would have to use Spruce for) that's all subject to change though |
What if I want to pass data in x-for to child component?
Is there any solution for this? |
I found the solution )) https://twitter.com/hugo__df/status/1310611867954556929?utm_source=alpinejs&utm_medium=email However, I want to know if this is the best one for now? |
@ponyjackal Yes that's a fine solution. Or otherwise there are some helpers here you can use ( https://github.com/alpine-collective/alpine-magic-helpers And if you want something more to store and access all your data, try Spruce: |
I tried to use $parent, but $parent is undefined |
You have to include the helper's script on your page. https://cdn.jsdelivr.net/gh/alpine-collective/alpine-magic-helpers@0.5.x/dist/component.min.js Here's a demo: https://codepen.io/KevinBatdorf/pen/ZEpMeJJ Note that I used |
I solved it like this:
Based on @KevinBatdorf 's solution |
Hi @calebporzio,
thanks for the amazing work so far. I was having a go with alpinejs and I noticed that, when there are nested components, the internal component cannot access the scope of the external one.
For example,
I would expect
span#s2
to display 'BAR' or, in alternative, i would expect to be able to reference foo2 in the internal data structure.I'm happy to work on a PR for this but I just wanted to check with you first in case this behaviour is expected and you do not want components to access external scopes.
Thanks,
Simone
The text was updated successfully, but these errors were encountered: