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

Conditional blocks #735

Open
chasegiunta opened this issue Apr 12, 2021 · 23 comments
Open

Conditional blocks #735

chasegiunta opened this issue Apr 12, 2021 · 23 comments

Comments

@chasegiunta
Copy link

chasegiunta commented Apr 12, 2021

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
Copy link
Contributor

NullVoxPopuli commented Apr 12, 2021

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 🤔

@pzuraq
Copy link
Contributor

pzuraq commented Apr 12, 2021

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
Copy link
Author

chasegiunta commented Apr 12, 2021

@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
Copy link
Member

chancancode commented Apr 12, 2021

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
Copy link
Member

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

@chasegiunta chasegiunta changed the title Return false upon calling has-block for empty blocks Conditional blocks Apr 12, 2021
@NullVoxPopuli
Copy link
Contributor

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

robclancy commented Mar 30, 2022

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
Copy link
Contributor

betocantu93 commented Apr 28, 2022

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 #460 (comment)

@NullVoxPopuli
Copy link
Contributor

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

@betocantu93
Copy link
Contributor

betocantu93 commented Apr 28, 2022

@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 #460 (comment)

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

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
Copy link
Contributor

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

@NullVoxPopuli
Copy link
Contributor

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

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

@wagenet
Copy link
Member

wagenet commented Jul 23, 2022

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

@NullVoxPopuli
Copy link
Contributor

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
Copy link
Member

wagenet commented Jul 23, 2022

@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
Copy link
Contributor

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

@sandstrom
Copy link
Contributor

sandstrom commented Aug 31, 2022

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
Copy link
Contributor

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
Copy link
Contributor

sandstrom commented Aug 31, 2022

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
Copy link
Contributor

NullVoxPopuli commented Aug 31, 2022

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
Copy link
Contributor

@NullVoxPopuli Makes sense, I understand. Good points!

@tejaskh3
Copy link

tejaskh3 commented Oct 23, 2024

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

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

9 participants