rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

Conditional blocks

Open chasegiunta opened this issue 4 years ago • 23 comments

~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

chasegiunta avatar Apr 12 '21 16:04 chasegiunta

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:

NullVoxPopuli avatar Apr 12 '21 16:04 NullVoxPopuli

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.

pzuraq avatar Apr 12 '21 16:04 pzuraq

@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

chasegiunta avatar Apr 12 '21 17:04 chasegiunta

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 avatar Apr 12 '21 17:04 chancancode

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

chancancode avatar Apr 12 '21 17:04 chancancode

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>

NullVoxPopuli avatar Apr 12 '21 18:04 NullVoxPopuli

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.

robclancy avatar Mar 30 '22 03:03 robclancy

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

betocantu93 avatar Apr 28 '22 14:04 betocantu93

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

NullVoxPopuli avatar Apr 28 '22 15:04 NullVoxPopuli

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

betocantu93 avatar Apr 28 '22 15:04 betocantu93

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

robclancy avatar Apr 28 '22 23:04 robclancy

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

betocantu93 avatar Apr 29 '22 06:04 betocantu93

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

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

NullVoxPopuli avatar Apr 29 '22 10:04 NullVoxPopuli

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

wagenet avatar Jul 23 '22 02:07 wagenet

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.

NullVoxPopuli avatar Jul 23 '22 12:07 NullVoxPopuli

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

wagenet avatar Jul 23 '22 16:07 wagenet

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

NullVoxPopuli avatar Jul 23 '22 17:07 NullVoxPopuli

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>

sandstrom avatar Aug 31 '22 10:08 sandstrom

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

NullVoxPopuli avatar Aug 31 '22 11:08 NullVoxPopuli

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>

sandstrom avatar Aug 31 '22 14:08 sandstrom

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?

NullVoxPopuli avatar Aug 31 '22 14:08 NullVoxPopuli

@NullVoxPopuli Makes sense, I understand. Good points!

sandstrom avatar Aug 31 '22 14:08 sandstrom