emberjs / rfcs

RFCs for changes to Ember
https://rfcs.emberjs.com/
791 stars 408 forks source link

Conditional blocks #735

Open chasegiunta opened 3 years ago

chasegiunta commented 3 years ago

~Currently if you have dynamic content inside a component block, if not shown, has-block will still return true for that block.~

~It should be possible that empty blocks (excluding whitespace/invisible characters) return false upon calling has-block.~

Provide the ability to optionally pass named blocks to components

NullVoxPopuli commented 3 years ago

We could probably implement this in a non-breaking way by providing a (has-block-content) helper to use instead of or in additional to some optional feature flag :thinking:

pzuraq commented 3 years ago

I think the core need here is the ability to pass optional blocks, rather than detecting whether or not the block has content. Something like:

<MyComponent>
  {{#if @someProp}}
    <:my-block><:/my-block>
  {{/if}}
</MyComponent>

Detecting whether or not a block is actively showing content seems like a much trickier problem to me.

chasegiunta commented 3 years ago

@pzuraq Definitely agree. Differentiating factor there being the requirement of a named block in order to conditionally show, hence my initial suggestion, but I like optional blocks better.

Not sure if helpful, but I wrote this example in Vue the other day to showcase my use-case, which was changing error state to an input field based on if anything was passed in an error slot. https://codesandbox.io/s/summer-http-osuil?file=/src/App.vue

chancancode commented 3 years ago

Yep, what @pzuraq said. As I explained on Discord the exact thing you asked for is not possible:

you can't tell whether a passed block is empty because you don't know unless you actually render it, but rendering it has possible side-effects etc and the block can be rendered multiple times, each time supplied with different block params, etc or even:

<Foo>
  <:lol>{{#if (gt (rand) 0.5)}}LOL{{/if}}</:lol>
</Foo>

...

{{yield to="lol"}}
{{yield to="lol"}}
{{yield to="lol"}}
{{yield to="lol"}}
{{yield to="lol"}}

so "is the passed block empty" is not really a question you can ask

Conditional blocks, on the other hand, does not have that problem, and that’s the equivalent of what your vue example is doing.

Well sort of. There is still the problem that we currently need to eagerly/statically know what blocks are passed before the component is invoked (they work very much like arguments), so there is still that problem, but perhaps we could try to be more lazy.

But even then, we will still have to work out the timing and restrictions on evaluating the conditions. For example, can you use this in that position? What happens if you do this and yield to the block multiple times?

<MyComponent>
  {{#if (gt (rand 0.5))}}
    <:my-block><:/my-block>
  {{/if}}
</MyComponent>
chancancode commented 3 years ago

In the meantime you could use contextual components to emulate this for your exact case.

NullVoxPopuli commented 3 years ago

In the meantime you could use contextual components to emulate this for your exact case.

Just for thoroughness, this approach would look like this:

my-component.hbs
.... stuff above
{{yield (hash
  foo=(component 'my-component/foo' defaultArg='something')
  bar=(component 'my-component/foo/bar' someArg=(eq @argA 2))
)}}
.... stuff below

usage:

<MyComponent as |stuff|>
  <stuff.foo />

  {{#if @condition}}
    <stuff.bar>
       content not shown at all -- down side is that you need another component 
       (located at app/components/my-component/foo/bar.hbs)
    </stuff.bar>
  {{/if}}
</MyComponent>
robclancy commented 2 years ago

I've been using React for a few months on a different project and even though it is pretty annoying to use compared to glimmer components (which are so nice to work with when not having to force workarounds), React doesn't have these basic issues that you would never expect any templating language to have.

Even the workaround with contexual components has issues because you can't pass attributes through to them anymore, you need let to workaround that one or just use params into attributes in the component template, a backwards compatibility break (in a framework that has RFC hell) that has just been ignored.

betocantu93 commented 2 years ago

This problem arises every time you make composition wrapping a component which presentation depends of wether or not the consumer has-block.

component.hbs

<div class={{if (has-block "description") "some-class"}}>
  <h1>{{@title}}</h1>
  {{#if (has-block "description")}}
    <p>{{yield to="description"}}</p>
  {{/if}}
</div>

composed-component.hbs

<Component @title={{@title}} class="my-unique-logical-class">
  <:description>
     {{yield to="description"}}
  </:description>
</Component>

The underlaying component will always render a <p> tag and will always add the class, so conditional blocks feature is needed for these patterns, in ember-eui this is really common. We are starting to converge in this pattern:

component.hbs

{{#let (and (arg-or-default @hasDescriptionBlock true) (has-block "description")) as |hasDescriptionBlock|}}
  <div class={{if hasDescriptionBlock "some-class"}}>
    <h1>{{@title}}</h1>
    {{#if hasDescriptionBlock}}
      <p>{{yield to="description"}}</p>
    {{/if}}
  </div>
{{/let}}

composed-component.hbs

<Component @title={{@title}} @hasDescriptionBlock={{has-block "description"}} class="my-unique-logical-class">
  <:description>
     {{yield to="description"}}
  </:description>
</Component>

Basically we have an escape hatch with a boolean that the consumer can provide by any means, like if the actual final consumer has that particular block or not.

EDIT: just found this comment that describes this too https://github.com/emberjs/rfcs/pull/460#issuecomment-902961346

NullVoxPopuli commented 2 years ago

@betocantu93 thoughts on being able to pass blocks as arguments?

betocantu93 commented 2 years ago

@NullVoxPopuli you mean like splatting/forwarding blocks instead of being explicit in a wrapping component?

Base component

<div class={{if (has-block "description") "some-class"}}>
  <h1>{{@title}}</h1>
  {{#if (has-block "description")}}
    <p>{{yield to="description"}}</p>
  {{/if}}
</div>

Wrapper

<Component 
  @title={{@title}}
  class="my-unique-logical-class" 
  ...blocks
/>

I think that would be a great way to avoid having to deal with conditional blocks stuff, but I think there's still a valid use case when you want to enrich the block in a wrapping component context without it being called if the real consumer doesn't call it, the escape hatch boolean im my prev comment also helps to avoid the permutations explosion you mentioned here https://github.com/emberjs/rfcs/pull/460#issuecomment-902961346

<Component @title={{@title}} @hasDescriptionBlock={{has-block "description"}} class="my-unique-logical-class">
  <:description>
     <span {{mutation-observer onMutation=this.cleverness}}>{{yield to="description"}}</span>
  </:description>
</Component>
robclancy commented 2 years ago

That still doesn't help that much because you won't always have the same name for the block. You should simply be able to use a block in an if statement, that's the only real solution (passing in blocks like above should be added as well though).

betocantu93 commented 2 years ago

Yeah, I agree, this solution is just future proof, since the condition will still evaluate to true/false if the ideal solution lands...

NullVoxPopuli commented 2 years ago

you won't always have the same name for the block.

I was thinking something like ...:blocks like what we do with attributes?

wagenet commented 2 years ago

What would it take to actually get this to RFC? Is there a path forward?

NullVoxPopuli commented 2 years ago

core team opinions / buy-in / acknowledgement / ideas?, I think? Personally, I'd like to go for the blocks as arguments approach, as it allows block forwarding, which is essential in wrapping / abstracting components which provide named blocks.

wagenet commented 2 years ago

@NullVoxPopuli so I understand that core team ideas at this point would be nice, but I don't think that's a necessity to move to RFC. When thing are nebulous it can actually be harder to get good feedback. If there were an RFC for this I can assure you that it will get reviewed and, if there's any promise in the idea, we'll work with you to get it to completion.

NullVoxPopuli commented 2 years ago

makes sense -- I'll try to find some time during work to figure out an RFC for this. thanks!

sandstrom commented 2 years ago

EDIT: unsure about this

Maybe a good starting point would be a smaller RFC that only focused on conditional blocks, and (maybe) also loops?

At least for us, that's the main thing lacking from named blocks.

Conditional

<MyComponent>
  {{#if @someProp}}
    <:my-block><:/my-block>
  {{/if}}
</MyComponent>

Loops

<MyComponent>
  {{#each @myList as |item|}}
    <:my-row @item=item><:/my-row>
  {{/if}}
</MyComponent>
NullVoxPopuli commented 2 years ago

I think allow blocks to be passed as args would cover this (aside from looping, that one doesn't (yet?) Make sense to me?)

sandstrom commented 2 years ago

EDIT: this may not make sense

There may be different ways of solving this, and I'm not sure my idea is the best. But to me, if/else gating would seem more natural.

If/else gating for blocks

From a DSL perspective, gating blocks behind if/else would make more sense to me.

Since we use if/else blocks in our HBS templates in general, it would be intuitive that they worked in this scenario too.

Looping scenario

<CheckboxSelect>
  {{#each myList as |item|
    <:option @value={{item.val}} />
  {{/each}}
</CheckboxSelect>

<!-- current workaround -->
<CheckboxSelect as |Option|>
  {{#each myList as |item|
    <Option @value={{item.val}}>
  {{/each}}
</CheckboxSelect>
NullVoxPopuli commented 2 years ago

I think you want contextual components instead of a named block. or a combination of. blocks can't receive arguments. Example:

<CheckboxSelect>
  <:options as |Option|> 
    {{! render the options in the specific block/slot where options go, 
      as layout is constrained (the primary use case for blocks)
    }}
    {{#each myList as |item|}}
      <Option @value={{item.val}} />
    {{/each}}
  </:options>
</CheckboxSelect>

and syntactically, if you allow {{#each}} and co outside of a block, then you allow everything, which... we also can't have nested named blocks -- how would that work?

sandstrom commented 2 years ago

@NullVoxPopuli Makes sense, I understand. Good points!

tejaskh3 commented 1 day ago

We could probably implement this in a non-breaking way by providing a (has-block-content) helper to use instead of or in additional to some optional feature flag 🤔

Hello @NullVoxPopuli, I landed to this while searching solution for an issue. So, I though to wave at you. Hehe