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

Access Component Properties From Template #4985

Closed
sandstrom opened this issue Jun 5, 2014 · 40 comments
Closed

Access Component Properties From Template #4985

sandstrom opened this issue Jun 5, 2014 · 40 comments

Comments

@sandstrom
Copy link
Contributor

It's sometimes useful to access properties of components from within its template.

Either by exposing the component itself, and otherwise by exposing explicit block params. Two examples are below.

Suggestions and ideas are much appreciated!

wycats mentions this in a related issue, so it seems relevant to reprint that here:

We may want to eventually provide ways for "block arguments" for the component to send information back in explicitly.

The way this should work (I think), is to have both a "template context/controller" and a "layout context/controller". The yield helper would switch into the template context, while the component's template (internally called the "layout") would use the layout context/controller.

source: #2917 (comment)


// ALTERNATIVE 1
// ability to specify specific block params [properties from the component],
// that are made available to the template.

_yield: function(context, options) {
  var view = options.data.view,
      parentView = this._parentView,
      template = get(this, 'template');

  if (template) {
    Ember.assert("A Component must have a parent view in order to yield.", parentView);

    var blockParams = magic(); // a method to generate block params, exact inner workings to be decided  // adjusted
    var keywords = Ember.merge(parentView.cloneKeywords(), blockParams);  // adjusted
    view.appendChild(View, {
      isVirtual: true,
      tagName: '',
      _contextView: parentView,
      template: template,
      context: get(parentView, 'context'),
      controller: get(parentView, 'controller'),
      templateData: { keywords: keywords } // adjusted
    });
  }
},


// ALTERNATIVE 2
// expose `component` keyword

_yield: function(context, options) {
  var view = options.data.view,
      parentView = this._parentView,
      template = get(this, 'template');

  if (template) {
    Ember.assert("A Component must have a parent view in order to yield.", parentView);

    var keywords = Ember.merge(parentView.cloneKeywords(), { component: this });  // adjusted
    view.appendChild(View, {
      isVirtual: true,
      tagName: '',
      _contextView: parentView,
      template: template,
      context: get(parentView, 'context'),
      controller: get(parentView, 'controller'),
      templateData: { keywords: keywords } // adjusted
    });
  }
},
@sandstrom sandstrom changed the title Access Component properties from template Access Component Properties From Template Jun 5, 2014
@arenoir
Copy link

arenoir commented Jun 26, 2014

I like the idea of exposing a component keyword. Here is a use case where I am using it.

http://jsbin.com/xuvuz/2

@oskarols
Copy link
Contributor

oskarols commented Jul 4, 2014

+1 for exposing component keyword.

I'd really like this to become a thing. Could you open a PR or start a thread on discuss?

@sandstrom
Copy link
Contributor Author

@stefanpenner what are your thoughts on this?

short version: expose component keyword inside component templates

@sly7-7
Copy link
Contributor

sly7-7 commented Jul 5, 2014

I'm not sure I understand well, but it seems to me that simply the view keyword inside a component template refers to the component.
@arenoir I've tried to use that in your jsbin, and seems to work like you want http://jsbin.com/giyah/1/edit

@rwjblue
Copy link
Member

rwjblue commented Jul 5, 2014

@sly7-7 - Yes, view should refer to the component itself in the components layout, but I think @sandstrom is referring to accessing the view from the components template (aka the thing inside the block passed in). Something like http://emberjs.jsbin.com/rwjblue/14/edit.

@trek
Copy link
Member

trek commented Jul 5, 2014

I've been 👎 on this in the past. @machty and I (and probably others) have spent crazy amounts of time thinking through the two interrelated problems: component composition and component isolation.

