I'm trying to render a list with a Marionette CompositeView. I am not sure why the rendered list just has an item displaying the word result. I was expecting the first item to display Level 1.
Here is a fiddle to my current code: http://jsfiddle.net/16L1hen4/
Here is my JS, template, and data:
JavaScript:
var App = new Backbone.Marionette.Application();
App.addRegions({
mainRegion: '#main'
});
var TreeModel = Backbone.Model.extend({
});
var TreeCollection = Backbone.Collection.extend({
model: TreeModel,
url: 'https://api.mongolab.com/api/1/databases/backbone-tree/collections/tree?apiKey=somekey'
});
var TreeView = Backbone.Marionette.CompositeView.extend({
initialize: function() {
console.log(this.collection);
},
tagName: 'ul',
template: _.template( $('#tree-template').html() )
});
var treeCollection = new TreeCollection();
treeCollection.fetch().done(function () {
var treeView = new TreeView({collection: treeCollection});
App.mainRegion.show(treeView);
});
Template:
<div id="main"></div>
<script type="text/template" id="tree-template">
<li><%- name %></li>
</script>
JSON Data:
{
"_id": {
"$oid": "54adab80e4b0aa674b256836"
},
"name": "Level 1",
"children": [
{
"name": "Child 1 - Level 2",
"children": [
{
"name": "Jon - Level 3"
},
{
"name": "Mary - Level 3"
}
]
},
{
"name": "Child 2 - Level 2",
"children": [
{
"name": "Bill - Level 3"
}
]
}
]
}
Read the marrionnete docs a bit closer - you need a childView defined....
You are using a CompositeView to display a Collection, but you need to define a childView to render the models
var LeafView = Backbone.Marionette.ItemView.extend({
// ...
});
var TreeView = Backbone.Marionette.CollectionView.extend({
childView: LeafView
})
here is an updated fiddle. http://jsfiddle.net/6ok1rptq/
Now the "result" showing in the html, without being familiar with the underscore source, I believe this is caused by the fact that the data given to the template is null, and a quick look at the source of underscore shows that it is using with
http://underscorejs.org/docs/underscore.html#section-148
"If a variable is not specified, place data values in local scope."
Meaning that the template can't find a "name" variable, and will instead look it up in the global scope (window)
Result is just the name of the jsfiddle iframe containing the result of the fiddle
<iframe name="result" ...>
I didn't test this, but I assume that the error lies with the fact that you didn't define a Marionette Itemview on the CompositeView.
The logical structure is to pass the Compositeview a collection as you did in the question, and the models will be rendered in separate itemviews.
In the itemview you can call:
this.model.get("property");
To access the properties from within the view.
Related
My problem is a bit complex, so i will try to explain it as detailed as possible.
I have a directive in a SPA that render their components based on a JSON data that i'm getting from an API. Based on the elements and their types (the JSON is an array of different objects) i'm rendering every object in an specific directive:
Objects Type 1: Renders in a Directive Type 1.
Objects Type 2: Renders in a Directive Type 2.
Objects Type 3: Renders in a Directive Type 3.
Directives Type 1-2-3 are contained in the parent directive and every directive has different controls (select, checkbox). This is a very simple Sketch:
And the "sub-directives":
I'm rendering my elements as follows (Container directive):
<div ng-repeat="element in elementList | customFilter:itemsType1">
<div class="line"></div>
<div class="form-group">
<directivetype1 itemdata="element" modeldata="data"></directivetype1>
</div>
</div>
<div ng-repeat="element in elementList | customFilter:itemsType2">
<div class="line"></div>
<div class="form-group">
<directivetype2 itemdata="element" modeldata="data"></directivetype2>
</div>
</div>
...
And this is the Directive 1 code:
<div class="container-fluid">
<div class="form-group">
<div class="col-xs-6 col-sm-3">
<div class="checkbox">
<label><input type="checkbox"/>{{itemdata.metadata.description}}</label>
</div>
</div>
<div class="col-xs-6 col-sm-3">
<label>Option</label>
<select class="form-control" ng-model="" ng-options="list.id as list.label for list in item.optionData"></select>
</div>
</div>
</div>
My problem goes when i try to attach the model to every element rendered, because of:
The model data comes from another API, in another structure.
I'm iterating the list of controls with ng-repeat, but, when i pass the model data to the sub-directive i'm passing all the possible data (as Array) and i'm not being capable of filter and know what object in that array belongs to an specific view element.
The data has the following structure:
View data:
[
{
"elementA": {
"metadata": {
"id": "001",
"subId": "016",
"description": "Element 1"
},
"optionData": [
{
"id": "5",
"label": "Option 1"
},
{
"id": "6",
"label": "Option 2"
},
{
"id": "7",
"label": "Option 3"
}
]
}
},
{
"elementB": {
"metadata": {
"id": "002",
"subId": "024",
"description": "Element 2"
},
"optionData": [
{
"id": "1",
"label": "Option 1"
},
{
"id": "2",
"label": "Option 2"
},
{
"id": "3",
"label": "Option 3"
}
]
}
}
]
Model data:
[
{
"metadata": {
"id": "002",
"subId": "024",
"description": "Element 2",
"selected": "1"
},
...(Some other data belonging to the model)
},
{
"metadata": {
"id": "001",
"subId": "016",
"description": "Element 1",
"selected": "5"
},
...(Some other data belonging to the model)
},
...
]
As you can see, the only way to correlate both models is with id and subId Fields in the metadata object (because the metadata itself can vary having more or less fields).
QUESTION
How can i filter my model object, based on the view object? My goal is to get the model object that correlates to the view object and pass it to the sub-directive for setting it as the model of the control that i'm rendering at that point.
EDIT:
As cmw pointed out, i've coded a function to correlate every model object with their respective view object, but that object is not reflected in the directive scope. itemdata and modeldata are passed to the directive using a bi-directional scope ('='). I think (but i'm not entirely sure) that, when i pass a function to modeldata the directive is not being capable of setting the returned object. The solution that i've coded based on the cmw answer is as follows:
Directive:
<directivetype1 itemdata="element" modeldata="getModelObject(data)"></directivetype1>
JS (coded in the Ctrl of the parent):
$scope.getModelObject = function(element){
var id = typeof element.metadata === 'undefined' ? null : element.metadata.id;
var subid = typeof element.metadata === 'undefined' ? null : element.metadata.subid;
var modelElement = null;
for (var i = 0; i < $scope.data.length; i += 1){
element = $scope.data[i];
if (modelElement.metadata.id === id && modelElement.metadata.subid === id) return element;
}
return null;
};
But when i try to work in the directive with modeldata i see "null" in FF/Chrome Console.
Any guideline to know what's happening?
Thanks.
EDIT 2:
I've added a version of my code here: http://plnkr.co/edit/xjp1l3PuWczdqYf5LP8q?p=preview. Sadly, in that Plunkr it works as expected but my code does not (i'm expecting to see the output of <h1>{{modeldata}}</h1>). I'm comparing the two versions to see any difference (note that i've included the same AngularJS version that i'm using in my project).
As you've pointed out, I believe the key is simply to make use of the id and subId properties on the meta object.
Something like this would probably work...
<div ng-repeat="element in elementList | customFilter:itemsType1">
<div class="line"></div>
<div class="form-group">
<directivetype1 itemdata="element" modeldata="modelDataFor(element)">
</directivetype1>
</div>
</div>
Then, in your controller, define a function like the following...
$scope.modelDataFor = function (element) {
var id = element.meta.id,
subId = element.meta.subId,
curr;
for (var i = 0; i < $scope.data.length; i += 1) {
curr = $scope.data[i];
if (curr.meta.id === id && curr.meta.subId === subId) {
return curr;
}
}
return null;
}
This seems like the most natural place to pluck out the relative data model object to pass into your nested directives.
I have an object and within this object I have items and one of the items is an array which also contains objects. A sample of the data is shown below.
I am using knockout to bind this data to the view so I think I need to implement a double loop for returning the objects and the objects within the child array to be able to bind them in the view.
Sample data:
"singers": {
"ijiyt6ih": {
"id": ObjectId('ijiyt6ih'),
"name": "John",
"songs": [
{
"id": ObjectId('okoiu8yi'),
"songName": "Hello There",
"year": "1980"
},
{
"id": ObjectId('sewfd323'),
"songName": "No More",
"year": "1983"
}
]
},
"98usd96w": {
"id": ObjectId('98usd96w'),
"name": "Jack",
"songs": [
{
"id": ObjectId('iew342o3'),
"songName": "Hurry Up",
"year": "1985"
}
]
}
}
I need to find a way to appropriately loop through this so that I can modify the returned data to bind it to the viewModel using knockout.
Here is how my viewModel looks like:
singersViewModel = function(data) {
var self = {
singerId: ko.observable(data.id),
singerName: ko.observable(data.name),
songName: ko.observable(...),
songYear: ko.observable(...)
};
I am not sure if I will have to return two different sets of data or not.
As for the looping. I was able to loop and return the list of singers to display on the page but I am not able to get the list of songs displayed within each singer.
Here is my loop so far:
var self = {},
singer,
tempSingers = [];
self.singers = ko.observableArray([]);
for (singer in singers) {
if (singers.hasOwnProperty(singer)) {
tempSingers.push(new singersViewModel(singers[singer]));
}
}
self.singers(tempSingers);
I tried to duplicate the same type of loop for songs within this loop but i would get an error using hasOwnProperty because songs is an array.
In the included snippet you can see how you can map the original data to a viewmodel that can be bound to a view.
I've left the ids as regular properties, and converted the names into observables, so thatthey can be edited. At the bottom you can see the current viewmodel state.
There is also a sample view which iterates the list of singers, and also the list of song within each singer.
As you can see I'm implementing the solution using mapping. For mapping you need to implement a callback that receives each original object and returns a new one with a new structure. For example this part of the code
_.map(_singers, function(singer) {
return {
id: singer.id,
name: ko.observable(singer.name),
// ... songs:
})
iterates over each singer (the sample data in the question), and for each one creates a new object with the id, an observable which includes the name (and the mapping of songs, which I don't show in this fragment).
NOTE: I'm using lodash, but many browsers support map natively as an array function
var ObjectId = function (id) { return id; }
var singers = {
"ijiyt6ih": {
"id": ObjectId('ijiyt6ih'),
"name": "John",
"songs": [
{
"id": ObjectId('okoiu8yi'),
"songName": "Hello There",
"year": "1980"
},
{
"id": ObjectId('sewfd323'),
"songName": "No More",
"year": "1983"
}
]
},
"98usd96w": {
"id": ObjectId('98usd96w'),
"name": "Jack",
"songs": [
{
"id": ObjectId('iew342o3'),
"songName": "Hurry Up",
"year": "1985"
}
]
}
};
var SingersVm = function(_singers) {
var self = this;
self.singers = _.map(_singers, function(singer) {
return {
id: singer.id,
name: ko.observable(singer.name),
songs: _.map(singer.songs, function(song) {
return {
name: ko.observable(song.songName),
id: song.id
};
})
};
});
return self;
};
var vm = new SingersVm(singers);
//console.log(vm);
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div data-bind="foreach: singers">
<div>
<input data-bind="value: name"/> (<span data-bind="text: id"></span>)
<ul data-bind="foreach:songs">
<li>
<input data-bind="value: name"/> (<span data-bind="text: id"></span>)
</li>
</ul>
</div>
</div>
<pre data-bind="html: ko.toJSON($root,null,2)">
</pre>
I have a collection of records that I am trying to log to the console after a fetch. I am not sure why the console is logging a lot of methods instead of the data in the records when I log the collection.
How do I log the data in the collection?
The records in the collection contain this:
{
"color": "yellow",
"date": "March 24, 2014",
"manufacturer": "Ford",
"name": "Mustang"
},
{
"color": "green",
"date": "July 1, 2014",
"manufacturer": "Toyota",
"name": "Corolla"
},
{
"color": "red",
"date": "February 2, 2014",
"manufacturer": "Honda",
"name": "Civic"
}
Here is my Backbone.js code:
var TheModel = Backbone.Model.extend({
defaults: {
'id': 'null',
'color': '',
'date': '',
'name': ''
}
});
var TheCollection = Backbone.Collection.extend({
models: TheModel,
url: 'https://api.mongolab.com/api/1/databases/testdatabase/collections/Content?apiKey=xcdsdfsdczdcdsdfs'
});
var aCollection = new TheCollection();
var TheView = Backbone.View.extend({
initialize: function () {
this.collection = aCollection.fetch();
},
render: function () {
console.log('this.collection');
console.log(this.collection);
return this;
}
})
var aView = new TheView();
aView.render();
this.collection will be an instance of a Backbone collection and that's an object with a bunch of methods and data (most of which you won't care about). If you want to look at just the models, then this.collection.models is for you:
models collection.models
Raw access to the JavaScript array of models inside of the collection.
But that will leave with an array of objects which have a bunch of methods and data that you don't care about.
If you just want the data, then you probably want this.collection.toJSON():
toJSON collection.toJSON([options])
Return an array containing the attributes hash of each model (via toJSON) in the collection.
If you've overridden toJSON anywhere then you can get around that by extracting the model attributes with something like:
console.log(this.collection.map(function(m) { return m.attributes })
or, if you don't want to deal with live references in the console:
console.log(this.collection.map(function(m) { return _(m.attributes).clone() })
That _(m.attributes).clone() call is pretty much what the default toJSON for models does.
Your problems calling methods on this.collection stem from this:
this.collection = aCollection.fetch();
The fetch call returns a jqXHR (the same thing that $.ajax returns), not the collection itself. You want your initialize to look more like this:
initialize: function() {
this.collection = aCollection;
}
Or you could let Backbone hook up the collection for you. Backbone views will attach some options to the view instances for you, collection is one of these so you could leave out initialize and say:
var aView = new TheView({ collection: aCollection });
Backstory
I'm using AngularJS/KendoUI and using angular-kendo-ui as a the 'bridge' between the two. I'm using Kendo for it's treeview component and this piece is a client requirement.
What I need to accomplish is
1. draw the tree menu from data provided by a service
2. periodically check each element in this data, and update a 'disabled' prop
3. redraw elements in the treeview based on the results from the above step.
Assumptions
1. If I want to be able to update the kendo tree view, then I need to use Kendo's observeables
2. I may be using Kendo's ObservableArray incorrectly here.
The problem
If I create a new ObservableArray like so
var things = new kendo.data.ObservableArray([{
text: "Parent 1",
items: [{text: "Child 1"}, {text: "Child 2"}, {text: "Child 3"}]
}])
things be logged to the console and the structure is intact.
However, once the treeview is instantiated with this data, further logging of things to the console show that the children (items) are no longer present. It is very hard to iterate over and update children if they don't exist!
Plunker here http://plnkr.co/edit/qJpIvK?p=info, if you view the 'script.js' file and open the console, you should be able to see my issue.
here is the code
HTML
<div ng-app="MyApp">
<div ng-controller="TreeController">
<div kendo-tree-view k-options="thingsOptions"></div>
</div>
</div>
JS
var app = angular.module("MyApp", ["kendo.directives"]);
app.controller('TreeController', function($scope, $timeout) {
var things = new kendo.data.ObservableArray([{
text: "Parent 1",
items: [{
text: "Child 1"
}, {
text: "Child 2"
}, {
text: "Child 3"
}]
}, {
text: "Parent 2",
items: [{
text: "Child 1"
}, {
text: "Child 2"
}, {
text: "Child 3"
}]
}]);
// should have 3 items
console.log('preTreeView init', things[1].items);
$scope.thingsOptions = {
dataSource: things
};
$timeout(function() {
// the 3 items expected are gone, why?
console.log('postTreeView init', things[1].items);
}, 1000);
});
Is this a misuse of ObservableArray and if so, what is the correct approach?
Internally, the TreeView widget turns your ObservableArray into a kendo.data.HierarchicalDataSource http://docs.telerik.com/kendo-ui/api/framework/hierarchicaldatasource which moves each of the children into DataSource objects of their own.
You can navigate them afterward like this:
var treeViewWidget = $(".k-treeview").data("kendoTreeView");
var dataSource = treeViewWidget.dataSource; // this is a HierachicalDataSource
var parents = dataSource.data(); // Parent1 and Parent2
var parent1 = parents[0];
var doesParent1HaveChildren = parent1.hasChildren; // true
var childrenDataSource = parent1.children; // this is a HierarchicalDataSource
var child1 = childrenDataSource.data()[0]; // this is {text: "Child 1"}
I have these models:
TravelClient.Tour = DS.Model.extend({
title: DS.attr('string'),
description: DS.attr('string'),
seats: DS.attr('number'),
takenSeats: DS.hasMany('TravelClient.TakenSeat', {embedded:'always'})
TakenSeats: function() {
console.log(this.get('takenSeats').toArray())
}.property('takenSeats')
});
TravelClient.TakenSeat = DS.Model.extend({
tour: DS.belongsTo('TravelClient.Tour'),
number: DS.attr('number')
});
JSON looks like this:
{
"tours": [
{
"id": "5110e8b5a8fefe71e0000197",
"title": "qui deserunt dolores",
"description": "Id velit nihil.",
"seats": 12,
"taken_seats": [
{
"id": "5110e8b5a8fefe71e0000196",
"number": "5"
},
{
"id": "5110e8b5a8feffffe0000196",
"number": "2"
}]
}
But yeah, when I do console.log(this.get('takenSeats').toArray() in Tour model's method, it returns Uncaught TypeError: Cannot call method '_create' of undefined, so, it seems that takenSeats did not load with parent model. What's wrong?
UPDATE
added tour_id to JSON, but now, when I want to use calculated property:
freeSeats: function() {
var seats = this.get('seats');
var takenSeats = this.get('takenSeats');
if (takenSeats) {
return (seats - takenSeats.length);
}
else {
return seats;
}
}.property('seats', 'takenSeats')
takenSeats is undefined.
UPDATE 2:
TravelClient.RESTSerializer = DS.RESTSerializer.extend({
init: function() {
this._super();
this.map('TravelClient.Tour',{
images:{embedded:'always'},
options:{embedded:'always'},
takenSeats:{embedded:'always'}
});
}
});
TravelClient.CUSTOMAdapter = DS.RESTAdapter.extend({
bulkCommit: false,
serializer: TravelClient.RESTSerializer.create(),
url: "http://192.168.1.27:3000",
buildURL: function(record, suffix) {
var s = this._super(record, suffix);
return s + ".json";
}
});
TravelClient.Store = DS.Store.extend({
revision: 11,
adapter: TravelClient.CUSTOMAdapter.create()
});
TravelClient.store = TravelClient.Store.create();
the TakenSeats computed property is perceived as a class because it's capitalized. Next to that embedded loading has to configured differently. Like so: This way the tour object becomes dirty when a takenseat changes.
DS.RESTAdapter.map('TravelClient.Tour', {
takenSeats: { embedded: 'always' },
});
Or: This way the tour doesn't become dirty.
DS.RESTAdapter.map('TravelClient.Tour', {
takenSeats: { embedded: 'load' },
});
Do this before you initialize your Store and Adapter. This will make the computed property unnecessary. You can just do tour.get('takenSeats');
Oh and you don't have to specify that the type is embedded anymore. The id's in the taken_seats array that link back to the tour need to be called tour_id.
{
"tours": [{
"id": "5110e8b5a8fefe71e0000197",
"taken_seats": [{
"id": "5110e8b5a8fefe71e0000196",
"tour_id": "5110e8b5a8fefe71e0000197"
"number": "5"
}]
}]
}
I had a similar problem to this. ember-model doesn't map through nested objects.
Your JSON output currently has all the data nested beneath the tours root.
If you have access to the API, then i suggest trying to remove the root, otherwise look into using your as the main object and then grabbing all the nested objects from there on down.
instead of this:
"tours": [{
"id": "5110e8b5a8fefe71e0000197",
"taken_seats": [{
"id": "5110e8b5a8fefe71e0000196",
"tour_id": "5110e8b5a8fefe71e0000197"
"number": "5"
}]
}]
make it look like this:
[{
"id": "5110e8b5a8fefe71e0000197",
"taken_seats": [{
"id": "5110e8b5a8fefe71e0000196",
"tour_id": "5110e8b5a8fefe71e0000197"
"number": "5"
}]
}]
its possible my syntax is off, but this similar idea worked for me.