Skip to content
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

Closed
SimoTod opened this issue Jan 7, 2020 · 63 comments
Closed

Nested components cannot access external data #49

SimoTod opened this issue Jan 7, 2020 · 63 comments

Comments

@SimoTod
Copy link
Collaborator

SimoTod commented Jan 7, 2020

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,

<html>
  <head>
    <script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v1.2.0/dist/alpine.js" defer></script>
  </head>
  <body>
    <div x-data="{ foo: 'bar', foo2: 'BAR' }">
      <span x-text="foo"></span>
      <div x-data="{ foo: 'bob' }">
        <span id="s1" x-text="foo"></span>
        <span id="s2" x-text="foo2"></span>
      </div>
    </div>
  </body>
</html>

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

@calebporzio
Copy link
Collaborator

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:
A) A "prop" system

<div x-data="{ foo: 'bar' }">
  <div x-data="{}" x-props="{ baz: foo}"></div>
</div>

or something like that
B) Accessing the parent scope from a magic $parent object or something:

<div x-data="{ foo: 'bar' }">
  <div x-data="{}">
    <span x-text="$parent.foo"></span>
  </div>
</div>

C) Accessing data through simply passing down scope like you mentioned:

<div x-data="{ foo: 'bar' }">
  <div x-data="{}">
    <span x-text="foo"></span>
  </div>
</div>

Let's keep the conversation going. Thanks!

@SimoTod
Copy link
Collaborator Author

SimoTod commented Jan 7, 2020

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?

@SimoTod
Copy link
Collaborator Author

SimoTod commented Jan 7, 2020

Leaving a couple of considerations for when you get back online:
I've built a PoC and option C is a bit of a pain when updating a scope property from the child component: it could either create the property in the child scope or climb the hierarchy, updating the property when it finds a match but creating the property in the child scope if it does not find the property.
None of these solution are really appealing and easy to understand.
The $parent solution at this point seems more solid and would remove ambiguities.

@thormeier
Copy link
Contributor

thormeier commented Jan 7, 2020

+1 on $parent, Vue is actually offering the same here, so devs that already know Vue likely know the concept: https://vuejs.org/v2/guide/components-edge-cases.html#Accessing-the-Parent-Component-Instance

@calebporzio
Copy link
Collaborator

Also, another issue is referring to the same thing: #21

@rosswintle
Copy link

rosswintle commented Jan 8, 2020

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 $parent though, would this only work to get data defined on the immediate parent element in the DOM? This feels quite limiting. I might want to reference something further up in the DOM than the parent? Did I understand this right?

While reading the Vue docs linked above I realised that using $refs might be a good solution and maybe not too hard to implement?

So your example would be:

<div x-ref="container" x-data="{ foo: 'bar' }">
  <div x-data="{}">
    <span x-text="$refs.container.foo"></span>
  </div>
</div>

And my button example would be something like:

<div x-ref="buttons" x-data="{ selectedButton: 1 }">
  <button
          x-data="{ buttonId: 1 }"
          x-bind:class="{ selected: buttonId == $refs.buttons.selectedButton }"
          x-on:click.prevent="$refs.buttons.selectedButton = buttonId">
    Button <span x-text="buttonId"></span>
  </button>
  <button
          x-data="{ buttonId: 2 }"
          x-bind:class="{ selected: buttonId == $refs.buttons.selectedButton }"
          x-on:click.prevent="$refs.buttons.selectedButton = buttonId">
    Button <span x-text="buttonId"></span>
  </button>
  
</div>

This is starting to get a bit verbose. But still better than writing handlers in vanilla Javascript.

@SimoTod
Copy link
Collaborator Author

SimoTod commented Jan 8, 2020

Hi @rosswintle

Thanks for your feedback.

This is the way I implemented it: $parent refers the parent alpine component, not to the parent DOM item.

<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 $parent property to access the 'grandparent' scope and so on.

<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 $refs would work because as far as I know you can only refer a child item.
A component don't have visibility of a reference defined on a parent DOM element unless we switch paradigm and we store a global list of refs somewhere but it doesn't seem the right direction to take.

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.
It's technically possible but we need to deal with:

  1. Naming conflicts
<div x-data="{ foo: 'bar' }">
    <div x-data="{ foo: 'bar' }">
        <span x-text="foo"></span>
        <!-- I can't access the parent foo -->
    </div>
<div>
  1. Set behaviour
<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.

@rosswintle
Copy link

