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

hasBlock.js #102

Closed
wants to merge 1 commit into from
Closed

Conversation

miguelcobain
Copy link
Contributor

Rendered

Please let me know of any other use cases for hasBlock in js land.
Also, please let me know of any drawbacks you can think of.

/cc @alexspeller @rwjblue

@alexspeller
Copy link
Contributor

@shaunc @BryanCrotaz pinging you here as you're currently building the whackiest components I'm aware of - is this an issue you've come across?

@BryanCrotaz
Copy link

Ooh nice. Yes please. Any component that has default behaviour that can be overridden by a block would find this useful.

@mmun
Copy link
Member

mmun commented Oct 28, 2015

@BryanCrotaz The question is what can you do with hasBlock.js that you can't do with {{#if hasBlock}}?

@shaunc
Copy link

shaunc commented Oct 28, 2015

We are in fact using {#if hasBlock}}. It does simply seem odd that you can't use hasBlock as a computed property. From a (framework) user interface perspective, perhaps the question should be -- is there any reason to make hasBlock exceptional in that it can't be used as a computed property? I can't think of any justification. Why have some properties that are "template properties" different from other properties; where is the documentation on this distinction and does it explain convincingly why it was made?

@mmun
Copy link
Member

mmun commented Oct 28, 2015

We could make has-block a subexpression instead and it would be more consistent. I agree we should strive to keep template context === the actual context.

@mmun
Copy link
Member

mmun commented Oct 28, 2015

I am persuaded by the argument that a block is as much a part of the public interface of a component as attributes are, and we already bundle up a snapshot of the attributes in attrs. It seems plausible to package up a blocks field in the same way:

{{#my-each items as |item index|}}
  {{index}}: {{item.name}}
{{else}}  
  No items :'(
{{/my-each}}
// components/my-each.js

export default Ember.Component.extend({
  init() {
    console.log(this.blocks);
    /* Logs:
      {
        default: { arity: 2 },
        inverse: { arity: 0 }
      }
    */
  }
});

@shaunc
Copy link

shaunc commented Oct 28, 2015

This looks great... (very similar to what functionality We've tried to support in ember-declarative); however:

  1. presumably wouldn't be possible until didRender() as the blocks have to render
  2. one general sticking point we've had to be careful about in declarative components is that if one part of the template depends on a value which is set in another part, you have to render twice. In theory, this should be avoidable if the template is rendered depth-first, but in practice seems very hard to avoid. Here, if blocks was used in the template things could get hairy.
  3. What I've toyed with is more like co-routine functionality:
{{#each items as |item index|}}
  {{yield-into "blocks" item index}}
{{/each}}

Where this would be the layout for a template:

{{index}}: {{item.name}}

Here the idea is that the yield-into sets blocks[index], which can be used later on in the template. To avoid rerendering the set needs an explicit exception in the machinery, I think, which would need to be noted in the documentation of yield-into. (This later feature -- opt into special treatment -- seems better than having blocks as a magical property that acts differently than others.).

The yield-into construct would allow having multiple loops, or just repeated elements and no loops.

@grapho
Copy link

grapho commented Nov 17, 2015

i am curious about the necessity for this? if a component is yielded as a block should we have the ability to compute properties from that status, potentially changing what the component actually does if it is a block or not? should a component be allowed to do completely different thing if it is rendered as a block vs. not?

i think that is the possibility that is ultimately being opened up here.

@miguelcobain
Copy link
Contributor Author

miguelcobain commented Nov 17, 2015

@grapho I read that you're asking for real use cases.

Here is mine:

I basically want to understand if I need to create a 3rd party popup or not. I made the component's block to be the popup's content. That was a design decision. This is probably an edge case, but there other different and more common cases.

It all depends on what your component semantics are. Here I assumed that the block of my component is the popup content. Similarly, other components semantics may require different stylings depending on hasBlock.
This is why we need more use cases.

@yankeeinlondon
Copy link

any chance of this coming back into focus?

@miguelcobain
Copy link
Contributor Author

@ksnyde can you add your use case?

@yankeeinlondon
Copy link

I'm using it for my ui-animate class which needs to know where to target the animation classes. If it's used as a block component then the target is itself, if its inline then it would be somewhere else in the DOM.

This is the most recent use-cases (aka, today) where I've wanted this but it's come up several times in the past.

@notmessenger
Copy link

notmessenger commented Aug 24, 2016

Use Case 1

Similar to those listed in emberjs/ember.js#11741: Need the ability to set a class depending on the presence of a block or not on the root element.

Use Case 2

Am developing a component to display error messages associated with the validation of form elements. If there is a string assigned to the error object for the property specified, then that error message is displayed. For example:

errors: {
  name: 'Cannot be empty'
}

{{f.error property="name"}}

Given a string assigned to the error object for the property specified it is also possible to provide your own error message rather than the one from the string:

errors: {
  name: 'Cannot be empty'
}

{{#f.error property="name"}}
  overriding the error message
{{/f.error}}

This latter behavior can also work with this setup

errors: {
  name: true
}

{{#f.error property="name"}}
  overriding the error message
{{/f.error}}

where the indication that there is an error, through the use of true is all that is needed, as the message to be displayed is coming from the template instead.

But in this scenario, the first component usage listed - {{f.error property="name"}} - will then display the text of "true" when it should not display any text. There is some additional more-complicated logic going on in the component, such as dynamic computed properties (which I can share if desired) but the crux of the issue is that I need for the property to not be displayed when is a boolean true but for it to be when it is a boolean true and the component is being used in block form.

EDIT: Regarding my last paragraph the determination of the value is a computed property in the component and I need to make this decision in the component, not in the template.

@nathanhammond
Copy link
Member

We actively ensured that hasBlock wasn't exposed in JavaScript land and went out of our way to make it happen. (Even directly rolling it back after it leaked into an early build.) @mmun gives a template-only solution above that covers most scenarios we're aware of. This is confirmed by the lack of activity on this RFC since it was originally opened.

In the future it's likely that the best solution emulates the Web Components "slots" proposal and remains completely declarative inside of the template. It is unlikely that we expose whether or not the component is simply invoked as a block as that interim step sets up more complicated migration paths for people to the more-idyllic solution.

@notmessenger
Copy link

@nathanhammond The template-only solution is only viable if you can wait until after the template has been rendered to make use of the set property in the component, such as in a computed property. If you want to use the knowledge of whether the component is being used in block or non-block form in init(), for example, you cannot, as the template has not yet been rendered to provide this information.

@nathanhammond
Copy link
Member

@notmessenger Confirm, but we don't want the layout to result in conditional changes to how something is loaded. We intentionally want information flow to be unidirectional, making it bidirectional results in lots of complexity and makes it far more difficult to understand what is happening inside of the template.

Your example in your previous post can certainly be implemented using {{#if hasBlock}} and I'm unsure of why you need to eagerly do processing inside of init based upon whether it is a block or inline invoked component?

(Yes, we can come up with contrived examples that do all sorts of weird things in init but I feel like this needs compelling evidence and common patterns to be enabled as a feature. That it has been crickets on this thread for months implies that it isn't a strong need to me.)

By splitting up or nesting or generally refactoring components I believe we can achieve all of the desired behavior.

@miguelcobain
Copy link
Contributor Author

@nathanhammond this thread going "crickets" doesn't make me not want this any less. 😞

I still need to use this hack: https://github.com/miguelcobain/ember-leaflet/blob/master/addon/mixins/popup.js#L17-L24

That case isn't trivial to implement with {{#if hasBlock}}.
Also, I always thought that the template's context was the component. I was very surprised to find that this.get('hasBlock') didn't work.

@notmessenger
Copy link

notmessenger commented Aug 24, 2016

And most of the activity on emberjs/ember.js#11741 occurred before this PR even existed.

@nathanhammond
Copy link
Member

@miguelcobain I appreciate how you're striving for an incredibly clean UI for consumers of ember-leaflet. I posit that dynamically adding popup behavior to be the content of the block if provided is a bit dangerous and far better supported by moving to a slot-based API.

I'd encourage us to look toward that as the direction we want to take this. @miguelcobain and @notmessenger if you'd like to work toward a slot-based proposal I think that would be something the Ember core team would be receptive to, but you should check pulse in #dev-ember first.

@mmun
Copy link
Member

mmun commented Aug 24, 2016

@miguelcobain You're right that that is very confusing. It was an accident to use the syntax {{#if hasBlock}}. It should have been (or maybe it already is supported?) {{#if (has-block)}} to make it explicit that it is a helper call.

@mmun
Copy link
Member

mmun commented Aug 24, 2016

I think most use-cases will be covered by Glimmer component. The new Glimmer component will have a more declarative way of handling the root element that will remove the need for classNameBindings/attributeBindings/etc.

Since there is a pretty good, albeit ugly, workaround for many cases (https://github.com/miguelcobain/ember-leaflet/blob/master/addon/mixins/popup.js#L17-L24) we are prioritizing on pushing Glimmer component to completion and teaching only the template helper version of has-block.

@miguelcobain
Copy link
Contributor Author

An has-block helper makes this story much more consistent, even if we don't end up having a hasBlock on the component declaration.

@SaladFork
Copy link

Having has-block as an explicit helper would go a long way towards making this easier to understand. I was very confused that {{#if hasBlock}} worked (when there was a block), but {{hasBlock}} was always undefined. In both cases it looks like a property lookup.

@rwjblue
Copy link
Member

rwjblue commented Sep 8, 2016

FWIW - Ember 2.9+ will have has-block that can be used much more consistently: https://jsbin.com/hezoju/edit?html,js,output.

@bcardarella
Copy link
Contributor

@rwjblue is the implementation accessible from within the component itself or only templates?

@rwjblue
Copy link
Member

rwjblue commented Sep 30, 2016

@bcardarella - Templates only

@bcardarella
Copy link
Contributor

@rwjblue is there a plan to provide a public API for components to determine if a block has been passed?

@shhQuiet
Copy link

shhQuiet commented Sep 30, 2016

More fuel for the discussion, my use case:

I am using an external framework (clipboard.js) and attempting to integrate it into my component. The framework requires a data- attribute on a tag. If my component is used in non-block form, I can bind the component's "value" property to the data- attribute. If it is used in block form, I need the text() contents of the block to be assigned to the data- attribute.

@seanCodes
Copy link

seanCodes commented Oct 14, 2016

Yet another use case, in which I'd actually like access to whether an inverse block was provided:

I've got a typeahead component and I'd like to allow consumers to provide markup for the typeahead dropdown's "default" state (when the input is focused but nothing is being searched):

{{#ui-typeahead as |suggestions|}}
  {{#if suggestions}}
    {{! Suggestion Template }}
  {{else}}
    No results.
  {{/if}}
{{else}}
  This is the default state.
{{/ui-typeahead}}

In this case, if an inverse block is provided, I'd actually like to change the behavior of the component. The typeahead would show it’s dropdown with the default state rendered inside when the input is focused—instead of only when there are suggestions to display. (The behavior would be similar to this example.)

{{! ui-typeahead.hbs }}

...
{{#if shouldShowDropdown}}
  <div class="typeahead-dropdown">
    {{#if searchTerm}}
      {{yield suggestions}}
    {{else if (hasBlock "inverse")}}
      {{yield to="inverse"}}
    {{/if}}
  </div>
{{/if}}

While a hasDefaultState flag could be passed to the component to explicitly enable the alternate behavior, it would be nice to elegantly handle the presence of an inverse block in the component JS without any extra effort from the consumer.

@atomkirk
Copy link

atomkirk commented Feb 6, 2017

I'll share my use case:

{{if (or showAdd (gt filteredResults.length 0) (and (gt searchText.length 0) (hasBlock "inverse") filteredResults.isSettled)) "" "hidden"}}

would really like to move that complex logic into a computed property in my js file

@mellatone
Copy link

+1 for @atomkirk's use case. It's a lot easier to unit test a complex computed property like that than having to step into an integration test. Not having a hasBlock property becomes especially painful when attempting to provide a clean API for components.

This RFC is naturally going to gather (less) attention which is proportionate to the number of people authoring add-ons and writing integrations in the community. These are still valid use cases.

@AustP
Copy link

AustP commented May 27, 2017

I would like to access hasBlock in the JS of a component as well. Here is my case:

My component extends Ember.LinkComponent. When you use {{link-to}}, the parameters change depending on if you called it as a block component or not. (See line 812). If it is called as a block, the first parameter is the route. If it is called as an inline component, the first parameter is the link title.

My component can be called as a block or inline component, but the link title is set by my component rather than the user. This is problematic for me, because if the user uses my component inline, the route gets set as the link title and routing breaks but if they use my component as a block, routing works fine.

Being able to do the following would solve my issue:

if (!this.get('hasBlock')) {
    params.unshift('generated link title');
}

The link-to logic takes place during the didReceiveAttrs stage so I can't wait for the template to render before accessing hasBlock. Right now my workaround is to extend didReceiveAttrs and check the value of linkTitle. If this value is the expected route, then I re-assign the rest of the values to be what they are supposed to.

@oso-mate
Copy link

still no hasBlock available in the component JS?
Determining if the element has children elements or not kills our ability to use CSS advanced features, like :empty.

@machty
Copy link
Contributor

machty commented Jul 30, 2017

This has been superseded by the Named Blocks RFC; you'll be able to check the falsiness of blocks passed in as properties to know whether a block was passed in, so there's no separate need for a hasBlock.js.

@machty machty closed this Jul 30, 2017
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

Successfully merging this pull request may close these issues.