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

Pre-RFC: iterable yield #531

Closed
NullVoxPopuli opened this issue Aug 29, 2019 · 10 comments
Closed

Pre-RFC: iterable yield #531

NullVoxPopuli opened this issue Aug 29, 2019 · 10 comments

Comments

@NullVoxPopuli
Copy link
Contributor

I've seen this come up a few times in the past couple weeks. People are wanting a way to wrap yielded content with either a component, or a html tag.

the API I could see for this is maybe something like:

{{#each yield as |yieldItem index|}}
  <p>
    {{yieldItem}}
  </p>
{{/each}}

Allowing the invocation site to get each block-level tag-scape to be wrapped with <p>

<BlogPost>
   <Abstract />
   <Preface />
   <Introduction />
   ...
</BlogPost>

This'd be the equivalent of today's:

<BlogPost>
   <p><Abstract /></p>
   <p><Preface /></p>
   <p><Introduction /></p>
   ...
</BlogPost>

So, given that, we still need to worry about about back-compat, so to talk about semantics, and hopefully cover all the use cases:

Traditional yield

{{yield}}
  • yields everything

Iterable yield

{{#each yield as |yieldItem|}}
  {{yieldItem}}
{{/each}}

This is semantically the same as {{yield}} -- especially if the implementation of child content given to a block is always represented as an array.

<ul>
  {{#each yield as |yieldItem index|}}
    <li>{{yieldItem index}}</li>
  {{/each}}
</ul>

In block form, passing positional arguments to yield wouldn't be allowed. Any arguments that would be passed to yield are now passed to yieldItem.

@pzuraq
Copy link
Contributor

pzuraq commented Aug 29, 2019

I feel like this would be a pretty dynamic thing to add to the language. I can see how it would be useful, in general, but it seems like it may be hard to support/optimize. I'd have to think on it a bit more.

I'm also wondering if expanding named blocks via block-capture might solve a lot of the same use cases. In the examples you've provided, there isn't much complexity in requiring the user to wrap every item in a tag:

<BlogPost>
   <p><Abstract /></p>
   <p><Preface /></p>
   <p><Introduction /></p>
   ...
</BlogPost>

But I can imagine that:

  1. We don't necessarily want the user to be in control of that. The specific tag being used may be an implementation detail of the parent component (as in the List example), and if that were to change over time we wouldn't want to have to update every single invocation of the component.
  2. The required boilerplate HTML may actually be fairly complex, for instance you may have a number of classes applied to each item, or some additional elements for each item.

Contextual components might work here, but it's a lot of extra work and doesn't quite fit. The solution with block-capture, however, may look like:

<BlogPost>
  <:section @title="Abstract"><Abstract /></:section>
  <:section @title="Preface"><Preface /></:section>
  <:section @title="Introduction"><Introduction /></:section>
</BlogPost>

And then in the BlogPost template, something like:

{{#each :section as |section|}}
  <h2>{{section.args.title}}</h2>
  <p>
    {{yield to=section}}
  </p>
{{/each}}

This would still require some boilerplate, even in very simple cases, but it would prevent that boilerplate from getting too complicated, or from leaking implementation details of the outer component. It would also be a bit more powerful if it had the ability to pass arguments/attributes into each block.

cc @dgeb

@btecu
Copy link

btecu commented Aug 30, 2019

What would this help with that yieldable named blocks (RFC merged, already implemented in glimmer?) can't do?

@pzuraq
Copy link
Contributor

pzuraq commented Aug 30, 2019

@btecu Currently, yieldable named blocks doesn't allow for you to define multiple versions of the same block. This means you can't have patterns like the one's in my comment, where you provide multiple <:section> blocks for the parent component to use.

To accomplish the original goal of this proposal, wrapping every sub-element passed into the BlogPost without the child doing it, you would have to use contextual components:

<!-- BlogPost -->
{{yield (element 'p')}}
<BlogPost as |section|>
  <section><Abstract /></section>
  <section><Preface /></section>
  <section><Introduction /></section>
</BlogPost>

@chancancode
Copy link
Member

chancancode commented Sep 2, 2019

Isn’t this use case already solved by contextual components?

IMO this is way more clear what is going on (from the caller side):

<BlogPost as |Wrapper|>
   <Wrapper><Abstract /></Wrapper>
   <Wrapper><Preface /></Wrapper>
   <Wrapper><Introduction /></Wrapper>
   ...
</BlogPost>

I don’t think it’s a good idea to interject hidden context-switching in what otherwise appears to be a normal content block.

Besides, how do we define block-level content? Is {{#each}} itself a “block”? Or are the content produced by each iteration of the loop a block? What about {{yield}}? What happens to the whitespace in between? Why is block-level content special for this purpose anyway?

I think this may be a confusion between the purpose of yieldable blocks and yielding components. They may feel similar but they are really the two opposite side of the same coin (I call you vs you call me).

@tstormk
Copy link

tstormk commented Sep 6, 2019

@chancancode I think there's a point where that clarity just becomes redundant, or worse, becomes clutter. For instance, we have a component named Stack, where all its content needs to be wrapped in stack items:

<Stack as |stack|>
  <stack.item><p>...</p></stack.item>
  <stack.item><p>...</p></stack.item>
  <stack.item><p>...</p></stack.item>
  <stack.item><p>...</p></stack.item>
</Stack>

As you can imagine, this quickly detracts from the overall clarity of a template instead of adding to it, especially when we have multiple nested Stacks. It would be very helpful if this wrapping could happen automatically for any immediate child that isn't already a stack.item (using a stack.item would be a legimate use case, as it can have custom arguments)

I think rather than just focusing on iterable yielding, we should expand this to include general awareness of parent and child components inside Glimmer components. I am currently developing a component library for internal use, and there have been many cases where I have needed to know how many instances of a child component my component contains, in which case I would need to send up events on did-insert and will-destroy. This pattern quickly becomes clutter when used in a lot of places, and it also means that the component tree needs to be rendered before I can determine the amount of instances. This can result in flickers in the UI, which is obviously not desirable.

Parent component awareness would also be beneficial, as you might want to display some components differently if they are hosted within a modal, for example.

@chriskrycho
Copy link
Contributor

chriskrycho commented Sep 6, 2019

That's an interesting set of behavior you're aiming for there, @tstormk – can I suggest you write up your use case in detail on the Ember Discuss forums? Reading this, my impression is that there may be better, cleaner ways to accomplish what you're aiming to do here, but we're only getting the solution you've landed on and not what you're trying to solve with it. This isn't the right context for that discussion, but it seems possible to me that this is a classic XY problem situation, and a longer discussion in an appropriate forum might be very illuminating!

@chancancode
Copy link
Member

chancancode commented Sep 6, 2019

@tstormk to me, that seems no worse or more redundant than having to type. <tr> and <td> in a <table>, or <li> within <ul> which matches the HTML programming model and ergonomics. I think the implicitness is especially when you are thinking about sometimes omitting it and sometimes not. I also still find the choice of special casing “block-level” elements to be rather arbitrary. It seems just as common to want to have no element or multiple elements per “row”.

@tstormk
Copy link

tstormk commented Sep 13, 2019

@chriskrycho It's just a basic layout component based on this: https://polaris.shopify.com/components/structure/stack Nothing out of the ordinary except for that one wrapping feature.

I do think you brought up a good point @chancancode and I've reconsidered the wrapping feature. Still though, I'd like to hear you guys' thoughts on bringing more context awareness into components (for example, awareness of parent and child components) like I mentioned in my previous post. In my view it could be very useful, but you also brought up a point re: the wrapping that I hadn't thought of, so maybe there's something I'm missing on it as well.

@rwjblue
Copy link
Member

rwjblue commented Sep 18, 2019

Just a quick 2 cents here, I find the example given by @NullVoxPopuli to be very confusing. It seems like a regression in the "template language" (in the same vein as why we got rid of "context shifting each"), and not an obvious improvement.

@NullVoxPopuli
Copy link
Contributor Author

I'm closing this, because the mental behind the desire for this behavior relies on the understanding of how yield works to be backwards.

yield, appropriately named, gives way to the calling context.
The original post made an assumption that block-content was passed to the component and rendered via a {{yield}} helper

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

No branches or pull requests

7 participants