I have a select box that gets populated through a Backbone Collection:
class Prog.Views.History extends Backbone.View
template: JST['backbone/templates/shapes/history']
tagName: "option"
initialize: ->
#model.bind('change:formatted', #render, this);
render: ->
$(#el).html(#template(history: #model))
this
backbone/templates/shapes/history
<option value="<%=history.get('origin')%>"><%=history.get('origin')%></option>
This works great, all the correct data is presented in the select box, but what I would like is for the first option to be "please select" i.e. a placeholder... I thought about injecting a record called "placeholder" into the collection but it seems like a round about way.
This is how I call it:
appdenDropdown: (history) ->
console.log("append trigger")
view = new Prog.Views.History(model: history)
$('select#history').append(view.render().el)
How can I default this?
First thing I don't think this implementation is gona work properly, I think the result of the render will be something like:
<option><option value="the_origin">the_origin</option></option>
Attention to the double option.
A proper solution could be this:
class Prog.Views.History extends Backbone.View
tagName: "option"
initialize: ->
#model.bind('change:formatted', #render, this);
render: ->
$(#el).attr( "value", this.get( "origin" ) )
$(#el).html( this.get("origin") )
this
Not need of template here.
About the inclusion of the "please select" option.
Include the "please select" option element hardcoded directly in your HTML into the select element.
Then not use this.$el.html() to populate the select but this.$el.append(), this will respect the actual default option and it will add the dynamic elements after it.
Updated
For example if your select element looks like this:
<select id="history"></select>
Just make it looks like this:
<select id="history">
<option value="0" selected="selected">Please select...</option>
</select>
As you are using append to add the dynamic option elements the placeholder option element will remain.
Related
I have a dataset in angular where I add a default value as follows:
vm.serviceProperties.serviceCategories = clientcontext.clientlookup.serviceCategoryLookups();
vm.serviceProperties.serviceCategories.splice(0, 0, { 'serviceCategoryId': 0, 'serviceCategoryDisplayName': 'All' });
I need to bind this dataset to two select controls. For one, I need to show the 'All' value as default. For the other, I don't need the 'All' value at all.
How can I achieve that with the same dataset? I remember I saw somewhere that without defining the default value in the dataset itself, we can create an element option within the . Something like below:
<select>
<option> default</option>
<option ng-repeat="the dataset"></option>
</select>
But I'm not sure how to do it correct.
Create a property on the controllers that determines whether the default option is displayed or not.
Controller
app.controller('servicePropertiesWithDefault', function() {
$scope.showDefault = true;
});
app.controller('servicePropertiesWithoutDefault', function() {
$scope.showDefault = false;
});
Then in the template we use ng-if to show or hide the default option by passing in showDefault as the expression. Using ng-if is better than ng-show as it removes the element from the DOM.
Template
<select>
<option ng-if='showDefault'> default</option>
<option ng-repeat="the dataset"></option>
</select>
When showing a dropdown composite view collection of around 200 countries my application gets far too slow.
What is the best way to increase performance when dealing with large collections in marionette composite views?
Here is the function in the controller that is very slow to load. It is fast with only the following lines removed:
#layout.shippingCountryRegion.show shippingCountryView
#layout.billingCountryRegion.show billingCountryView
So it appears to be a very slow rendering issue.
Show.Controller =
showProfile: ->
#layout = #getLayoutView()
#layout.on "show", =>
headerView = #getHeaderView()
#layout.headerRegion.show headerView
accessView = #getAccessView()
#layout.accessRegion.show accessView
billingReadmeView = #getBillingReadmeView()
#layout.billingReadmeRegion.show billingReadmeView
billingFieldsView = #getBillingFieldsView()
#layout.billingFieldRegion.show billingFieldsView
shippingReadmeView = #getShippingReadmeView()
#layout.shippingReadmeRegion.show shippingReadmeView
shippingFieldsView = #getShippingFieldsView()
#layout.shippingFieldRegion.show shippingFieldsView
MyApp.request "location:get_countries", (countries) =>
billingCountryView = #getBillingCountryView(countries)
#layout.billingCountryRegion.show billingCountryView
MyApp.request "location:get_states", MyApp.activeCustomer.get('billing_country_id'), (states) =>
billingStateView = #getBillingStatesView(states)
#layout.billingStateRegion.show billingStateView
MyApp.request "location:get_countries", (countries) =>
shippingCountryView = #getShippingCountryView(countries)
#layout.shippingCountryRegion.show shippingCountryView
MyApp.request "location:get_states", MyApp.activeCustomer.get('shipping_country_id'), (states) =>
shippingStateView = #getShippingStatesView(states)
#layout.shippingStateRegion.show shippingStateView
MyApp.mainRegion.show #layout
The billing country view:
class View.BillingCountryDropdownItem extends MyApp.Views.ItemView
template: billingCountryItemTpl
tagName: "option"
onRender: ->
this.$el.attr('value', this.model.get('id'));
if MyApp.activeCustomer.get('billing_country_id') == this.model.get('id')
this.$el.attr('selected', 'selected');
class View.BillingCountryDropdown extends MyApp.Views.CompositeView
template: billingCountryTpl
itemView: View.BillingCountryDropdownItem
itemViewContainer: "select"
The template, simply:
<label>Country
<select id="billing_country_id" name="billing_country_id">
<%- name %>
</select>
</label>
Your code can be optimized. Just move content of onRender method to the ItemView attributes.
class View.BillingCountryDropdownItem extends MyApp.Views.ItemView
template: billingCountryItemTpl
tagName: "option"
attributes: ->
var id = this.model.get('id');
var attributes = { 'value': id };
if MyApp.activeCustomer.get('billing_country_id') == this.model.get('id')
attributes['selected'] = 'selected';
return attributes
The difference between this method and onRender case is that, on render will execute when collection already rendered and 200+ operations will be done with DOM nodes, which will bring performance issues.
In case of attributes method, it executes upon view creation.
There are few advices you can follow:
1) Ask your self do you really need to render all items at once? Maybe you can render part of collection and render other items on scroll or use pagination or use 'virtual scrioll' with SlickGrid or Webix for example.
2) Checkout how often you re-render your view. Try to minify num of events cause re-render
3) Try to minify num of event listeners of ItemView. Its good practice to delegate context events to CollectionView
4) You can use setTimeout to render collection by parts. For example you divide you coll in 4 parts by 50 items and raise 4 timeouts to render it.
5) You can optimize underscore templating and get rid of with {} operator. http://underscorejs.org/#template
What's in your 'billingCountryItemTpl' varible? If it's just string with template ID then you could precompile your template using Marionette.TemplateCache.
So you'll have:
template: Marionette.TemplateCache.get(billingCountryItemTpl)
I'm walking into a large Backbone.js project so I'm still getting my bearings. My template, my-group-item.jhbs has:
{{#if isComplete}}
.row-fluid
.span2
img.entity-image(src="/pictures/{{entityId}}.png")
.span10
.row-fluid
.span12
h3 {{entityName}}
p My first variable {{totalFirst}} and my second variable {{totalValue}}
{{/if}}
My View is:
module.exports = class MyItemView extends View
className: ->
templateData = #getTemplateData()
primaryData = #model.get('primaryData')
tagName: 'li'
template: require 'views/my-group-item'
initialize: () ->
super
primaryData = #model.get('primaryData')
In my template, the totalFirst and totalValue variables show nothing.
I'm calling my view with:
#groupView = new MyItemView
collection: groups
el: '.group-list'
How can I get these to show in the template?
You could pass more than 1 variable to your template, the first being your model and the second being the attributes of primaryData.
I have an API resource that gives me a list of users that each have several items. The hierarchy is like so:
- users
- user
- items
- item
- item
- item
- user
- items
- item
- item
- item
I would like to display the list of users on a single page, with each user entry displaying each of its items on the page as well.
When any one of these items is clicked, it should set an chosen attribute that is accessible through the overall users collection.
I'm having difficulty getting the item click information to bubble back up. My current implementation is creating a separate items collection in order to render the view, but then I lose the connection to its original user model, so I can't notify it when the item is selected.
My views are structured like so:
class List.Item extends Marionette.ItemView
template: "path/to/template"
events:
"click" : "choose"
choose: (e) ->
# what to do?
class List.User extends Marionette.CompositeView
collection: #collection
template: "path/to/template"
itemView: List.Item
itemViewContainer: "span"
initialize: ->
#collection = new App.Entities.Items(#model.get("items"), parent: #)
events:
"click a" : "toggleChoose"
#include "Chooseable"
class List.Users extends Marionette.CollectionView
itemView: List.User
Is there a better way to structure these collections or views, or is there a way to pass the information from the List.Item view to the parent List.User view and then into the users collection?
EDIT
I have tried backbone-relational, but it didn't seem to quite do what I need. Please correct me if I'm wrong here.
Your List.Item should contain it's current model with all properties at the time when choose is triggered. In this way, you can trigger other events with the List.Item's model values:
choose(e) : ->
trigger("mylistitem:choose", model)
Then listen for the event elsewhere :
itemView.on("itemview:mylistitem:choose", ( childView, model ) -> {
alert(model.get('..whatever..')
}
It is actually possible to instantiate the items collection to reference the parent user and vice-versa directly in Backbone:
class Entities.User extends Backbone.Model
...
initialize: ->
#items = new Entities.Items #get("items"),
user: #
class Entities.Items extends Backbone.Collection
...
initialize: (models, options) ->
#user = options?.user
So now the List.User CompositeView can pass this information to the List.Item ItemView:
class List.User extends Marionette.CompositeView
collection: #collection
...
initialize: ->
#collection = #model.items
With this in place, it is possible to access the user directly from the ItemView:
class List.Item extends Marionette.ItemView
...
events:
"click" : "choose"
choose: (e) ->
e.preventDefault()
user = #model.collection.user
console.log "user: ", user
And from there it's possible to take any necessary actions on the user and its collection.
The problem is I want to render option elements with the value attribute set as well as the text node.
So setting tagName to option in my ItemView does not just do this. The solution I have at the moment is to set it to option and then use the following code in the ItemView constructor to set the value attibute:
onRender: function () {
this.$el.attr('value', this.model.get('name'));
}
This works.
But is there any other way?
What I'd really like to do is just tell Marionette not to output an element at all and then in my ItemView template have this:
<option value="<%= name %>"> <%= name %> </option>
Is this possible?
It's possible but a bit fiddly, by default Backbone always uses a wrapper element (definable by using tagName) but you'd have to expressly populate the attributes (as you are doing above).
It is however possible to circumvent the wrapper element using a slightly convoluted setElement approach and this will enable you keep all markup in a template with attributes on the root node populated from the model. I like this approach as well since I personally think it keeps a clearer separation of concerns.
Take a look here for an example - Backbone, not "this.el" wrapping
Not sure if Marionette has a build in mechanism for doing this.
I am not sure you should from all different kind of reasons.
But I understand that in the use case of a single tag view, the way Marionette/Backbone work you either create another tag and have a view, or you use onRender and query to update the single tag that was already generated for you.
You could have this Object that will extend ItemView, lets call it SingleTagView. Basicly I am extending ItemView and using overrunning it's render function with one change.
Instead of doing:
this.$el.html(html);
I am doing:
var $html = $(html);
this.$el.replaceWith($html);
this.setElement($html);
Here is the code:
var SingleTagView = Marionette.ItemView.extend({
render: function() {
this.isClosed = false;
this.triggerMethod("before:render", this);
this.triggerMethod("item:before:render", this);
var data = this.serializeData();
data = this.mixinTemplateHelpers(data);
var template = this.getTemplate();
var html = Marionette.Renderer.render(template, data);
// above is standart ItemView code
var $html = $(html);
this.$el.replaceWith($html);
this.setElement($html);
// this code above replaced this.$el.html(html);
// below is standart ItemView code
this.bindUIElements();
this.triggerMethod("render", this);
this.triggerMethod("item:rendered", this);
return this;
}
});
If you are going to use this kind of angle, make sure you test the behavior properly (event handling, Dom leaks, different flows with before:render events).
I would try:
var optionTag = Marionette.ItemView.extend({
tagName: "option",
initialize: function(){
this.attributes = {
value: this.model.get('name');
}
}
}
Backbone should then do the rest (i.e. putting the content of attributes into the HTML tag)...