Component composition (basically "how do you compose a component by combining other, smaller components" is the upcoming holy grail for web apps and something I've been hoping Ember would lead on for years. It's ostensibly the goal of view-only libraries like https://github.com/facebook/react and https://github.com/polymer. I think we've actually fallen behind or just plain gotten this stuff wrong.

Although it's not terribly difficult to write components with complex behaviors in Ember it's still painfully hard to author components modularly (i.e. with internal pieces can be configured and replaced for customization).

Contributing to this problem is poor component isolation. Our description of components is basically a lie. Blockless components don't have access to the context of where they get used but block components do, which opens up questions like "ok, but in that block how do I change contexts back to the component?"

Ask @rpflorence and he'll tell you if you find yourself using templates in components it's because you're missing some abstraction

I suspect we'll either need to extend Handlebars with sugar around these use cases (which is what block args would help do) or put templates down, back away slowly, and admit that they're useful for non-isolated, controller backed display but an anti-pattern for data isolated display.

As an aside: default two-way data binding is the other big way that component isolation is broken. Data mutation from inside the component leaks back out into the context where it was used without your control. It's incredibly convenient (and demos well!) but can such a source of pain when you don't want it.

@sandstrom
Copy link
Contributor Author

@trek I read this as a vote for increased isolation. Would you prefer removing access to the surrounding scope for 'blockful' components (to increase isolation)?

If so, a component keyword would be on the right side of the isolation divide, no? (if one consider both the template and the Ember.Component object to be part of the component).

I'm not arguing against here. Although I haven't given it much thought isolation for components seems desirable, and removing access to outer context for component templates might be a good idea. I'm just trying to understand if you consider the template to be part of the 'isolation cell' or not.

@trek
Copy link
Member

trek commented Jul 6, 2014

Would you prefer removing access to the surrounding scope for 'blockful' components (to increase isolation)?

If you removed surrounding scope access (which I don't think we can do because of helper/components like each and link-to) you won't need a component keyword since this would resolve to the component itself.

I don't think anyone has really nailed the scope problem. I've seen Angular's $scope turn into a giant global dumping ground. React punts on the problem be forcing you to say which lookup object you want (this.props vs this.state).

Ember has the additional complication of a four desired lookup locations (and action targets) when you use a block-template component inside a controller backed template:

  1. outer context lookup from outer context (targets controller)
  2. outer context view's lookup from outer context (targets view)
  3. inner context lookup from inner context (targets the component inside the components own template)
  4. inner context lookup from the outer context (targets the the component)

application.hbs

(1) {{foo}}<- the controller
(2) {{view.bar}} <- the controller's paired view
{{#my-thing thing=foo}}
  (1) {{foo}} <- controller
  (2) {{view.bar}} <- the controller's paired view
  (4) {{component.propertyBasedOnThing}} <- component 
{{/my-thing}}

components/my-thing.hbs

  (3) {{thing}} <- component
  {{yield}}

It's totally reasonable to want access to each of them, so I'm not 👎 on component keyword per se. I'm 👎 on whole messy situation, of which wanting component keyword is a symptom, that causes for so much complecting. It's entirely reasonable in Ember to have a template using a component and switch back and forth between various contexts but I think it causes a spike in complexity. I'm worried it is basically a footgun for maintainability.

@ryanflorence
Copy link

React punts on the problem be forcing you to say which lookup object you want (this.props vs this.state).

This thread is meaningless in React. Components are just functions where normal JavaScript scope applies. Props and state separate what the outside world gave you and what you are managing yourself. Data flows down, you wouldn't ever be asking a child for its state from the owner. You would have your own piece of state and pass that down to a child as a prop, and if the child needed to change it you'd also pass it a handler, receive the new value, and then set state again.

@ryanflorence
Copy link

I'm not sure I understand the use-case here.

If you need a component's property in a yielded template from the controller, then pass in a controller property to the component and use it.

If both the controller and the component care about the property then that's exactly what these bound properties are for, right?

I am in a controller template

{{#x-foo}}
  {{component.bar}}
{{/x-foo}}

That seems bad if the outside world can be reaching in,
instead, just give it what you want

{{#x-foo bar=controllerBar}}
  {{controllerBar}}
{{/x-foo}}

Curious what is wrong with this ...

@trek
Copy link
Member

trek commented Jul 6, 2014

@rpflorence (4) {{component.propertyBasedOnThing}} <- component from my example code is the use case. You have a component property based on a value that you passed and want to use that in the block:

export default Component.extend({
  bar: null,
  isMeaninglessInReact: function(){
    return this.get('bar') > 20;
  }.property('bar')
});
{{#x-foo bar="someProperty"}}
  {{#if component.isMeaninglessInReact}}
    :trollface:
  {{/if}}
{{/x-foo}}

@sly7-7
Copy link
Contributor

sly7-7 commented Jul 6, 2014

@rjackson @trek Just as a note,
the (2) {{view.bar}} used inside a component refers the component itself. Not in master, but in the current ember release. see Robert's previous fiddle against the release build: http://emberjs.jsbin.com/garife/1/edit
So it's basically the same behavior as (4). I don't know if this behavior's change was intented.

Anyway, I think I have a similar use case, trying to implement a modal component, with header, body, footer sub-components, where I wanted to factorize some actions in the root component, and call them from the subcomponents (for example, the close action is on the modal, but could be send either from a button in the header, and from a button in the footer).

I just watched @rpflorence's video, and I realize I am maybe developing the component in the wrong way. Will try to implement differently, like ic-tabs, and will see if it smells better.

@arenoir
Copy link

arenoir commented Jul 10, 2014

@sly7-7 the example I posted earlier is another use case. A button-group-component where children toggle the "open" property. Like you stated it works in release but not in master.

release: http://jsbin.com/giyah/1/edit
master: http://jsbin.com/setej/1/edit

I am wondering if I am going off in the wrong direction? I could pass the "open" property in from the controller but I feel that is over doing it, especially with multiple button groups.

@manuelmitasch
Copy link

Yes, it seems the behavior has changed. It is breaking some of our components.

On Ember 1.6.1:

On Ember 1.7.0 and master:

Is this a bug or a yet undocumented breaking change?
PING @rwjblue @trek

@alexspeller
Copy link
Contributor

I have heard that in the future you will be able to explicitly specify properties that are available in the block template.

For now, the workaround if you really need it is to use _view.parentView (private api warning here)

http://emberjs.jsbin.com/yihupahupihe/1/edit

@krisselden
Copy link
Contributor

@manuelmitasch it was a bug fix, the block is supposed to be isolated from component, thus the view keyword isn't supposed to be the component view in that case. We are working a new feature to yield args to the block.

@krisselden
Copy link
Contributor

@alexspeller I wouldn't count on that continuing to work.

@krisselden
Copy link
Contributor

This is the direction we are currently looking at in regards to this issue emberjs/rfcs#3

@manuelmitasch
Copy link

@krisselden: Thanks for the clarification! This means there is currently no reliable way to access properties of a component from within a block? The only real solution for now is to use a view instead of a component, right?

@sandstrom
Copy link
Contributor Author

@manuelmitasch though it's a hack, perhaps // ALTERNATIVE 2 above can be helpful. I'm using that right now in a live app and it works well.

@manuelmitasch
Copy link

@sandstrom: Thanks, this looks good.

@aaronbhansen
Copy link

Having recently updated to 1.7, we also found some of our components we built stopped working. It was rare that we needed to access a component from within a yield, but it did happen and was advantageous to do so.

We could convert the existing components to views, but we lose the ability to specify specific actions and have actions within the view (without extending that ability onto the view). One example we used was with a file upload component that took care of all the of uploading a file within the component. An overview can be seen here. https://gist.github.com/aaronbhansen/8ae4fb14f5dd82aeed27

If the block format will still allow you to access the component and call out actions to the component, then that will work as a solution. In the meantime, we can implement // ALTERNATIVE 2 above, but it would be nice to have a way to access the component from within a yield, until handlebars is updated to allow block statements.

@ppcano
Copy link
Contributor

ppcano commented Oct 10, 2014

@rwjblue, which is the current workaround to access the component inside a yielded block content? Is this currently possible?

This jsbin demonstrates the use case and example http://emberjs.jsbin.com/japipe/1/edit

{{#some-random}}

      {{#each item in model}}
         <div {{action 'select' item target=view}}>Click {{item}}</div>
      {{/each}}

{{/some-random}}

components/some-random.handlebars

   Some component header content.... 
   {{yield}}

@manuelmitasch
Copy link

@ppcano AFAIK you still need to use //ALTERNATIVE 2 from above. I have update your jsbin with a working example. http://emberjs.jsbin.com/gavuhu/1/edit

I have also updated the _yield method to mirror the current component implementation:

  _yield: function(context, options) {
    var view = options.data.view;
    var parentView = this._parentView;
    var template = Ember.get(this, 'template');

    if (template) {
      Ember.assert("A Component must have a parent view in order to yield.", parentView);

      var keywords = Ember.merge(parentView.cloneKeywords(), { component: this });  // adjusted      

      view.appendChild(Ember.View, {
        isVirtual: true,
        tagName: '',
        _contextView: parentView,
        template: template,
        context: options.data.insideGroup ? get(this, 'origContext') : Ember.get(parentView, 'context'),
        controller: Ember.get(parentView, 'controller'),
        templateData: { keywords: keywords, insideGroup: options.data.insideGroup }
      });
    }
  },  

@sandstrom
Copy link
Contributor Author

@ppcano also, there is ongoing discussion in emberjs/rfcs#3 which, according to krisselden's comment above, will probably be used to solve this issue.

@samselikoff
Copy link
Contributor

@rpflorence what about this:

I am in a controller template

{{#x-panel}}
  {{#x-panel-header}}
    Something interesting
    {{#if component.isOpen}}
      <i class='arrow-down'>
    {{else}
      <i class='arrow-up'>
    {{/if}}
  {{/x-panel-header}}
{{/x-panel}}

The isOpen prop is only "needed" by the controller so the user can customize the component's template based on its state. You could pass in a prop from the controller but that seems unnecessary from the user's perspective (he doesn't want to make a new prop, just wants to customize the template).

What do you think? Is there a better way to do this?

@ryanflorence
Copy link

I think I'd just do this:

{{#x-panel isOpen=panelIsOpen}}
  {{#x-panel-header}}
    Something interesting
    {{#if panelIsOpen}}
      <i class='arrow-down'>
    {{else}
      <i class='arrow-up'>
    {{/if}}
  {{/x-panel-header}}
{{/x-panel}}

@samselikoff
Copy link
Contributor

What about when you have multiple panels though

@ryanflorence
Copy link

Yeah, that sounds terrible. I don't find component inside there to be terrible, probably works well for a lot of things.

@brian-gates
Copy link
Contributor

You could always specify the context with "with"

{{#x-panel}}
  {{with component as panel1}}
    {{#x-panel-header}}
      Something interesting
      {{#if panel1.panelIsOpen}}
        <i class='arrow-down'>
      {{else}
        <i class='arrow-up'>
      {{/if}}
    {{/x-panel-header}}
  {{/with}}
{{/x-panel}}

Alternativey, labels within the component statements themselves?

{{#x-panel as panel1}}
  {{#x-panel-header}}
    Something interesting
    {{#if panel1.panelIsOpen}}
      <i class='arrow-down'>
    {{else}
      <i class='arrow-up'>
    {{/if}}
  {{/x-panel-header}}
{{/x-panel}}

@samselikoff
Copy link
Contributor

component is a keyword? Could you jsbin?

Oh, is that using alternative 2 above? private apis right?

@wagenet
Copy link
Member

wagenet commented Nov 1, 2014

@emberjs/owners is this something we should pursue?

@rwjblue
Copy link
Member

rwjblue commented Nov 1, 2014

The block params RFC is likely the way forward here.

@wagenet
Copy link
Member

wagenet commented Nov 1, 2014

Closing in favor of emberjs/rfcs#3.

@wagenet wagenet closed this as completed Nov 1, 2014
@akhomchenko
Copy link

Good day.

How can block params help in case when I need component ref itself?

E.g.

{{! controller's scope }}
{{#hidden-block}}
    <a class="opener" {{action 'toggleHidden' target=component}}></a> // (?)
 ....
{{/hidden-block}}

So, this is a ref to controller, not to a HiddenBlockComponent which handles an action.

Thanks.

@rwjblue
Copy link
Member

rwjblue commented Feb 10, 2015

make the hidden-block's layout the following:

{{yield this}}

@Globegitter
Copy link

@rwjblue I just tried this and it doesn't seem to work for me.

I have a component as follows:

{{results.length}} results found:
{{#each results as |result|}}
    {{#if template}}
      {{yield this}}
    {{else}}
      {{!-- Some default template just for prototyping --}}
    {{/if}}
{{else}}
  0 results found. Just try another query.
{{/each}}
{{!-- Will be called from different controllers with different model and templates --}}
{{#search-results results=model}}
  {{#link-to 'topics.topic' result}}{{result.text}}{{/link-to}}
 {{/search-results}}

Which does not work on one of the latest ember master. Is this supposed to work now? If so how?

@Globegitter
Copy link

Ah, I found this http://www.slideshare.net/mixonic/new-component-patterns-in-emberjs and git it working as follows (for reference):

Component:

{{results.length}} results found:
{{#each results as |result|}}
  <h4>
    {{#if template}}
      {{yield result}}
    {{else}}
      {{!-- Default template --}}
    {{/if}}
  </h4>
{{else}}
  0 results found. Just try another query.
{{/each}}
{{#search-results results=model as |result|}}
   {{#link-to 'topics.topic' result}}{{result.text}}{{/link-to}}
{{/search-results}}

@ahouchens
Copy link

What do I do if I want to use a component action inside of the component block-helper?

Context is a controller template.
{{#ui-sidebar}}
  <a {{action "componentToggleSideBarAction"}}> Click me to toggle </a>
{{/ui-sidebar}}

This doesn't currently work.

@ldong
Copy link

ldong commented Apr 7, 2017

@ahouchens

ui-sidebar.hbs

{{#ui-sidebar}}
  {{yield (hash myClick=(action "componentToggleSideBarAction")) }}
{{/ui-sidebar}}

ui-sidebar.js

actions: {
  componentToggleSideBarAction() {
   // code
  }
}

This is how you use it

{{#ui-sidebar as |uiSideBar|}}
  <a onclick={{action uiSideBar.myClick}}> Click me to toggle </a>
{{/ui-sidebar}}

Tested on ember version: v2.3

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