alpinejs / alpine

A rugged, minimal framework for composing JavaScript behavior in your markup.
https://alpinejs.dev
MIT License
28.17k stars 1.23k forks source link

Nested components cannot access external data #49

Closed SimoTod closed 4 years ago

SimoTod commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

+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 commented 4 years ago

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

rosswintle commented 4 years ago

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 commented 4 years ago

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>

2) 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 commented 4 years ago

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:

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 commented 4 years ago

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 commented 4 years ago

Yes. I think you nailed it.

I'd probably sum up my position as:

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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.

SimoTod commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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.

earthboundkid commented 4 years ago

I think that would fail in cases where the name goes from undefined to defined. Yes, thatโ€™s bad practice and you should explicitly set the name to null or false, but I can see it creating hard to diagnose bugs.

bep commented 4 years ago

For me, the important part is that a component should be "pluggable" (so, no $parent, please). I like option C's ease of use, which I think fits nicely into Alpine's way. A component could specify its own namespace in the "interface contract" to remove some surprises:

<div x-data="{ myComponentData: { foo: 'bar' }}">
  <div x-data="{}">
    <span x-text="myComponentData.foo"></span>
  </div>
</div>
earthboundkid commented 4 years ago

If x-data were interpreted in the context of the enclosing component, you could do

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

I think this makes a very natural "props" system that emerges organically from the existing Alpine design without adding any new attributes.

earthboundkid commented 4 years ago

One more example:

<div x-data="{ isLoading: false, data: 'value', ...ajaxThing()  }">
  <div x-data="{ isLoading: false, data: 'value2', parentData: data, ...ajaxThing() }">
    <span x-text="parentData"><!-- equals value --></span>
    <span x-text="data"><!-- equals value2 --></span>
  </div>
</div>

And you can imagine how this might work with some sort of AJAX loader, etc.

earthboundkid commented 4 years ago

Okay, one more idea:

<div x-data="someComponent()">
  <div x-data="someComponent($parent)">
    <div x-data="someComponent($parents)">
    </div>
  </div>
</div>

Normally, x-data can just be what it is, but the magic names $parent and $parents can pass the parent node or an Array of parent nodes respectively, if the sub-component for some reason wants to mess with the parent. This is probably mostly a bad idea because you should just send messages instead, but it's an option.

nyura123 commented 4 years ago

For me, the important part is that a component should be "pluggable" (so, no $parent, please). I like option C's ease of use, which I think fits nicely into Alpine's way. A component could specify its own namespace in the "interface contract" to remove some surprises:

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

This PR plays with the idea of evaluating something for a child component in the context of the parent component, but instead of automatically inheriting the scope, the vars are passed via x-props. With this approach, x-data continues to be internal encapsulated state (like React's state), and x-props are the inputs to the component (like React's props).

It can support prop callbacks as well, though currently they have to be bound to parent's $data explicitly (see examples/index.html accordion examples). Usage:

<div x-data="{ foo: 'bar' }">
    <div x-data="{}" x-props="{ prop1: foo }">
        <span x-text="$props.prop1"></span>
    </div>
</div>
earthboundkid commented 4 years ago

I continue to think this would be just as good if x-data were evaluated in the context of its parent, like this:

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

I don't thinking adding x-props really adds anything in terms of clarity.

In fact, inherit the context when evaluating x-data would also solve another minor problem with Alpine. Today, all x-data functions must be defined on window, so that they can be looked up at initialization. With an inherited context for x-data, you could define one window function and put it on the root and then all the other x-data could inherit it:

<html x-data="defineComponents()">
  <head>
    <script>
    function defineComponents() { return { ... } }
    </script>
    <script src="cdn/blah/alpine.js"></script>
  </head>
  <body>
    <div x-data="someComponent1()"></div>
    <div x-data="someComponent2()"></div>
  </body>
</html>
SimoTod commented 4 years ago

The main problem with those approaches is that are quite frontend oriented. If your views are composed serverside (e.g. Blade component) you could sometimes have a generic component such as an accordion between your parent and your child. That would break the chain and since the accordion is generic and mayne used with a lot of other components you can't have all the possible combinations in it. I think it's what bep meant when he said pluggable, he wants to be able to compose other components at any time without breaking the ineritance.

I think that what @carlmjohnson proposed could work as long as x-data can resolve variables from any ancestor in the chain and not just the parent (not sure if you meant that).

P.s. About thr last snippet, it will be different in v3 but with v2 is better not to define components on the html or body tag because any time something changes, Alpine has to walk the component DOM so it's not ideal performance wise.

bep commented 4 years ago

I think it's what bep meant when he said pluggable, he wants to be able to compose other components at any time without breaking the ineritance.

For me "pluggable" in this context means that the "accordeon component" (or whatever) must not need to know about its surroundings to do its thing. It needs some data according to a contract/interface.

In that sense, all of A, B, and C above satifies that (even if the $parent.foo looks sucpicous) ...

I do, however, suspect that if we discuss this enough, we will eventually end up with the conclusion that Vue got it right.

SimoTod commented 4 years ago

I was more rigid than you, then ๐Ÿ˜‚ My ideal implementation would be one that doesn't force me to go through all the children to add an additional $parent if i decide to change

<div id="foo" x-data="..." >
  <div id="bar" x-data="..." >... <\div>
<\div>

to

<div id="foo" x-data="..." >
  <div id="bob" x-data="..." >
    <div id="bar" x-data="..." >... <\div>
  <\div>
<\div>

or force me to add the variables that I want to pass through in the intermediate component if it doesn't use them.

I understand the point about having a sort of interface so we control what passes through so, after reading the last posts, I would also be happy if something like this worked:

<div id="foo" x-data="{baz: 'test', bin: 'test2'}" >
  <div id="bob" x-data="{open: false}" >
    <div id="bar" x-data="{baz: baz}" >I can use baz here but i can't use bin or open<\div>
  <\div>
<\div>

I think Caleb is looking into options for v3.

nyura123 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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

SimoTod commented 4 years ago

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 commented 4 years ago

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

nyura123 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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