This addon intends to provide helpers for building a somewhat different kind of components, i.e components which primary goal isn't to render DOM.
To install this addon, run the usual
ember install ember-composability-tools
This addon was essencially extracted from ember-leaflet and then adapted for more generic scenarios. The idea behind ember-leaflet is to compose components the usual way, but to have them to produce leaflet layers and not DOM. To do that, Ember's component lifecycle hooks are used to create the leaflet layers and not produce any DOM (tagless and templateless components).
This idea sounded good in theory, but in practice some problems appeared. The following sections will illustrate both those problems and how ember-composability-tools helps solving them.
Consider the following template:
We would like it to be equivalent to the corresponding code using the Leaflet API:
let map = L.map(this.element).setView([51.505, -0.09], 13);
L.tileLayer('http://sometiles.com/{z}/{x}/{y}.png').addTo(map);
L.marker([51.505, -0.09]).bindPopup('Hello World!').addTo(map);
In other words, there is an order that needs to be followed. This order is more evident when there are more levels of nesting, but in this case the order is:
- create map
- create tile
- create marker
Unfortunately, this isn't the order that any of the lifecycle hooks trigger. E.g, if we had a console.log('didInsertElement');
in each didInsertElement
hook, we would see the following order (demo in ember-twiddle):
didInsertElement <-- tile-layer
didInsertElement <-- marker-layer
didInsertElement <-- leaflet-map
The render hooks start being called on children, which is not compatible with our 3rd party API logic.
Also, we need to use didInsertElement
at least on leaflet-map
because we need to make sure an element is available to create a map (L.map(this.element)
). This is a very common pattern, not only with leaflet.
Likewise, the destroy lifecycle hooks are not called in the desired order.
ember-composability-tools fixes this problem providing components new render and destroy hooks that trigger in our desired order. Those hooks are called didInsertParent
and willDestroyParent
. You just need to include ChildMixin
on child components and ParentMixin
on parent components to have access to them. Example:
import Ember from 'ember';
import { ParentMixin } from 'ember-composability-tools';
const { Component } = Ember;
export default Component.extend(ParentMixin, {
didInsertParent() {
this._super(...arguments);
// The topmost parent hook call.
// Here we have a `this.element` available and
// we are certain that none of the children's
// `didInsertParent` hooks were called
},
willDestroyParent() {
this._super(...arguments);
// the reverse is applied here.
// We are certain that this call will take place
// when all of the children's `willDestroyParent`
// were called.
}
});
The same hooks are available when using ChildMixin
.
Note that a component can be a child and a parent at the same time. e.g marker-layer
is a child to leaflet-map
but a parent to popup-layer
. In that case just include both mixins. They are compatible.
While composing components like we say in our previous ember-leaflet example, we often need to access parent/child components. E.g, when we write:
we want the child tile-layer to be added to the parent map instance. In other cases one might need to access children directly from the parent.
With ember-composability-tools we can essentially do:
// tile-layer example implementation
L.tileLayer(this.get('url')).addTo(this.get('parentComponent')._mapInstance);
parentComponent
is available on any component that includes the ChildMixin
and is used inside the block of a component that includes the ParentMixin
.
Likewise, the childComponents
property is available on a component that includes the ParentMixin
:
// invoke draw on all child components
this.get('childComponents').invoke('draw');
The third problem ember-composability-tools aims to solve is the problem of getting the contents of a block as a DOM you can pass in to a 3rd party library. If you take a closer look, in our previous example we had:
But how will that Hello World!
ends up in a leaflet popup? Using the leaflet API, we quickly that problem:
L.marker([this.get('lat'), this.get('lng')])
.bindPopup(?) // how do we get the contents of the block of the current component?
.addTo(this.get('parentComponent')._mapInstance);
At first sight one might ask "Why not just this.$()
or this.element
?". The problem in doing this is that the contents would still be rendered to the DOM in the document, like normally. Remember that at this stage we're in a DOM zone that "isn't ours". It belongs to the leaflet map and we're not sure how leaflet treats the DOM here, so it might not be safe to change the DOM here. We should only "render" leaflet layers by now.
ember-composability-tools solves this problem by rendering the component's block to an element created by the component itself (using document.createElement()
).
To use this functionality you just need to include the RenderBlockMixin
. By default, the RenderBlockMixin
doesn't render anything to the destinationElement
. This way you don't render anything you don't want to. You control when to render by setting shouldRender
to true
or false
.
Example:
import Ember from 'ember';
import { RenderBlockMixin } from 'ember-composability-tools';
const { Component } = Ember;
export default Component.extend(RenderBlockMixin, {
// use `this.get('destinationElement')` in the component context
// in this case we decided to always render the block
// so we set shouldRender to true at init time.
shouldRender: true
});
git clone <repository-url>
this repositorycd ember-composability-tools
npm install
bower install
ember serve
- Visit your app at http://localhost:4200.
npm test
(Runsember try:each
to test your addon against multiple Ember versions)ember test
ember test --server
ember build
For more information on using ember-cli, visit http://ember-cli.com/.