First of all, I think you're a blimmin genius, and a very generous and gracious one at that too! 👏

$parent refers the parent alpine component, not to the parent DOM item.

Nice. This is probably flexible enough for me.

I don't think $refs would work because as far as I know you can only refer a child item.

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"

A component don't have visibility of a reference defined on a parent DOM element unless we switch paradigm and we store a global list of refs somewhere but it doesn't seem the right direction to take.

Sure. It's hard for an end-user (me) to discuss the possibilities without fully understanding the internals. Thanks for bearing with me!

["magic" inheritance is] technically possible but we need to deal with...

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 x-data. But if not then usual programming rules would dictate that this is set in the innermost scope?

Summary:

  • I'm happy with $parent for now - as long as it's well documented (happy to help with this part!)
  • I can see that $refs doesn't work - thanks!
  • I can see that a bigger shift to something "magic" would be cool.
  • I (personally) think that regular programming scoping rules answer your questions about conflicts. But I could be wrong.

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?

@SimoTod
Copy link
Collaborator Author

SimoTod commented Jan 8, 2020

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. 👍

@rosswintle
Copy link

Yes. I think you nailed it.

I'd probably sum up my position as:

  • I'd like to be able to access data from a containing scope
  • I don't really mind that much how it's achieved (your $parent is sufficient for now!) ...
  • ...AS LONG AS it's well documented (which I'm sure it will be and which I'm super happy to help with).

I'd leave your $parent for now until we have a decision from the boss.

I guess implementing $parent now could affect any future implementation of scoping "magic". So yeah, we'll have to see if @calebporzio takes an interest in this idea and makes a call on it.

It was AWESOME working this through with you - I really hope you get something merged in! 👏

@panda-madness
Copy link

panda-madness commented Jan 10, 2020

There's one use case that's not covered by $parent. Imagine a simple Collapse component that toggles the visibility of it's child:

<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 $parent covers this use case, but what ends up happening is that the outer component's functionality is tightly coupled to it's HTML structure. If we ever decide that we would like to nest something inside the Collapse component this approach breaks.

In Vue this is tackled via slots and scoped slots:

<outer-component>
    <accordion>
        {{ some_data_of_outer-component }}
    </accordion>
</outer-component>

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>

@SimoTod
Copy link
Collaborator Author

SimoTod commented Jan 10, 2020

Thanks for your feedback @panda-madness

Do you mean wrapping the span tag with another tag with a x-data attribute (new scope)?
Yeah, in that case the span property needs updating (it will change to $parent.$parent.foo).

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. :)

@panda-madness
Copy link

panda-madness commented Jan 10, 2020

@SimoTod

Do you mean wrapping the span tag with another tag with a x-data attribute (new scope)?
Yeah, in that case the span property needs updating (it will change to $parent.$parent.foo).

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 $parent is the only available mechanism for component communication. I want to point out that I didn't say that your PR is unneeded, I'm saying that it doesn't cover a pretty big number of use cases. In Vue $parent is also provided as an escape hatch, for use in very tightly coupled components that don't make sense without each other (e.g. carousel and carousel-item).

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?

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.

@SimoTod
Copy link
Collaborator Author

SimoTod commented Jan 10, 2020

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 x-data="" syntax). Also, we need to check if Caleb would be happy to move towards that direction because it's the kind of thing that will be really hard to change later on.

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.

@SimoTod
Copy link
Collaborator Author

SimoTod commented Jan 10, 2020

@panda-madness

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 foo. I say partially because if your Laravel component contains a scope defining a variable with the same name, the inner label will use that one.

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 <span x-text="$root.foo"></span>.

Any thoughts?

Both approaches can be improved adding something like named scopes later on.

@panda-madness
Copy link

@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.

@SimoTod
Copy link
Collaborator Author

SimoTod commented Jan 11, 2020

@panda-madness
I've created a new branch for the "direct access" solution and it seems to be as good as the previous one.

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.

@calebporzio
Copy link
Collaborator

One thought on the $parent syntax. Making child components reactive to changes in $parent data will add lots of overhead to the codebase. I would want to weigh out pros-cons carefully.

Interested to hear any ideas on how we could do this given the current architecture.

@SimoTod
Copy link
Collaborator Author

SimoTod commented Jan 13, 2020

