I'm looking at the Angry Cats Backbone/Marionette tutorial posts here
http://davidsulc.com/blog/2012/04/15/a-simple-backbone-marionette-tutorial/
http://davidsulc.com/blog/2012/04/22/a-simple-backbone-marionette-tutorial-part-2/
and I came upon the same question/need posted here:
Backbone.js turning off wrap by div in render
But I can only get that to work for Backbone.Views, not Backbone.Marionette.ItemViews.
For example, from the simple backbone marionette tutorial links above, take AngryCatView:
AngryCatView = Backbone.Marionette.ItemView.extend({
template: "#angry_cat-template",
tagName: 'tr',
className: 'angry_cat',
...
});
The template, #angry_cat-template, looks like this:
<script type="text/template" id="angry_cat-template">
<td><%= rank %></td>
<td><%= votes %></td>
<td><%= name %></td>
...
</script>
What I don't like, is that the AngryCatView needs to have
tagName: 'tr',
className: 'angry_cat',
-- if I take tagName out, then angry_cat-template gets wrapped by a <div>.
What I would like is to specify the HTML in one place (the angry_cat-template) and not have most HTML (all the <td> tags) in angry_cat-template and a little HTML (the <tr> tag) in AngryCatView. I would like to write this in angry_cat-template:
<script type="text/template" id="angry_cat-template">
<tr class="angry_cat">
<td><%= rank %></td>
<td><%= votes %></td>
<td><%= name %></td>
...
</tr>
</script>
It just feels cleaner to me but I've been mucking around with Derik Bailey's answer in "Backbone.js turning off wrap by div in render" and can't get it to work for Backbone.Marionette.
Any ideas?
2014/02/18 — updated to accommodate the improvements noted by #vaughan and #Thom-Nichols in the comments
In many of my itemView/layouts I do this:
var Layout = Backbone.Marionette.Layout.extend({
...
onRender: function () {
// Get rid of that pesky wrapping-div.
// Assumes 1 child element present in template.
this.$el = this.$el.children();
// Unwrap the element to prevent infinitely
// nesting elements during re-render.
this.$el.unwrap();
this.setElement(this.$el);
}
...
});
The above code only works when the wrapper div contains a single element, which is how I design my templates.
In your case .children() will return <tr class="angry_cat">, so this should work perfect.
I agree, it does keep the templates much cleaner.
One thing to note:
This technique does not force only 1 child element. It blindly grabs .children() so if you've incorrectly built the template to return more than one element, like the first template example with 3 <td> elements, it won't work well.
It requires your template to return a single element, as you have in the second template with the root <tr> element.
Of course it could be written to test for this if need be.
Here is a working example for the curious: http://codepen.io/somethingkindawierd/pen/txnpE
While I'm sure there's a way to hack the internals of render to get it to behave the way you'd like, taking this approach means you'll be fighting the conventions of Backbone and Marionette through the whole development process. ItemView needs to have an associated $el, and by convention, it's a div unless you specify a tagName.
I empathize -- especially in the case of Layouts and Regions, it appears to be impossible to stop Backbone from generating an extra element. I'd recommend accepting the convention while you learn the rest of the framework and only then deciding if it's worth hacking render to behave differently (or to just choose a different framework).
This solution works for re-rendering. You need to override render.
onRender tricks won't work for re-render. They will cause nesting on every re-render.
BM.ItemView::render = ->
#isClosed = false
#triggerMethod "before:render", this
#triggerMethod "item:before:render", this
data = #serializeData()
data = #mixinTemplateHelpers(data)
template = #getTemplate()
html = Marionette.Renderer.render(template, data)
##$el.html html
$newEl = $ html
#$el.replaceWith $newEl
#setElement $newEl
#bindUIElements()
#triggerMethod "render", this
#triggerMethod "item:rendered", this
this
Wouldn't it be cleaner to use vanilla JS instead of jQuery to accomplish this?
var Layout = Backbone.Marionette.LayoutView.extend({
...
onRender: function () {
this.setElement(this.el.innerHTML);
}
...
});
For IE9+ you could just use firstElementChild and childElementCount:
var Layout = Backbone.Marionette.LayoutView.extend({
...
onRender: function () {
if (this.el.childElementCount == 1) {
this.setElement(this.el.firstElementChild);
}
}
...
});
There is a good reason why Marionette automatically inserts the wrapper DIV. It's only when your template consists of just one element when you can drop it. Hence the test for number of child elements.
Another option is to use the attachElContent method present in every Marionette view. Its default implementation means re-renders of the view will overwrite the root element's inner HTML. This ultimately gives rise to the infinite nesting mentioned in bejonbee's answer.
If you would rather not overwrite onRender and/or require a pure-JS solution, the following code might be just what you want:
var Layout = Backbone.Marionette.LayoutView.extend({
...
attachElContent: function (html) {
var parentEl = this.el.parentElement;
var oldEl;
//View already attached to the DOM => re-render case => prevents
//recursive nesting by considering template's top element as the
//view's when re-rendering
if (parentEl) {
oldEl = this.el;
this.setElement(html); //gets new element from parsed html
parentEl.replaceChild(this.el, oldEl); //updates the dom with the new element
return this;
//View hasn't been attached to the DOM yet => first render
// => gets rid of wrapper DIV if only one child
} else {
Marionette.ItemView.prototype.attachElContent.call(this, html);
if (this.el.childElementCount == 1) {
this.setElement(this.el.firstElementChild);
}
return this;
}
}
...
});
Note that for re-rendering to work, the code assumes a template with a single child that contains all markup.
Related
I'm trying to render a partial and rerender elements of my page with updated elements from said partial. My js with erb as of right now looks like this (which I'm not happy with):
var showPage = '<%= j render partial: "orders/show-price", locals: {variant: #variant, line_item: #line_item} %>'
$('[data-unit-price]').html($(showPage)[0])
$('[data-total-price]').html($(showPage)[2])
showPage returns a string of HTML:
<p data-unit-price="">Unit Price: 11.86</p>
<p data-total-price="">Total: 118.60</p>
And $(showPage) returns T.fn.init(3) [p, text, p]
Ordinarily, I'd just do something like:
$('[data-unit-price]').html($('[data-unit-price]', showPage))
which in fact exists similarly elsewhere in my code:
var sidebar = '<%= j render partial: "layouts/sidebar" %>'
if($('[data-cart-link]').length){
$('[data-cart-link]').html($('[data-cart-link]', sidebar))
} else {
$('[data-cart-link-prepend-target]').prepend($('[data-cart-link]', sidebar))
}
where sidebar also returns an html string. The above example works as desired in that it takes updated information from a rendered partial and rerenders that element in the DOM.
What am I doing wrong here? What is different about the two situations? I want to be able to find my elements by attribute rather than by array index, but I'm not having any luck.
Thanks very much.
The issue is because when you provide a selector a context to search within, as you are in this line: $('[data-cart-link]', sidebar), it uses find() internally. This will not work for the HTML you have stored in showPage because there is no parent element to find() within, hence nothing is returned.
The solution to this problem is to instead use filter() to search through all elements in the current collection:
var showPage = `<p data-unit-price="">Unit Price: 11.86</p>
<p data-total-price="">Total: 118.60</p>`;
var $showPage = $(showPage);
$('[data-unit-price]').append($showPage.filter('[data-unit-price]'));
$('[data-total-price]').append($showPage.filter('[data-total-price]'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div data-unit-price></div>
<div data-total-price></div>
Also note the use of append() here, as you're providing a jQuery object to the method, not a string.
By alter I mean changing color/font size/ etc.
So this is the action in my controller
actions {
changeColor: function() {
this.$().css('background-color', '#f1f1f2'); //error!
}
}
<span {{action 'changeColor' this}} > </span>
this.$() is causing an error. What should I use instead?
The action must be in the controller.
When you say {{action 'someAction' this}} this from within the template in this sense will refer to the model/content of the template not the DOM element of the action.
Here is another suggestion. I am not sure if this is best practice.
Lets say we are working within the index template and have a span with a action:
//index template
<span>Change my background</span>
Now the index template has a view that is backing it called the IndexView. Within that View we can use one of the DOM events. in the code below we use the click Dom event which will send along an event with which we can use to the grab the target and then send to controller.
//App.IndexView
click: function(evt) {
//get the evt target
var target = evt.target
//send a action the controller
this.get('controller').send('handleBackgroundChange', target);
}
Now in the indexController:
//indexController
actions: {
handleBackgroundChange: function(target) {
$(target).css({'background': 'green'});
}
}
Again this works but I am not sure it is a good idea.
If you want to do that change with jQuery, you can do something like the following:
// You'd have to know the selector of that view or the css class wrapping it
// in order to do this in the controller, since the "this"
// is not a pointer to your view at this point (I think)
Ember.$('selector goes here selector').css({'color': '#00F'});
It would be much easier form a view instead of a controller, since you could do things like (note the this keyword):
this.$().css('color': '#0F0');
This code would work in a view with the this keyword and without the need for a selector, since it is a pointer to the view at that moment. So this.$() in a view would essentially mean $("#ember416") where "ember416" is the id Ember assigned to that view.
Another way you can do this would be through a View or Component implementation so you could use attributeBindings for style, and have a lot more control over what is going on. You can apply rules to specific css properties based on the business reqs and take advantage of the binding to always recompute the style. With this approach you don't need to use $.css() to change the styles, as it gets built and applied for you.
So... say you create a style property watches other properties (e.g. color, border, size, etc) within your class and ultimately compose the style string. This way you can program each property and then bind the result of a given combination directly to your view via attributeBindings. Example:
// JS
App = Em.Application.create();
App.LeToggleButtonComponent = Ember.Component.extend({
// I want an input
tagName: 'input',
// of the type button
type: 'button',
// that has a `clicked` flag
clicked: false,
// When I change the `clicked` flag, I want my to force any
// properties depending on `clicked` to re-evaluate their values
changeColor: function() {
this.toggleProperty('clicked');
}.on('click'),
// Then I want the color attribute to re-evaluate itself
// based on the `clicked` property, switching it from red to blue
color: function() {
return "color: %#;".fmt((this.get('clicked')) ? "#00F" : "#F00");
}.property('clicked'),
// And I also want the border attribute to re-evaluate itself
// based on the `clicked` property, switching it from red to blue
border: function() {
return "border: %#;".fmt((this.get('clicked')) ? "1px solid #00F" : "1px solid #F00");
}.property('clicked'),
// Then I want to combine all css related properties
// into a single string, composing the style of this component
style: function() {
return '%#1%#2'.fmt(this.get('border'), this.get('color'));
}.property('border', 'color')
// finally, I want to bind some attributes to the view
// so the styles get updated automatically when
// any of them change. The real important one, is the `style` prop
attributeBindings: ['type', 'value', 'style'],
});
// Handlebars
<script type="text/x-handlebars" data-template-name='index'>
{{le-toggle-button value='Click Here'}}
</script>
<script type="text/x-handlebars" data-template-name='application'>
<h2>button test</h2>
{{outlet}}
</script>
(see jsbin)
An argument can be made (and I'd agree) that for the majority of the scenarios, one should use classes instead of inline styles. This proposed approach would be good when you have very specific rules and/or complex style combinations that could potentially create duplicated/hard-to-maintain css classes.
Update
I wanted to satisfy my own curiosity on how to send and handle css changes through a controller or route and went on reading. I came up with a second solution that allows for changes made directly in the controller or route:
// JS
App = Ember.Application.create();
App.Router.map(function() {
this.route('other');
});
App.IndexRoute = Em.Route.extend({
actions: {
changeColor: function(selector) {
console.log("Index > Ember.$('%#')".fmt(selector), Ember.$(selector));
// now I can simply call jQuery to
// change the style of the view in
// this particular route
Ember.$(selector).css({
'color': '#F00',
'font-weight': 'bold'
});
}
}
});
App.OtherRoute = Em.Route.extend({
actions: {
changeColor: function(selector) {
console.log("Other > Ember.$('%#')".fmt(selector), Ember.$(selector));
// or on this one
Ember.$(selector).css({
'height': '80px'
});
}
}
});
App.LeOtherButtonComponent = Ember.Component.extend({
tagName: 'input',
type: 'button',
target: Ember.computed.alias('route'),
// within the action handler, you can
// call this.triggerAction, passing the
// action name, context and target.
// in this case, i didn't pass the target
// with the hash, but left as the default
// target of the view, and also changed
// it's default value to the route just
// as an example
changeColor: function(e) {
var selector = '#%#'.fmt(this.get('elementId'));
this.triggerAction({
action: 'changeColor',
actionContext: selector
});
}.on('click'),
attributeBindings: ['type', 'value' ]
});
// Markup
<script type="text/x-handlebars" data-template-name='index'>
{{le-other-button value='Click Here - Index'}}
</script>
<script type="text/x-handlebars" data-template-name='other'>
{{le-other-button value='Click Here - Other'}}
</script>
<script type="text/x-handlebars" data-template-name='application'>
<div class="more">
{{#link-to 'index'}}Index{{/link-to}} |
{{#link-to 'other'}}Other{{/link-to}}
</div>
{{outlet}}
</script>
(see jsbin)
I don't really know if I like this solution because it's really easy to go out of sync with inline styles versus whatever state your view/component is supposed to be in and look like at that point. The approach on the first hypothetical component is good because you can tell exactly what is going to change, without the need of using jQuery. On the other hand, you have to create a computed property and add it as a dependency of the style each time you want to add a new style attribute in that string, making the 2nd hypothetical component look more flexible, allowing each controller/route to implement its own changeColor, and allowing you to inject an animation or another type of operation in that component or view. But I think it sorta leaves it open for imperative style changes (not tied to any logic/rule, which can become difficult to maintain).
I'm sure there's a better way to do this :P
The best approach is to bind the DOM node's class attribute to a controller property, like this:
<div {{bind-attr 'class' myClass}}>blah blah</div>
and in your action:
actions: {
changeColor: function() {
this.set('myClass', 'someCssClass');
}
}
If you'd rather deal with styles directly (not recommended), then you could do the same thing with the style attribute binding.
Hello dear SO users and contributers. I'm unsure on how to manipulate compositeViews using the JS framework MarionetteJS based on BackboneJS. Hopefully someone will be able to give me some advice on how to continue.
The application context: The application I'm building is a GRID that can accept draggable items. This grid is being build using two CompositeView's to build the rows within the page section and an ItemView for each field that's actually represented on the website.
The application views: I posses a gridView that has a collection of gridModels which get split over multiple rows in the initialize method. This generates a section in the HTML that will hold the GRID. The gridRowView will generate a div with the class 'day' for each row within the gridView build from all of the gridItems.
It has been mentioned that in a previous question relating my GRID that a treeView is probably more suitable for the purposes. But I feel this does not concern the scope of my question.
planboard.gridItemView = Backbone.Marionette.ItemView.extend({
model: planboard.gridModel,
collection: planboard.gridCollection,
template: "#grid-item-template",
onShow: function() {
//code to plan dropped orders
}
});
planboard.gridRowView = Backbone.Marionette.CompositeView.extend({
template: "#grid-row-template",
itemView: planboard.gridItemView,
itemViewContainer: "div.day",
initialize: function() {
this.collection = new Backbone.Collection(_.toArray(this.model.attributes));
}
});
planboard.gridView = Backbone.Marionette.CompositeView.extend({
template: "#grid-template",
itemView: planboard.gridRowView,
tagName: "section",
initialize: function() {
var grid = this.collection.groupBy(function(list, iterator) {
return Math.floor(iterator / collums); // 4 == number of columns
});
this.collection = new Backbone.Collection(_.toArray(grid));
}
});
The application templates:
<!-- Grid templates -->
<script type="text/template" id="grid-item-template">
<div class="gridItem droppable" id="{{cid}}"><b>{{data}}</b></div>
</script>
<script type="text/template" id="grid-row-template">
<div class="day"></div>
</script>
<script type="text/template" id="grid-template">
<section></section>
</script>
The problem: The code below (minus the event wrapper) resembles code as structured in the DOM. As you can see the views generate the expected result. I am however in a spot where I need to use positioning to place items on the GRID. To do this I've taken the start of div container around the section (the start of the GRID).
Although this works, this becomes a problem once scroll-bars are introduced. Since the start position of the GRID does not actualy change you can scroll down but the orders will always remain on the same position off the screen.
Possible solution: Watching the Google Calendar I've realized that they use an additional event-wrapper inside of their rows for positioning. This seems logical to me since then the offset to determine the position from would no longer be the start of your GRID, but rather the start of row (class 'day'). This should solve the problem that scroll-bars could pose.
In my mind this would look something like the code below:
<section>
<div> <!-- wrapper generated by framework -->
<div class="day">
<div class="event-x"> <!-- used as offset for positioning -->
<div id="c39" class="gridItem droppable"> <!-- grid column --></div>
</div>
</div>
<div>
</section>
My question: Since my row (the div with class 'day') is automatically generated by the MarionetteJS CompsositeView I am unable to give this some unique value to identify it with that does not change. Having an additional event wrapper with a unique identifier would most likely solve my problem, since then I could use JQuerySelectors to get the positioning from that element.
However... Marionette practically generates the wrapper for me and I'm unsure how to add a wrapper inside of this row with a unique on top of this. Do any of you (who have experience with MarionetteJS or Backbone) have any idea how to go about this?
Am I even looking in the right direction? Any help would be much appreciated.
If you only need to assign unique id / class to wrapper, generated by View, following answer might help. Note, that it actually doesn't any additional wrapper, it is just way to add attributes to automatically generated wrappers.
You can define attributes for View (or anything, that extends it e.g. CompositeView). These attributes will be automatically assigned to wrapper element. You can define them in extend or as a function or hash
If you need just static identifier, use first approach:
planboard.gridRowView = Backbone.Marionette.CompositeView.extend({
template: "#grid-row-template",
itemView: planboard.gridItemView,
itemViewContainer: "div.day",
id: "some-id",
initialize: function() {
this.collection = new Backbone.Collection(_.toArray(this.model.attributes));
}
});
If you need to generate id based on model, use second approach:
planboard.gridRowView = Backbone.Marionette.CompositeView.extend({
template: "#grid-row-template",
itemView: planboard.gridItemView,
itemViewContainer: "div.day",
attributes: function() {
return {
id: this.model.get("id") // or something like this
};
},
initialize: function() {
this.collection = new Backbone.Collection(_.toArray(this.model.attributes));
}
});
Lets take for example the following template:
<div class="picture">
<img src="<%= model.get("url") %>"></img>
<p class="author"><%= model.get("author") %></p>
<p class="date"><%= model.get("date") %></p>
<div class="likers"><%= some logic that outputs a nice list of people that have liked this picture %></div>
<button class="like-button <%= model.get("is_liked") ? 'active' : '' %>"><%= model.get("is_liked") ? 'You liked this picture' : 'Click here to like this picture' %></button>
</div>
Lets say I render that template using Backbone and clicking the like-button triggers the following function:
Option A:
var me = "Peeter";
var model; //refrence to the model
var likers = model.get("likers");
model.get("is_liked") ? likers.remove(me) : likers.add(me); //Add/remove me
model.set({
"is_liked" : !model.get("is_liked"), //Toggle state
likers : likers
});
var $button = $(".like-button")
var $likers = $(".likers");
$button.toggleClass("active");
$button.hasClass("active") ? $button.text('You liked this picture') ? $button.text('Click here to like this picture');
$likers.text(/* copy/paste the logic that's in the template to render the list of likers*/);
Option B:
var me = "Peeter";
var model; //refrence to the model
var template; //refrence to the template declared at the top of this question
var likers = model.get("likers");
model.get("is_liked") ? likers.remove(me) : likers.add(me); //Add/remove me
model.set({
"is_liked" : !model.get("is_liked"), //Toggle state
likers : likers
});
template.render();
Which one of the methods would you recommend? Why? Also please take into count a mobile browser, is re-rendering an entire template (if the template was a bit bigger) a bit too slow on a mobile device?
This is a question that cannot be answered in general and some profiling for your specific use case will certainly help.
But as a general answer I think you have to review to following points:
Size of the template and the resulting time to process it with data
Number of events that you bind to your view. The more, the more you need to bind and unbind with every re-rendering
Your render function and how you have set it up as significant influence.
For more detais, please check these links:
http://ianstormtaylor.com/break-apart-your-backbonejs-render-methods/
http://ianstormtaylor.com/assigning-backbone-subviews-made-even-cleaner/
http://ianstormtaylor.com/rendering-views-in-backbonejs-isnt-always-simple/
I would prefer the update via JQuery. JQuery small updates are fairly quick even on 600 MHz android.
You could try to bind your UI element to your model with Backbone ModelBinder.
It's the best wait to update small UI section without re-render the whole template.
I just realized I was misunderstanding the el attribute of a Backbone.View. Basically my views require dynamic id attributes based on its model's attribute. I thought I had this working fine because I simply specified it in my template:
<script type="text/template" id="item_template">
<li class="item" id="{{identifier}}">
<span class="name">{{name}}</span>
</li>
</script>
However, I realized that what Backbone was actually doing was putting this compiled template into another element, div by default. I learned more about this by reading the documentation, but I'm still confused on how to create a dynamic id.
Preferably, I would love to find a way to make it such that the stuff in the above template serves as my el, since it already has everything I want, but I don't know if that is possible. So I'm wondering if, quite simply, there is a way to specify a dynamic id attribute.
I tried setting it within the initialize method, this.id = this.model.get('attr') but it didn't seem to have any effect, possibly because by this time it is already too late.
What I'm currently doing is just using jQuery to add the id in during render():
this.el.attr(id: this.model.get('identifier'));
it works, but of course, I'm simply asking if there is a preferred way to do it through Backbone.
Yes there is a standard way to do this in Backbone. You can pass id to the View constructor. You can also refactor your template so that Backbone creates the parent <li> element for you. Try this simpler template:
<script type="text/template" id="item_template">
<span class="name">{{name}}</span>
</script>
And add these to your view:
myView = Backbone.View.extend({
className: "item",
tagName: "li"
})
And instantiate it like this:
var view = new YourView({
model: mymodel,
id: mymodel.get('identifier') // or whatever
})
Good luck!
There is one more approach. I found it more convenient than passing id every time you create an instance of your view.
Template:
<script type="text/template" id="item_template">
<span class="name">{{name}}</span>
</script>
View:
var MyView = Backbone.View.extend({
tagName: 'li',
attributes: function(){
return {
id: this.model.get('identifier'),
class: 'item'//optionally, you could define it statically like before
}
}
})
When you create your view, pass in a selector that will let the view find your existing pre-rendered DOM element:
var id = "1234";
var view = YourView({el: '#'+id});