I'm trying to set up a simple Marionette's CompositeView. Here's what I want in the end:
%select#target-id
%option{:value => "#{#id}"} = #name
%option{:value => "#{#id}"} = #name
etc
In my CompositeView I specify the childViewContainer: select and I need to display both #name (for the readability) and #id (for the related event) in the options of this select. Due to the nature of default div element I can to speficfy tagName as option in my ItemView:
class New.TargetView extends App.Views.ItemView
template: "path/to/template"
tagName: 'option'
And in the template I can pass only the content of to-be-created option element: = #name. This works fine, Marionette creates an option element for each model and populates it with the name of the model. The problem is that I don't know how to pass an attributes as well, since I can't specify an attribute of the element that hasn't been created yet.
I've also tried to set an attributes property on the ItemView like this:
attributes:
value: "#{#id}"
And it technically works: the options are populated with the value="" attribute, but the content is undefined. Please advice.
I'm not sure about part when you use attributes. You should pass hash or function that will return hash as stated in Backbone view.attributes docs.
attributes:
value: "#{#id}"
In old money it works like this. Here is jsfiddle.
attributes: function () {
return {
value: this.model.id
};
}
CompositeView and ItemView are views that require a Marionette collection and model, respectively. When you create a CompositeView you pass a collection of models, each of which will be passed to the corresponding ItemView.
My guess is that it's not a problem with the template but with how you are setting up the data. As far as I know, there is no attributes option for ItemView, you have to either initialize the CompositeView with a properly formed collection or create the new attributes with the serializeData method on the ItemView.
In the first case, you will do something like this:
var SomeCompositeView = Marionette.CompositeView.extend({
template: "your-template-name",
childViewContainer: select
});
var SomeItemView = Marionette.ItemView.extend({
template: "your-other-template",
tagName: 'option'
});
// This collection can be retrieved from a database or anywhere else
// Just make sure that the models have the fields you want
var myCollection = new Backbone.Collection([{id: "someid1", name: "name1"}, {id: "someid2", name: "name2"}]);
var view = new SomeCompositeView({collection: myCollection});
On the second case you'll have something similar but on the ItemView you'll have:
var SomeItemView = Marionette.ItemView.extend({
template: "your-other-template",
tagName: 'option',
serializeData: function () {
return { someAttribute: "someValue" }
}
}
Just remember that for this second approach, the ItemView has to have access to the values you are returning. serializeData is only for reformatting data or performing operations on the data that you cannot perform on the template. The template will only have access to the variables you are returning from serializeData, independently of the original model.
Related
I have created a simple view, MyView, that extends from ItemView. Then when I create the instance of MyView, I am trying to add references to UI elements within the view as well as events that use those UI elements.
HTML
<div id="container"></div>
<script type="text/template" id="my-template">
<p>This is a rendered template.</p>
<button data-ui="changeModelNameButton">Change Model Name</button>
</script>
JS
// Define a custom view that extends off of ItemView
var MyView = Marionette.ItemView.extend({
template: "#my-template"
});
// Instantiate the custom view we defined above
var view = new MyView({
el: "#container",
ui: {
changeModelNameButton: '[data-ui~=changeModelNameButton]'
},
events: {
'click #ui.changeModelNameButton': function() {
alert('here');
}
}
});
// Render the view in the element defined within the custom view instantiation method
view.render();
I am receiving the following error in the console:
Uncaught TypeError: Cannot read property 'changeModelNameButton' of
undefined
I noticed that if I move the ui declarations to the view definition, it works fine, but I would like to know why I can't add those when I create the instance. Is there no way to add them to the instance or am I missing something?
Note: I am using Backbone 1.3.3, Marionette 2.4.4, Underscore 1.8.3, and jQuery 3.1.1
Options overriding view's properties
Not all the options are automatically overriding the view class properties when passed on instantiation.
ui looks like it's not one of them.
Backbone will automatically apply the following (private) viewOptions on a view:
// List of view options to be set as properties.
var viewOptions = [
'model',
'collection',
'el',
'id',
'attributes',
'className',
'tagName',
'events'
];
In the view constructor, this is extended with the chosen options (source):
_.extend(this, _.pick(options, viewOptions));
How to pass the ui option?
You either need to put the ui hash in the view class definition, or to apply the ui hash yourself.
var MyView = Marionette.ItemView.extend({
template: "#my-template",
initialize: function(options) {
// merge the received options with the default `ui` of this view.
this.ui = _.extend({}, this.ui, this.options.ui);
}
});
Note that options passed to a view are available in this.options automatically.
What's the real goal behind this?
If you're going to mess around the ui and the events with its callbacks, it would be best to define a new view.
It looks like a XY problem where the real problem lies in the architecture of the app, but we can't help since you're asking about what's blocking you right now.
I'm a junior javascript developer, got an internship in a company that uses Backbone and Marionette.
My first task is to create searching, filtering and sorting functionality in a collection based in some inputs, the thing is i got 2 differents views: one itemView renders the input fields(search field, sorting selection,etc ) and a collectionView renders the collection.
I've bee analizing backbone event aggregator, listenTo method, etc to find a way to make the collectionView listen to submit, click events in the itemView so it can render itself accordingly. For example when the user enters "frog" in the search field, the collectionView displays models containing that criteria, if the user clicks the last modified sorting option, the collectionView renders itself that way.
Any suggestion is really welcome.
Thanks in advance.
Basically Marionette does everything for you, you just need to initialize collection view properly.
You can specify which child view events your collection view should listen to (anyway it listens to some default events by default)
Here is example of search functionality and event handling of child views:
HTML
<script id='itemViewTemplate' type = "text/template">
<div class='itemView'><%= title %></div>
</script>
<script id='collectionViewTemplate' type = "text/template">
<div class="collectionView"></div>
</script>
<input value='' id='search' placeholder='search'>
Javascript
// our data to show and filter
var data = [
{title: "title 1"},
{title: "title 2"},
{title: "title 3"}
];
// item model
var FooBar = Backbone.Model.extend({
defaults: {
title: ""
}
});
// collection of items
var FooBarCollection = Backbone.Collection.extend({
model: FooBar
});
// item view
var FooView = Marionette.ItemView.extend({
template: "#itemViewTemplate",
events: {
"click": "_onClick"
},
_onClick: function() {
this.trigger('click', this); // here we trigger event which will be listened to in collection view
}
});
// collection view
var MyCollectionView = Marionette.CollectionView.extend({
childView: FooView,
template: "#collectionViewTemplate",
childEvents: {
'click': '_onItemClick' // listen to any child events (in this case click, but you can specify any other)
},
_onItemClick: function(item) {
$('.message').text("item clicked: " + item.model.get("title"));
console.log(arguments); // event handler
}
});
// init our collection
var myCollection = new FooBarCollection(data);
// another collection which will be filtered
var filterCollection = new FooBarCollection(data);
// init collection view
var myCollectionView = new MyCollectionView({
collection: myCollection
});
// render collection view
$("body").append(myCollectionView.render().$el);
// search
$('#search').change(function() {
var value = $(this).val(); // get search string
var models = filterCollection.where({
title: value
}); // find models by search string
value ? myCollection.reset(models) : myCollection.reset(filterCollection.models);
// reset collection with models that fits search string.
// since marionette collection view listens to all changes of collection
// it will re-render itself
// filter collection holds all of our models, and myCollection holds subset of models, you can think of more efficient way of filtering
});
// just to show click event info
$('body').append("<div class='messageContainer'>Click on item to see its title:<div class='message'></div></div>");
Marionette collection view listens to all myCollection events, for exaple
If you'll write
myCollection.add({title: 'title 4'});
It will automatically render new itemView in collection view.
Same for remove, reset and other defaut Backbone.Collection methods (which trigger events, and marionette listens to them);
Here is jsfiddle: http://jsfiddle.net/hg48uk7s/11/
And here is docs on marionette:
http://marionettejs.com/docs/v2.4.3/marionette.collectionview.html#collectionviews-childevents
I suggest to start reading docs on marionnet from beginnig, because CollectionView inherits a lot from ItemView and ItemView inherits a lot from View and so on, so you could know all the features Collection.View has.
UPDATE
Maybe I misunderstood a question a bit, you need a communication between collectionView and some other view (itemView in this case another view, not the view collectionView uses to render its children, that's what I thought). In this case, here is an updated fiddle:
http://jsfiddle.net/hg48uk7s/17/
You need third entity to handle communication between collectionView and searchView for example. Usually it some controller, which listens to searchView events, then calls some handler, giving control to collectionView, which uses search value to filter itself.
I started to learn Marionette.View concept.for this I created model with 1 attribute and assign default value.
I have dropdown list in my html file. Whenever it changes it triggers a function name as myfunction.Inside myFunction I changed model attribute value.
Whenever attribute value changes automatically it triggers another function.that event I written inside Marionette.CompositeView.but it's not working.
Earlier I did same thing using myModel.on there it's working fine.But it's not working modelEvents in Marionette.CompositeView.
let's check the following code.
var mymodel=Backbone.Model.extend({
defaults:{
"name":"oldValue"
}
});
var mymodelObj=new mymodel();
//marionette.View code
var backView=Backbone.Marionette.CompositeView.extend({
events:{
"change #pop1":"myFunction"
},
myFunction:function(){
mymodelObj.set({name:"updatedValue"})
},
modelEvents:{
"change:name":"myFunction1"
},
myFunction1:function(){
alert("Hi");
}
});
//creating instance
var backViewObj=new backView({el:$("#sidebar")});
How can I fix this.
Right Now I am trying to understanding where Marionette.js useful in my Backbone Applications.
If I am not mistaken, model is not attached to the view you have created. For modelEvents to be triggered, there should be a model attribute in CompositeView. For this you should specify the model during initialization of CompositeView;
var backViewObj=new backView({el:$("#sidebar"), model : mymodelObj});
To do this though you need to pass the model in when creating the backView like so:
var backViewObj = new backView({ el:$("#sidebar"), model: myModel });
Marionette accomplishes this by overriding delegateEvents which Backbone.View calls in it's constructor and delegating your modelEvents object to the model:
Marionette.bindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents"));
Based on your comments above I think that you're unclear about the different types of views that Backbone.Marionette provides as you are asking how to pass more model instances. I also was unclear when I began using Marionette.
Marionette.ItemView represents a single model.
Marionette.CollectionView represents a collection and renders a Marionette.ItemView for every model in the collection. It does not have the ability to render a template.
Marionette.CompositeView represents both a model and a collection (leaf and branch in a tree structure). This is for recursive data structures although you can use a CompositeView to render a template (like a table header, etc.) and use itemViewContainer to place the itemViews in a specific element.
If you want your CompositeView to be able to render multiple models you'll need to pass a collection of models into the CompositeView and not one single model:
I have the following ItemView template which is filled with customer data (firstName, lastName) and I want to add a CollectionView into the div .addresses.
Template
<script type="text/html" id="template-customer-details">
<h4><%= firstName %> <%= lastName %></h4>
<button class="edit">Edit</button>
<h5>Addresses</h5>
<div class="addresses">...</div>
</script>
Layout
Layout.Details = Backbone.Marionette.ItemView.extend({
template: '#template-customer-details',
regions: {
addresses: ".addresses"
},
serializeData: function () {
return this.model.attributes;
},
initialize: function () {
this.addressList = new App.Models.AddressList();
// Error!
this.regions.addresses.show(this.addressList);
this.bindTo(this, "render", this.$el.refresh, this.$el);
this.model.bind("change", this.render.bind(this));
}
});
I am getting the error "Uncaught TypeError: Object .addresses has no method 'show'."
Do I have to wait until the view is loaded?
I think you've got things a bit confused. An ItemView doesn't do anything with a regions property (you may be thinking of the Application class), so when you try to call this.regions.addresses.show that's the same as calling ".addresses".show.
I think you probably want to use a CompositeView in this case as it combines an ItemView (which you can use for your customer data) and a CollectionView which you can use for your AddressList. You'll also need to define a separate ItemView for an address (as a CollectionView just creates an ItemView for each item in a collection).
Something a little like this (which I've not tested, so may not be entirely correct):
AddressView = Backbone.Marionette.ItemView.extend({
template: '#addressTemplate'
});
Layout.Details = Backbone.Marionette.CompositeView.extend({
template: '#template-customer-details',
itemView: AddressView,
itemViewContainer: '.addresses'
});
// Then create your view something like this:
new Layout.Details({
model: new App.Models.CustomerDetails(),
collection: new App.Models.AddressList()
});
I also don't think you need to specifically bind the change & render events like in your example as marionette will usually take care of that (the same with your implementation of serializeData, which looks like it's vaguely the same as the default implementation)
I understand how to get a collection together, or an individual model. And I can usually get a model's data to display. But I'm not clear at all how to take a collection and get the list of models within that collection to display.
Am I supposed to iterate over the collection and render each model individually?
Is that supposed to be part of the collection's render function?
Or does the collection just have it's own view and somehow I populate the entire collection data into a view?
Just speaking generally, what is the normal method to display a list of models?
From my experience, it's the best to keep in your collection view references to each model's view.
This snippet from the project I'm currently working on should help you get the idea better:
var MyElementsViewClass = Backbone.View.extend({
tagName: 'table',
events: {
// only whole collection events (like table sorting)
// each child view has it's own events
},
initialize: function() {
this._MyElementViews = {}; // view chache for further reuse
_(this).bindAll('add');
this.collection.bind('add', this.add);
},
render: function() {
// some collection rendering related stuff
// like appending <table> or <ul> elements
return this;
},
add: function(m) {
var MyElementView = new MyElementViewClass({
model: m
});
// cache the view
this._MyElementViews[m.get('id')] = MyElementView;
// single model rendering
// like appending <tr> or <li> elements
MyElementView.render();
}
});
Taking this approach allows you to update views more efficiently (re-rendering one row in the table instead of the whole table).
I think there are two ways to do it.
Use a view per item, and manipulate the DOM yourself. This is what the Todos example does. It's a nice way to do things because the event handling for a single model item is clearer. You also can do one template per item. On the downside, you don't have a single template for the collection view as a whole.
Use a view for the whole collection. The main advantage here is that you can do more manipulation in your templates. The downside is that you don't have a template per item, so if you've got a heterogeneous collection, you need to switch in the collection view template code -- bletcherous.
For the second strategy, I've done code that works something like this:
var Goose = Backbone.Model.extend({ });
var Gaggle = Backbone.Collection.extend({
model: Goose;
};
var GaggleView = Backbone.View.extend({
el: $('#gaggle'),
template: _.template($('#gaggle-template').html()),
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
}
};
var g = new Gaggle({id: 69);
g.fetch({success: function(g, response) {
gv = new GaggleView({model: g});
gv.render();
}});
The template code would look something like this:
<script type="text/template" id="gaggle-template">
<ul id="gaggle-list">
<% _.each(gaggle, function(goose) { %>
<li><%- goose.name %></li>
<% }); %>
</ul>
</script>
Note that I use the _ functions (useful!) in the template. I also use the "obj" element, which is captured in the template function. It's probably cheating a bit; passing in {gaggle: [...]} might be nicer, and less dependent on the implementation.
I think when it comes down to it the answer is "There are two ways to do it, and neither one is that great."
The idea of backbone is that view rendering is event driven.
Views attach to Model data change events so that when any data in the model changes the view updates itself for you.
What you're meant to do with collections is manipulate a collection of models at the same time.
I would recommend reading the annotated example.
I've also found this a confusing part of the Backbone framework.
The example Todos code is an example here. It uses 4 classes:
Todo (extends Backbone.Model). This represents a single item to be todone.
TodoList (extends Backbone.Collection). Its "model" property is the Todo class.
TodoView (extends Backbone.View). Its tagName is "li". It uses a template to render a single Todo.
AppView (extends Backbone.View). Its element is the "#todoapp" . Instead of having a "model" property, it uses a global TodoList named "Todos" (it's not clear why...). It binds to the important change events on Todos, and when there's a change, it either adds a single TodoView, or loops through all the Todo instances, adding one TodoView at a time. It doesn't have a single template for rendering; it lets each TodoView render itself, and it has a separate template for rendering the stats area.
It's not really the world's best example for first review. In particular, it doesn't use the Router class to route URLs, nor does it map the model classes to REST resources.
But it seems like the "best practice" might be to keep a view for each member of the collection, and manipulate the DOM elements created by those views directly.