Some ideas I had about composable/reusable components


#1

Since this commit landed, I’ve been focusing on how we can start composing shareable components and establishing a nice ecosystem of reusable components in the Ember community. There’s no real consensus on this from the core team, these are just some of my thoughts, but here’s a little writeup, would love your thoughts:

Ideas for components / composability

Ember now has the capability to implement the following patterns, but I’d like to see what people think of these patterns and whether there are use cases that have been left out, concerns, etc.

Calendar Widget

This has been Trek’s canonical example of composability. I don’t think I’m going totally in the same direction of what he had in mind; I’m probably focusing more on highlighting the configurability of default components rather than making use of an ecosystem of single-purpose, reusable low-level components, but they’re somewhat closely related.

Add the calendar to your app.

// Within some initializer...
// Not focusing too much on how EmberCalendar is loaded into your
// app, just assume it's global for now.
container.register('component:ember-calendar', EmberCalendarComponent);

// fwiw, though we're trying to get away from storing stuff on globals,
// the following will also work
App.EmberCalendarComponent = EmberCalendar;

This makes the calendar renderable in your templates via {{ember-calendar}}.

Render the default calendar

{{ember-calendar}}

(pardon the jQuery UI ripoff)

Override some defaults

Internally, the ember-calendar defines lots of blocks of content that can be overridden within the block you provided to ember-calendar. Here we override month-header to remove the year and add some exclamation points.

Note that this syntax is strange by today’s Handlebars conventions, but go with it for a second.