Yeah, it requires to update all the children at the minute because a parent component doesn't know which children use the updated param.
It was meant to use the concerned data array so it would have updated only the relevant tags but now that the array has been dropped, so the only immediate way is to keep the reactivity is to refresh each child.

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>```

@stuartpullinger
Copy link

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.

@rosswintle
Copy link

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:

  • Caleb wants to do this!
  • He's working out the best way (Yay! Go Caleb!)
  • Adam's first reaction was that child components should magically inherit the parent scope. (Yay!)
  • I think they propose a relatively simple solution. But we'll have to wait and see.

Worth a listen if you're interested in this issue.

@SimoTod
Copy link
Collaborator Author

SimoTod commented Jan 17, 2020

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 won't get merged but I've updated my branch (https://github.com/alpinejs/alpine/compare/master...SimoTod:feature/parent-scope-access?expand=1) to work with the latest structure since I think it could be, at least, a source of inspiration regarding a possible approach. It also contains a few improvements, thanks to the new structure, so I don't need to allocate a children array and trigger the refresh manually any more.

It's fully working and performance doesn't seem bad (I didn't have a proper benchmark though).

Codepen: https://codepen.io/SimoTod/pen/PowyjQj

@bep
Copy link
Contributor

bep commented Jan 21, 2020

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 $parent.$parent.$parent.foo in that sense.

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):

@dephiros
Copy link

dephiros commented Mar 20, 2020

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: |val| val + x where val is defined to be "captured" from the outer scope.

This would also solve the hard to trace error that @stuartpullinger raised

@fredcarle
Copy link

@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.

@earthboundkid
Copy link
Contributor

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 isLoading and hasLoaded or data because an inner component might accidentally get a value from an outer component.

I think a Vue-like solution would be much better than creating a single global namespace.

@SimoTod
Copy link
Collaborator Author

SimoTod commented Apr 26, 2020

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.

@nyura123
Copy link

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.

@bep
Copy link
Contributor

bep commented Apr 30, 2020

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...

@SimoTod
Copy link
Collaborator Author

SimoTod commented Apr 30, 2020

I wouldn't want a child component that I include somewhere down the tree changing my x-data

Many devs would probably expect to be able to change the parent data from the children, though (a sort of shared state).
Do you image x-prop to be read only?

@nyura123
Copy link

@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:

<div x-data="{foo: 'bar'}">
      <div x-data="{foo:  'bar'}" />
</div>

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:

<div x-data="{foo: 'bar'}">
      <div x-data="{foo:  foo}" />
</div>

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).

<div x-data="{foo: 'bar'}">
      <div x-data="{foo: $props.foo}" x-props="{foo: foo}">
          Initial foo: <span x-text="foo" />
          Latest foo: <span x-text="$props.foo" />
       </div>
</div>

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:

<div x-data="{foo: 'bar'}">
       <div x-data="{}">
           <span x-text="foo"/>
        </div>
</div>

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.

@nyura123
Copy link

@SimoTod forgot to mention that my PR borrowed some docs, tests and examples from your $parent PR so thanks :)

@SimoTod
Copy link
Collaborator Author

SimoTod commented Apr 30, 2020

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.
The concept of third party components is interesting, at the moment it doesn't exist (I know people use InnerHTML a lot but I think it's a really bad practice, just my personal tastes) but I assume we'll have it something like reusable components at some points.
Food for thoughts 👍

@SimoTod
Copy link
Collaborator Author

SimoTod commented Apr 30, 2020

Btw, if I remember correctly, Caleb liked the prop solution at the very beginning.

@nyura123
Copy link

some more brainstorming on option C, using React's Context idea to enable children components to see data without explicit prop passing.
x-data would remain private/local, but anything in x-context would be visible to all children components. Nested contexts would overwrite parent ones. Contexts would also be namespaced to prevent name collisions between different contexts

<div x-data="{}" x-context:ctx1="{value: 1}">
   value 1: <span x-text="$context:ctx1.value" />
   <div x-data="{}">
      <div x-data="{}">
         value 1 visible through several nesting levels: <span x-text="$context:ctx1.value" />
      </div>
    </div>
   <div x-data="{}" x-context="{value: $context:ctx1.value+1}">
      value 2: <span x-text="$context:ctx1.value" /></div>
     <div x-data="{}">value 2: <span x-text="$context:ctx1.value" /></div>
    </div>
</div>

@SimoTod
Copy link
Collaborator Author

SimoTod commented Apr 30, 2020

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>

@zeroid
Copy link

zeroid commented May 4, 2020

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.

<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>

Just my two cents worth as someone learning the framework with no preconceptions.

@dsongman
Copy link

dsongman commented May 8, 2020

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 props approach.

I'd suggest a different syntax, though, which would follow the $ref pattern Alpine already uses. Contexts would name themselves with x-context and there would be a new magic property called $contexts that would allow access to any named context in the node's ancestry.

<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!

@SimoTod
Copy link
Collaborator Author

SimoTod commented May 9, 2020

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 window modifier.

Generic considerations
myevent is the name of the event and you can use any name you think it's appropriate, the important part is that you need to use the same name in your click handler.
For example, $dispatch('foobar') and @foobar.window.
The second argument of $dispatch is an object that you can retrieve via the detail property of the $event object in your listeners so it can carry any message you want to pass between components

About tabs
In regards of your specific request, I believe It can be designed in a different way.
Your alpine component is, logically speaking, a tab group, not a single tab.
A tab by itself doesn't have any interactivity and only makes sense along other tabs.
Inside a tab, you can still have nested components if needed.
Following these considerations, a possible architecture for this use case would be

<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.

@SimoTod
Copy link
Collaborator Author

SimoTod commented May 9, 2020

Closing for now since it's not in scope for v2.

@SimoTod SimoTod closed this as completed May 9, 2020
@Luddinus
Copy link

Luddinus commented Jun 25, 2020

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"

// html
<div x-data="{ someParentVariable: 'lol' }>
    <button @click="openModal('modal-1')">Open modal</button>

   <div id="modal-1" x-data="modal()" x-init="init()">
      <template x-if="$visible">
         // make an ajax request and show the content here when $visible is true,
         // this way it will only make the request when the modal is opened.

         // If I want to access to "someParentVariable", it will be undefined
      </template>
   </div>
</div>

// modal component
function modal()
{
   return {
      $visible: false,

      init() {
         var id = this.$el.getAttribute('id');

         // the "openModal" function fires this event
         window.addEventListener(`modal:show ${id}`, () => {
            this.$visible = true;
         })
      }
   };
}

What should I do? Thanks.

@earthboundkid
Copy link
Contributor

Change openModal() to take a second argument, which is someParentVariable, send that as part of the event, and change the event listener to get that value from the event.

@Luddinus
Copy link

@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)

<div x-data="{ foo: 'bar' }">
   <div x-data="{ baz: 'baz' }>
       <span v-text="foo"></span> // foo is undefined
   </div>
</div>

@Braunson
Copy link

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?

@SimoTod
Copy link
Collaborator Author

SimoTod commented Jul 24, 2020

See #49 (comment)
Alpine won't implement it, at least not in v2. Basic cases can be implemented using the event pattern. For complex cases, there's is a third party library called spruce.

@Braunson
Copy link

Thanks @SimoTod I never knew about Spruce, super handy! I figured it'll be addressed in V3. Thanks.

@HugoDF
Copy link
Contributor

HugoDF commented Jul 24, 2020

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

@ponyjackal
Copy link

ponyjackal commented Jan 11, 2021

What if I want to pass data in x-for to child component?

<template x-for="(pet, index) in pets" :key="index">
   // I want to create a new component for pet card
   <div x-data="{name:''}" x-init={name: pet.name}>
  ..... 
   </div>
</template>

Is there any solution for this?

@ponyjackal
Copy link

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?

@KevinBatdorf
Copy link
Contributor

@ponyjackal Yes that's a fine solution. Or otherwise there are some helpers here you can use ($parent/$component):

https://github.com/alpine-collective/alpine-magic-helpers

And if you want something more to store and access all your data, try Spruce:

https://github.com/ryangjchandler/spruce

@ponyjackal
Copy link

I tried to use $parent, but $parent is undefined

@KevinBatdorf
Copy link
Contributor

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 $el.__x_for.c - 1 to get the x-for index. I'm not sure how reliable that is. You may want to not use `x-for and instead implement the insertion manually.

@guillermo7227
Copy link
Contributor

guillermo7227 commented Mar 24, 2021

I solved it like this:

<div x-data="{myVariableInParent: 'blah'}" x-bind:data-my-variable-in-parent="myVariableInParent">
    <div x-data="{myLocalVariable: 'muah'}" x-init="myLocalVariable = $el.closest('[data-my-variable-in-parent]').dataset.myVariableInParent">
         <span x-text="myLocalVariable"></span> <!-- will be 'blah' -->
    </div>
</div>

Based on @KevinBatdorf 's solution

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.