Best practice for using bound Ember templates along with Bootstrap components


#1

A few (popvers, tooltips) Bootstrap modules rely on passing strings of static HTML to render content, not allowing for bound templates to be passed around.

There are a few workarounds out there, but they all feel very hackish. Does anyone have a recommended approach to this problem ?


#2

Well https://github.com/ember-addons/bootstrap-for-ember is the place to start. And for your specific asks about popovers and tooltips you want to use components and the {{yeild}} helper. This allows your component to wrap around template content and add popover functionality to it.

I actually wrote a popover component and kept meaning to submit a request to the repo. It’s something like this:

App.BsPopoverComponent = Ember.Component.extend({
direction: 'right',
isPopoverVisible: false,
spacer: 15,
title: "Popover Title",
top: 200,
left: 100,

inlineStyle: function () {
	return "top: %@px; left: %@px; display: block;".fmt(this.get("top"), this.get("left"));
}.property("top", "left"),

click: function () {
	Ember.Logger.log("clicked: %@".fmt(this.get("title")));
	this.toggleProperty("isPopoverVisible");
	Ember.Logger.log("isPopoverVisible: ", this.get("isPopoverVisible"));
},

didInsertElement: function () {
	Ember.run.scheduleOnce('afterRender', this, 'processYieldedContent');
},

processYieldedContent: function () {
	var direction = this.get("direction");
	var yieldedContent = this.$("div.popover ~ *");
	var contentWidth = yieldedContent.width();
	var contentHeight = yieldedContent.height();
	var contentPosition = yieldedContent.position();

	var popover = this.$(".popover");
	var popoverWidth = popover.width();
	var popoverHeight = popover.height();

	var spacer = this.get("spacer");

	var positionFunctionMapping = {
		'right': this.getPositionRight,
		'left': this.getPositionLeft,
		'top': this.getPositionRight,
		'bottom': this.getPositionRight
	};

	var popoverPosition = positionFunctionMapping[direction](
		contentWidth,
		contentHeight,
		contentPosition,
		popoverWidth,
		popoverHeight,
		spacer
	);

	this.set("top", popoverPosition.top);
	this.set("left", popoverPosition.left);

},

getPositionRight: function(contentWidth, contentHeight, contentPosition, popoverWidth, popoverHeight, spacer) {
	var left = contentPosition.left + contentWidth + spacer;
	var top = contentPosition.top + Math.round((contentHeight/2) - (popoverHeight/2));

	var absPosition = {
		top: top,
		left: left
	};

	Ember.Logger.log("right position: ", absPosition);
	return absPosition;
},

getPositionLeft: function(contentWidth, contentHeight, contentPosition, popoverWidth, popoverHeight, spacer) {
	var left = contentPosition.left - spacer - popoverWidth;
	var top = contentPosition.top + Math.round((contentHeight/2) - (popoverHeight/2));

	var absPosition = {
		top: top,
		left: left
	};

	Ember.Logger.log("right position: ", absPosition);
	return absPosition;
}});

and the template could look like:

    <div {{bind-attr class=":popover :fade direction isPopoverVisible:in:out" style="inlineStyle"}}>
	<div class="arrow"></div>
	<h3 class="popover-title">{{title}}</h3>
	<div class="popover-content">{{{content}}}</div>
</div>{{yield}}

Which is essentially yields the original content of the parent view adjacent to the popover in the DOM, and the pop over is just absolutely position in reference to it with inline styles.

And finally you have the declaration of the popover like so:

   {{#bs-popover
title="Title Of Popover"
content="Content on first line of popover<br />second line..."
}}
<button type="button" class="btn btn-default btn-xs" title="">
<span class="glyphicon glyphicon-globe"></span>
</button>
{{/bs-popover}}

And of course you would click the button to toggle the popover. The only problem I found with this method is that since each popover is independent, they get a z-index based on the position in the DOM and if two popups are close to one another the second on will always go on top even if you clicked it second, where as if there was a centralized popover manager, it could do cool things like close all the popovers, ensure the last clicked popover is a highest level of z index, etc… but this should at least be a start.


#3

This is nice and I’ll probably end up using something like this. The problem with it is that it is a re-implementation of Bootstrap’s module - which has its niceties (the width and height don’t need to be manually set for instance). There’s no way to reuse the existing lib? That’s one big hurdle to jump through.


#4

on a related issue, I’m trying to create a custom component that encapsulates Bootstrap panel. something like

<div class='panel panel-default'>
  <div class="panel-heading">
    <h3 class="panel-title">{{title}}</h3>
  </div>
  <div class="panel-body">
    {{yield}}
  </div>
  <div class="panel-footer">
    {{yield}}
  </div>
</div>

Since components don’t allow multiple yields, afaik, I’m stuck. Am I going about this wrong?


#5

I don’t really see a compelling use-case for panels. The reason we have to create components in ember for some of the bootstrap UI elements is because we want ember to manipulate the DOM with it’s own run loop and managed view hierarchy and we need a way to allow ember to be aware of anything dynamic that would normally happen via bootstrap.js

Since panels are static content already, you don’t need to use a component. The only advantage is getting to make panels with less markup.

If you did want this though, I think a better approach than yeild would be to use the block {{#view}} … {{/view}} wrapper with a layout specified, and inside the panel-default view, you could have sub view for the footer.


#6

Ah, I see what you’re doing with components in your case. And yes, I’m simply trying to clean up repeated markup. I’ll have to try your suggestion with a view wrapper. For now, I was able to get things working with named outlets and renderTemplate in the route:

renderTemplate: function() {
this._super(this, arguments);
// render panel before inserting into it
this.render('panel');
this.render('users_list', {
  outlet: 'body',
  into: 'panel'
});
this.render('users_footer', {
  outlet: 'footer',
  into: 'panel'
});
}

for anyone googling


#7

Yea… I would not recommend this. You started out trying to save 7 lines of html, and ended up adding 12 lines of router configuration which is not semantically correct and is confusing for anyone not familiar with what your trying to do. Ember is built on the foundation of convention over configuration and if you’re putting routes/outlets into templates simply to control where content is rendered instead of actually changing based on the current URI it breaks conventions.

Getting back to the popover problem, the original guy raised a good point about trying to re-use the logic from within the bootstrap library. I think that is a good idea for code maintenance and for consistency. If it’s possible to abstract what you need from the bootstrap js then I would do it. Other wise sometimes you just have to re-write your own.


#8

Thanks for the review. Agreed - I’m not crazy about the route not matching the template. fwiw, since I can’t find anything better in the Ember Guides, i used the examples here http://blog.safaribooksonline.com/2013/09/10/ember-js-outlets/

I guess I’m still not seeing a decent option for my use case, and perhaps I’m just thinking about this wrong. I simply want to DRY up repeated html code by having a reuseable layout with 3 variable sections in it. Seems easy, right?

Thanks again.