{{#ember-calendar}}
  {{#month-header}}
    {{month}}!!!!
  {{/month-header}}
{{/ember-calendar}}

Override multiple defaults

To override more things, just provide multiple blocks…

{{#ember-calendar}}
  {{#month-header}}
    {{month}}!!!!
  {{/month-header}}

  {{#day-cell}}
    {{day}}!
  {{/day-cell}}
{{/ember-calendar}}

Handling events

This isn’t specific to new composability stuff, but you can combine the above with standard component event handling API.

{{#ember-calendar day-selected='daySelected'}}
  {{#day-cell}}
    {{day}}!
  {{/day-cell}}
{{/ember-calendar}}

You could handle this event in some controller that closes the datepicker, saves it to a model, proceeeds to the next step in a wizard, etc.

super

I don’t know how I feel about this yet, but here goes; say we want to make the month header clickable, even though the component author never intended it to be.

App.CustomCalendar = CalendarComponent.extend({
  actions: {
    monthHeaderClicked: function() {
      // Do some custom stuff, maybe call `this.sendAction()`
      // to forward to some outside controller.
    }
  }
});
{{#ember-calendar}}
  {{#month-header}}
    <a href="#" {{action 'monthHeaderClicked'}}>
      {{super}}
    </a>
  {{/month-header}}
{{/ember-calendar}}

We can easily wrap in an a tag, make it fire an event, and display the original content inside the a tag.

On the authoring side of things

I haven’t written the code to support the above, yet, but it’s all very doable. It’d largely be an extension of this JSBin, which makes use of a very Railsy content-for helper to accomplish much of the above. It seems like the authoring solution will follow a similar pattern of 1) define your default content blocks, 2) call yield, 3) invoke the top-most component/helper of your shareable component.

TODO

  • Establish a pattern for sharing/importing components (it’s easy enough to defer / advanceReadiness at the start of an app to make sure all your components are loaded ahead of time, among other things, but we should spend some time making sure whatever we settle on looks pretty, demoes well, etc.)
  • Adopt any awesome ideas yall have?

Views, Components, nesting and {{yield}}
Component ecosystem. Plugin API?
Multiple yield in a component
#2

I like the method of registering the component in the container. Will that work with the current API?

I’ve been wondering whether a hook could be added to the application’s Resolver for handling a component’s template location; in the same way the ‘resolveTemplate’ hook can be overriden in the Resolver to allow templates to be located elsewhere for a particular application.

In an application I’ve been working on, I have multiple ember apps inside a single Rails app. The way I’d taken to organizing it was like:

app1/
  controllers/
  helpers/
  models/
  templates/
  etc...
app2/
  controllers/
  helpers/
  models/
  templates/
  etc...

I use ember-rails for handling template precompilation and then override the resolveTemplate hook for each application to specify that it should look for the templates under the application namespace. Unfortunately, I hit a hitch when trying to implement components inside one of the applications. It seems the components HAVE to be located at templates/components/ (outside of the scope of the application) in order for Ember to pick them up as being valid.

My problem is less applicable since you’re asking about reusable components and my problem pertains to having application specific components. But, I think even in the reusable case, it would be advantageous to be able to override the component’s template naming conventions.

Perhaps if the Component can be registered in the application container, then the template location could either be a property on the component or could, by default, by a combination of the component’s namespace and name. I.E., for a JqueryUi.EmberCalendarComponent, the component template name could resolve to jquery_ui/ember_calendar_component


#3

@machty is might be a bit of an off topic question but it’s related to the idea of ecosystem and reusable components.

From your experience working with components, could it be possible to use components outside of an Ember App or as an HTML element? I could always create an app that only has the component in the application template but seems like boilerplate.

Your proposal looks great.


#4

I’m working on a site which will be dedicated mainly on ember components, how they work and how you can create your own reusable ones, but it’s still in progress. But the proposal about an ecosystem for components gives me just more reasons to put more efforts in the process. One thought I can share right away is that I like the way EAK does register components as of today: https://github.com/stefanpenner/ember-app-kit/blob/master/app/utils/register_components.js which seams a good approach to me to lookup components and register them in the container for availability.

I really hope ember components will get a real ecosystem really soon…


#5

I’d love to be able to override certain parts of a component.

I’ve currently implemented this hackily for a pop-over component so I can invoke it as such:

{{#pop-over}}
    {{#pop-over-toggle}}
        {{selectedItem}}
    {{/pop-over-toggle}}

    {{#pop-over-body}}
        <ul>
            {{#each item in items}}
                <li {{action select item}}>{{item}}</li>
            {{/each}}
        </ul>
    {{/pop-over-body}}
{{/pop-over}}

I’ve done this with pop-over being a proper component, and pop-over-toggle/pop-over-body being handlebars helper views which happen to know that they are inside of a pop-over but it’s a little nasty at the moment.

Having support for this baked in would be a massive win. I also have a calendar component which would be great to be able to override the day view etc exactly as you describe.


#6

A couple of thoughts come to mind when we start creating shared common components. The main concern is that there should be a few common conventions and naming patterns that make different component easy to use together.

1.) Provide some reccommended best practices around name conventions of the action and sendAction methods referenced.

For example in a nested hierarchy of components it would be useful to have easy to understand method names.

e.g. inside the root template we might see something like this

{{ parent-component actionFoo="handleFoo" }}
    {{ sub-component actionFoo="handleFoo" }}
        {{ sub-sub-component actionFoo="handleFoo" }}
    {{/sub-component}}
{{/parent-component}}

each individual component should have an actions signature that is roughly equivalent to this:

actions: {
    handleFoo: function(input) {
        // Implement behavior and pass along hierarchy

        // Send action to external scope
        this.sendAction('actionFoo', input);
    }       
}

Each component may mutate the input in some way before passing along.

So if a deeply nested component has some “Foo” event/action there is:

a.) Something obvious to name it

b.) a reasonable chance that multiple components can all have an opportunity to respond to the event, preferably in a hierarchical way.

2.) It is not entirely clear that when there are many components, that there is a default and organized or hierarchical way to respond to events. And perhaps the response shouldn’t always be vertical and hierarchical. There may be horizontal sequences of event handling for example.

By naming things the same way and implementing or overriding the methods there can be some reasonable consistency in how to handle and bubble events and data through the system.

This seems particularly important when you have keyboard events, and when dealing with focused and unfocused components. You want to be able to arbitrarily say this should component should “always be first to respond to keyboard input” but delegate the event and give all components an opportunity to respond in turn.

This is all a long way of saying perhaps we need some notion of a “event managing object” that can be used to customize the way events flow through a bunch of components.

Probably need to model after the pattern in Cocoa they call the “ResponderChain”

http://cocoadev.com/ResponderChain

https://developer.apple.com/library/mac/documentation/cocoa/conceptual/EventOverview/EventArchitecture/EventArchitecture.html

And have some notion of registering the “FirstResponder”.

Forgive me if there is already something like this implemented. I am still new enough to ember that I might not yet understand all the nuance of the event and target behavior.


#7

The nested tags are cool, but components are suppose have close parity with the html spec for custom tags. I worry that doing something like nested tags would mean that custom tags wouldn’t be useable.


#8

I agree with @rlivsey that overriding parts of a component is highly desirable. After creating a few custom components, I’ve found that {{yield}} is somewhat useless, since the context of blocks does not allow the component to influence the block at all. IMO, this is the most important implication of @machty’s proposal: the component dictates the context of blocks (or sub-blocks). This is illustrated in the {{day-cell}} sub-block who’s context contains the day property, specified by the component.

To give a concrete example, imagine a select-like component a la typeahead.js. The ideal use of blocks in this case would be to provide a custom template for result items – whose content is in a format unknown to the component:

{{#auto-select value=someValue prefetch=dataUrl}}
  <p class="info">{{info}}</p>
  <p class="name">{{name}}</p>
  <p class="description">{{description}}</p>
{{/auto-select}}

Written in the form of @machty’s example, the parent context can even be maintained if needed:

{{#auto-select value=someValue prefetch=dataUrl}}
  {{#with this as parentContext}}
    {{#result-item}}
      <p class="info">{{parentContext.otherInfo}} - {{info}}</p>
      <p class="name">{{name}}</p>
      <p class="description">{{description}}</p>
    {{/result-item}}
  {{/with}
{{/auto-select}}

A related limitation I’ve encountered is that blocks cannot communicate with their enclosing component. Imagine a dialog component:

{{#dialog close="dialogClosed"}}
  <div class="modal">
    <div class="modal-header">
      <button class="close" {{action "close"}}>&times;</button>
      <h3>Heeeey</h3>
    </div>
    <div class="modal-body">
      <p>This is a dialog.</p>
    </div>
  </div>
{{/dialog}}

The close action is triggered on the parent context’s target, skipping the component altogether. The solution to this seems to be a bit more straightforward (in theory if not in practice): actions in blocks should get triggered on the component by default, and simply bubble to the parent context if not handled.


#9

I agree with @slindberg about the not straighforward handling of events in composed components. I also would expect the action to bubble up component hierarchy before bubbling up to the controller and then the route.

At my company we are creating components independently of an app and register them via a handlebars helper to make them accessible when a component file is added to the project. Something like this:


Components = Ember.Namespace.create({
  VERSION: '1.0.0',
  componentPath: function(name) {
    //path ember-rails generates based on directory + name
  }
}); 

Components.SomeComponent = Ember.Component.extend({
  templateName: Components.componentPath('something')
});

Ember.Handlebars.helper('some-component', Components.SomeComponent);

This makes it possible to share components over different projects. As mentioned in my code example, we are using ember-rails and are embedding our js-files via the asset pipeline. This leads to the need to register something like a templatePath-helper or componentPath helper on the app/namespace. Patching the resolveTemplate hook like @AdamFerguson suggested seems like a cleaner solution to this but I was not able to get it working on our project.

@machty’s way on registering the component on the container seems like the best solution yet.


#10

When might we see nested components land in stable? It’s pretty frustrating to see ember market components when they are clearly incomplete in comparison to the angular implementation.


#11

Would you be able to share this code? This would be tremendously valuable. Thanks!


#12

@davidjnelson How are they incomplete relative to Angular?

Keep in mind that all of the ideas I described in the original post, such as components that only exist within the scope of another, are not actually possible in angular, and I’ve already demonstrated this working in decent capacity in a JSBin.

afaik the component composability/reusability story is pretty much the same b/w Angular and Ember, but let me know if your experience/understanding is different.


#13

I want ember components so greatly to have parity with angular directives, since I have to use ember now. I hope it’s just my ember ignorance (I’m extremely new to ember and ramping up as fast as I can).

This is trivial in angular:

<outer>
Hello
<inner>
World
</inner>
</outer>

After compiling these two directives via the compiler service, it’s simple to have it render:

<div>
Hello
<div>
World
</div>
</div>

How do you do this currently with ember components?


#14

In angular, one easy way to share data between nested directives is via injecting services between the directives controllers. I’m on the ipad right now but I’ll take a look at your jsbin when I’m back on the desktop.


#15

@davidjnelson This is trivial in Ember: http://jsbin.com/ucanam/2150/edit

I wasn’t saying you can’t share data between angular directives, but you can’t define scoped directives, i.e. you can’t make a screwdriver directive that only exists when invoked within a toolbox component. The most you can do is globally define a directive and put in some check (probably via restrict) that makes sure that directive is being rendered within some other directive (or within some controller).

It sounds like you have some misconceptions as to the limitations of Ember components relative to Angular directives. To my knowledge they’re both about equally powerful. The one thing I’d say is that angular gives you just slightly more control as to how to how scope/transclusion behaves in a directive you write, whereas Ember for the most part makes you choose between a view, component, or a helper, which each have their own implications on scope/composability. But in practice I haven’t found a compelling case where Ember’s choice of rendering primitives deprives you of any meaningful degree of control.


#16

Thanks @machty !!! This is brilliant! So what I was doing wrong was trying to nest components solely defined in handlebars templates. I’m also new to advanced handlebars fwiw.

Man, I am so grateful for your answer here.

I see what you’re saying. I’ve never needed that in angular, so I haven’t looked into the “only renders inside a certain component’s scope” use case. Not that it isn’t valuable, I just haven’t personally needed it.

Is there any documentation about how to use ember components in similar ways that angular directives are used?

I’ll come back here and ask you when I get stuck again.

You just made my day, THANK YOU!!!


#17

@machty

In angular directives, to do an xhr in your component, you would inject a service that did the xhr into the controller for the directive. I’m trying to think of how to apply this metaphor to ember since afaik ember components do not support encapsulated controllers or the ability to inject models.

How would you architect a similar relationship in ember?

Thanks!


#18

@davidjnelson I think you’ll want to expand your scope beyond Ember Components and dig into how the Ember Router can be used to structure an application, fetch data, manage state, etc. It sounds like you’re taking Angular’s most fundamental building block (directives) and trying to find the closest Ember equivalent as means of figuring out how to structure an Ember app, but it might make sense to consider a Router-based approach.

It’s definitely possible to inject other “services” into components the way you would in Angular, but the usual intent of components is for as much as possible to be explicitly passed in by whoever’s rendering the component. So rather than an x-calendar component looking up the events service, you’d be more likely, in one template, to have:

{{x-calendar events=someEventsPropertySetByAController}}
{{x-calendar events=someOtherEventsPropertySetByAController}}

and in some other template

{{x-calendar events=someEventsPropertySetByYetAnotherController}}
{{x-calendar events=someOtherEventsPropertySetByYetAnotherController}}

The events properties set on the controller could be computed properties or they could perhaps be values set by the router (i.e. you navigated to some the url ‘calendar/2007/12/25’ so the router did a lookup of all the events on Christmas 2007) and passed them to the controllers whose templates rendered a calendar component.

Given the above approach, you can access the value of events via this.get('events') (or even just this.events)

I’d encourage you to see if that approach makes more sense in your app (it often does).


#19

Thanks @machty , this is fantastic and really helps!!


#20

Spread the good word!