Dynamically set properties on a view from Handlebars - ember.js

I'm trying to DRY up my templates by creating views for common layout elements. I have the following views defined
App.SplitListView = Em.View.extend({
tagName: 'div',
classNames: [ 'panel', 'panel-default' ]
});
App.SplitListHeaderView = Em.View.extend({
classNames: [ 'panel-heading' ],
templateName: 'split-list-header-view-layout'
});
The template for the SplitListView is a simple {{yield}}
The template for the SplitListHeaderView is
<span class="panel-title">{{view.headerText}}</span>
{{#if view.showCreateButton}}
<span class="pull-right">
<button type="button" class="btn btn-primary btn-sm">
<i class="fa fa-plus fa-lg" style="padding-right: 10px;"></i>{{view.createButtonText}}
</button>
</span>
{{/if}}
Then the template for the submodule:
{{#view App.SplitListView}}
{{view App.SplitListHeaderView headerTextBinding="Sandwiches" showCreateButtonBinding=true createButtonTextBinding="Make me a sandwich!"}}
{{/view}}
The desired end result is that I'd like to use the SplitListView and SplitListHeaderView everywhere in my app that I use that layout and just set the relevant bits of text via the controller. But so far it's just not working. I feel like this should be easy to do and I'm just missing something. I found this question which looks to be along the same lines as my question but the solution did not work for me.
Does anyone have any experience with something like this or am I off my rocker in trying to use views in this manner?

I believe you have three options here:
1) Use a component instead of a view. Components are reusable, self-contained 'modules' that allow you to bind properties from the component's context in exactly the same manner as you are trying with your view.
2) If the only thing you're reusing between the current non-DRY templates is the html/handlebars you should use a {{partial}} instead of a view because that doesn't create any scope within the view hierarchy and will allow you to bind the handlebars in the partial directly to route's view/controller properties without specifying additional property bindings or scope.
3) Use a Em.Handlebars.helper to accept three arguments (headerText, buttonText, and showCreateButton) and return a new Handlebars.SafeString('some html string'); or something along those lines.
A solution
If I were you, I would utilize methods 2 and 3 together as follows:
First, use a helper (I'm using a helper with a globally accessible method) instead of App.SplitListView to wrap some html around the buffer (i.e. content) inside of the opening and closing handlebars:
// Handy method you can use across many different areas of the app
// to wrap content in simple html
Utils.wrapBuffer = function(open, close, options, context) {
options.data.buffer.push('\n' + open);
if (options.fn(context)) {
options.data.buffer.push('\n' + options.fn(context));
}
options.data.buffer.push('\n' + close);
}
Then the actual helper
App.Handlebars.helper('split-list', function(options) {
var open = '<div class="panel panel-default">;
var close = '</div>';
Utils.wrapBuffer(open, close, options, this);
});
Now, your template will look like:
{{#split-list}}
// This is where our content will go
{{/split-list}}
This has the advantage of wrapping what is inbetween the handlebars opening and closing tags with html without adding or changing scope. Thus, property bindings will work seamlessly.
Now, replace App.SplitListheaderView with a component set up in a similar manner:
App.SplitListHeaderComponent = Em.Component.extend({
classNames: ['panel-heading'],
headerText: null,
createButtonText: null,
showCreateButton: false,
});
You layout (components use layouts, not templates) will be located at templates/components/split-list-header.hbs. it will look as follows:
<span class="panel-title">{{headerText}}</span>
{{#if showCreateButton}}
<span class="pull-right">
<button type="button" class="btn btn-primary btn-sm">
<i class="fa fa-plus fa-lg" style="padding-right: 10px;"></i>{{createButtonText}}
</button>
</span>
{{/if}}
Note, that properties are something not view.something and each property is declared allowing it to be overwritten in the handlebars helper. Now your submodule's template will look like:
{{#split-list}}
{{split-list-header
headerText='Sandwiches'
showCreateButton=true
createButtonText='Make me a sandwich!'
}}
{{/split-list}}
If you wanted, you could bing these properties to a property on your controller or view instead of writing them in the template every time.
Another Improvement
You could go one step further and scrap the wrapping helper all together because it's not doing anything except adding HTML. In this case, the component would look like {{split-list headerText=blah...}} and would have the following template:
<div class="panel-heading">
<span class="panel-title">{{view.headerText}}</span>
{{#if view.showCreateButton}}
<span class="pull-right">
<button type="button" class="btn btn-primary btn-sm">
<i class="fa fa-plus fa-lg" style="padding-right: 10px;"></i>{{view.createButtonText}}
</button>
</span>
{{/if}}
</div>

Related

Stop Ember helper rendering inside div

I have an Ember helper which is literally defined as this:
<span class="glyphicon glyphicon-info-sign" data-toggle="tooltip" data-placement="right" data-title="{{text}}"></span>
It's used to render a Bootstrap tooltip. It renders the following HTML:
<div id="ember572" class="ember-view">
<span class="glyphicon glyphicon-info-sign" data-toggle="tooltip" data-placement="right"
data-title="<script id='metamorph-35-start' type='text/x-placeholder'></script>
When a project is archived, no new items can be created in it.
<script id='metamorph-35-end' type='text/x-placeholder'></script>"
data-original-title="" title="" aria-describedby="tooltip416856">
</span>
</div>
How can I stop the helper rendering the containing div?
Create a Component with tagName: 'span' instead of <span> in it's template:
Component:
App.TooltipElementComponent = Em.Component.extend({
tagName: 'span',
attributeBindings: ['data-toggle', 'data-placement', 'data-title']
});
Now, you can use following:
{{tooltip-element data-toggle='tooltip' data-placement='right' data-title='When a project is archived, no new items can be created in it.'}}
Which produces following HTML:
<span id="ember257" class="ember-view" data-toggle="tooltip" data-placement="right" data-title="When a project is archived, no new items can be created in it."></span>
Notice you can still use bindings(data-title=text):
{{tooltip-element data-toggle='tooltip' data-placement='right' data-title=text}}
Working demo.

Ember markup breaks bootstrap CSS

I am generating a button group with a template:
<div class="btn-group">
{{#if allowNew}}
{{#link-to newRoute type="button" class="btn btn-primary"}}<i class="fa fa-plus"></i> {{t generic.add}} {{capitalize singularHuman}}{{/link-to}}
{{/if}}
{{#if allowDeleteAll}}
<button type="button" {{action "destroyAllRecords"}} class="btn btn-danger"><i class="fa fa-trash-o fa-lg"></i> {{t generic.delete_all}} {{capitalize pluralHuman}}</button>
{{/if}}
</div>
Ember is placing <script> nodes inside the button group, I imagine to handle binding, view updates or whatever.
The problem is that bootstrap is relying on CSS rules like
.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle)
to finetune the styles. Since the <script> tag is placed before the first <button> or <a> tag, and after the last one, those CSS rules are getting applied where they should not, and instead of having something like this:
I get something like this:
As you can see the stock bootstrap style has rounded corners for the first and last button (the external corners), but my implementation has no such rounded corners.
Is it possible to somehow overcome this problem?
Use an ember view. See https://coderwall.com/p/f1npbg
App.MyView = Ember.CollectionView.extend({
classNames: ['btn-group'],
itemViewClass: Ember.View.extend({
template: Ember.Handlebars.compile("{{view.content.name}}"),
tagName: 'button',
classNames: ['btn']
}),
attributeBindings: ['data-toggle'],
'data-toggle': 'buttons-radio'
});

Ember view with dynamic class names

Considering the following:
Parent template:
{{view App.SomeView id="42" panelClass="default"}}
View template:
<div class="col-md-3 col-sm-6">
<div class="panel panel-{{panelClass}}">
<div class="panel-heading">
<h3 class="panel-title">
{{name}}
</h3>
</div>
<div class="panel-body">
{{description}}
</div>
</div>
</div>
View JS:
App.SomeView = Ember.View.extend({
templateName: 'views/some-view'
});
How can I achieve output HTML where the panel class gets set properly? At the moment it doesn't work because it wants to bind, so it inserts the ember metamorph script tags, instead of just plain text for the panel class.
Also, the template is wrapped in an extra div. How would I modify it so that the ember-view wrapping div is actually the first div in the template (the one with col-md-3 col-sm-6)?
The bind-attr helper exists for that reason. Here's the guide entry.
<div {{bind-attr class=":panel panelClass"}}></div>
Also, not sure if you can use a prefix on panelClass in the template. If might be easier just to use a computed property to add the panel- beforehand.
I'm sorry, I didn't see your second question about the extra div. The guide explains here how to extend the element.
App.SomeView = Ember.View.extend({
classNames: ['col-md-3', 'col-sm-6']
});

EmberJS multiple yield helper

I have a custom view that I've created in Ember. I really love the {{yield}} helper to allow me to control the 'bread' of the sandwich. However, what I'd like to do now, is create a 'double decker' sandwich, and have a view with more than 1 yield in it, or at the very least be able to parameterize which template to use in the 2nd yield.
so for example:
layout.hbs
<div>
<div class="header">Header Content</div>
<div class="tab1">
Tab 1 Controls.
<input type="text" id="common1" />
{{yield}}
</div>
<div class="tab2">
Tab 2 Controls.
<input type="text" id="common2" />
{{yield second-template}} or {{template second-template}}
</div>
</div>
app.js
App.MyDoubleDeckerView = Ember.View.extend({
layoutName:"layout',
templateName:"defaultTemplate",
"second-template":"defaultSecond"
});
App.MyExtendedDoubleDecker = App.MyDoubleDeckerView({
templateName:"myTemplate",
"second-template":"mySecondTemplate"
});
is there any way of doing something like this? What I love about the views in ember is the ability to centralize & extend views which allows me to keep the things that are common among all the views in one place...
As of Ember 3.25 you can use so called "named blocks" (see the Passing multiple blocks subsection of https://api.emberjs.com/ember/release/modules/#glimmer%2Fcomponent).
Example component:
<h1>{{yield to="title"}}</h1>
{{yield}}
and then use it like this:
<PersonProfile #person={{this.currentUser}}>
<:title>{{this.currentUser.name}}</:title>
<:default>{{this.currentUser.siganture}}</:default>
</PersonProfile>
I think you should use named outlets for this
http://emberjs.com/guides/routing/rendering-a-template/
Something like this should work:
layout.hbs
<div>
<div class="header">Header Content</div>
<div class="tab1">
Tab 1 Controls.
<input type="text" id="common1" />
{{yield}}
</div>
<div class="tab2">
Tab 2 Controls.
<input type="text" id="common2" />
{{view "view.secondView"}}
</div>
</div>
app.js
App.MyDoubleDeckerView = Ember.View.extend({
layoutName:"layout',
templateName:"defaultTemplate",
secondView: Ember.view.extend({
templateName: "defaultSecond"
})
});
App.MyExtendedDoubleDecker = App.MyDoubleDeckerView({
templateName:"myTemplate",
secondView: Ember.view.extend({
templateName: "mySecondTemplate"
});
});
In other words, invoke a view given by view.secondView from within your template. Then, set the secondView property in your class or subclass.
You could add a bit of syntactic sugar with
App.viewForTemplateName = function(templateName) {
return Ember.View.extend({
templateName: templateName
});
};
Then, in your view definitions above, do
secondView: App.viewForTemplateName('mySecondTemplate')

Ember.js: Toggle Nested Views

I have a header with some login/signup forms that popup when you click the respective buttons.
While it was working fine using just jQuery, I've now started to integrate Ember into the application and I'm running into some trouble with some simple toggle functionality.
Here's the basic HTML markup:
<header>
<h1>Page Title<h1>
<nav>
<a id="toggles-login" class="button {{active_class_binding}}">Login</a>
<a id="toggles-signup" class="button {{active_class_binding}}">Signup</a>
</nav>
<div id="popup-forms">
<div id="login-form"></div>
<div id="signup-form"></div>
</div>
<header>
I'm completely new to Ember and I really have no idea how to set this up. The only thing I want is to be able to set the popup forms up as Ember.View objects and toggle them with some action helpers.
I really am lost on this one.
A simple solution would be to trigger simple actions to show the respective forms:
<a id="toggles-login" class="button {{active_class_binding}}" {{action showLoginForm target="view"}}>Login</a>
<a id="toggles-signup" class="button {{active_class_binding}}" {{action showSignupForm target="view"}}>Signup</a>
The corresponding view would have to implement both actions:
App.YourView = Ember.View.extend({
showLoginForm : function(){
this.$("#login-form").toggle();
},
showSignupForm : function(){
this.$("#signup-form").toggle();
}
});