How to achieve deprecated CompositeView functionality in Marionette 3+? - backbone.js

As stated in the latest Marionette docs:
CompositeView is deprecated. You should use the replaceElement option on Region.show and
render a CollectionView into a region inside a View to achieve this functionality.
I still can't understand how CompositeView functionality should be achieved now.
Previously, CompositeView was perfect for using with such template:
<script id="table-template" type="text/html">
<table>
<% if (items.length) { %>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<% } %>
<tbody></tbody>
<tfoot>
<tr>
<td colspan="3">some footer information</td>
</tr>
</tfoot>
</table>
new MyCompositeView({
template: "#table-template",
templateContext: function() {
return { items: this.collection.toJSON() };
}
// ... other options
});
If we decide to use LayoutView instead of CompositeView then we need to code manually a lot of event bindings (for example to show / hide table header based on number of items in collection). This makes things harder.
Are there any clean and not complicated ways to live without CompositeView?
Thank you for any help or advice.

It looks like Marionette 3 is going to get rid of some concepts to make the framework simpler overall, and easier to understand.
Marionette.View in 3 is going to include functionality from what was ItemView and LayoutView. CompositeView is deprecated in favour of just using RegionManager, which is now included into View.
v2 --> v3
View -> AbstractView
ItemView, LayoutView -> View
Here's a quick example app:
var color_data = [ { title:'red' }, { title:'green' }, { title:'blue' } ];
var Color = Backbone.Model.extend({
defaults: { title: '' }
});
var Colors = Backbone.Collection.extend({
model: Color
});
var ColorView = Mn.View.extend({
tagName: 'tr',
template: '#colorTpl'
});
var ColorsView = Mn.CollectionView.extend({
tagName: 'tbody',
childView: ColorView
});
var AppView = Mn.View.extend({
template: '#appTpl',
templateContext: function(){
return {
items: this.collection.toJSON()
};
},
ui: {
input: '.input'
},
regions: {
list: {
selector: 'tbody',
replaceElement: true
},
},
onRender: function(){
this.getRegion('list').show(new ColorsView({
collection: this.collection
}));
},
events: {
'submit form': 'onSubmit'
},
onSubmit: function(e){
e.preventDefault();
this.collection.add({
title: this.ui.input.val()
});
this.ui.input.val('');
}
});
var appView = new AppView({
collection: new Colors(color_data)
});
appView.render().$el.appendTo(document.body);
<script src='http://libjs.surge.sh/jquery2.2.2-underscore1.8.3-backbone1.3.2-radio1.0.4-babysitter0.1.11-marionette3rc1.js'></script>
<script id="colorTpl" type="text/template">
<td><%=title%></td>
<td style="background-color:<%=title%>"> </td>
</script>
<script id="appTpl" type="text/template">
<table width="100%">
<% if(items.length) { %>
<thead>
<tr>
<th width="1%">Title</th>
<th>Color</th>
</tr>
</thead>
<% } %>
<tbody></tbody>
<tfoot>
<tr>
<td colspan="2">
<form><input type="text" class="input" autofocus><input type="submit" value="Add Color"></form>
</td>
</tr>
</tfoot>
</table>
</script>

Related

ng-click, ng-model not working in angularjs datatable

I have a datatable with column filters made with AngularJS.
Here is the HTML:
<body ng-app="myApp" ng-controller="appController as Ctrl">
<table class="table table-bordered table-striped table-hover dataTable js-exportable" datatable="ng" dt-options="Ctrl.dtOptions" dt-columns="Ctrl.dtColumns">
<thead>
<tr>
<th></th>
<th>Name</th>
</tr>
</thead>
<tfoot>
<tr>
<th></th>
<th>Name</th>
</tr>
</tfoot>
<tbody>
<tr ng-repeat="user in userList">
<td>
<input type="checkbox" id="user-{{ $index }}" ng-model="Ctrl.checkboxValue[$index]" ng-click="Ctrl.checkValue(user.id)" ng-true-value="{{user.id}}" />
<label for="user-{{ $index }}"></label>
</td>
<td>
<a href="#">
{{ ::user.name }}
</a>
</td>
</tr>
</tbody>
</table>
Here's the script:
angular.module('myApp', ['ngAnimate', 'ngSanitize', 'datatables', 'datatables.columnfilter'])
.controller('appController', function($scope, $compile, DTOptionsBuilder, DTColumnBuilder){
$scope.userList = [
{
id: '1',
name: 'hello'
},
{
id: '2',
name: 'hi'
}
];
var vm = this;
vm.dtOptions = DTOptionsBuilder.newOptions()
.withPaginationType('full_numbers')
.withOption('createdRow', function (row, data, dataIndex) {
$compile(angular.element(row).contents())($scope);
})
.withColumnFilter({
aoColumns: [{
}, {
type: 'text',
bRegex: true,
bSmart: true
}]
});
vm.dtColumns = [
DTColumnBuilder.newColumn('').withTitle(''),
DTColumnBuilder.newColumn('name').withTitle('Name'),
];
vm.checkboxValue = [];
vm.checkValue = function(id){
console.log(id);
}
});
Issues:
id of the user does not get passed to checkValue function. Hence, the console.log is undefined.
Suppose if the checkbox of 1st user is checked, the value of checkboxValue array is [undefined: '1']. If checkbox of 2nd user is checked the value of checkboxValue array becomes [undefined: '2'].
Only one checkbox gets checked. Why is that?
Demo: https://plnkr.co/edit/A3PJfBuwtpUQFAIz8hW7?p=preview
You kill your code with redundancy. Look at this :
When using the angular way, you CANNOT use the dt-column directive.
Indeed, the module will render the datatable after the promise is
resolved. So for DataTables, it's like rendering a static table.
You are in fact using the "angular way" along with dt-columns. You could switch to use DTColumnDefBuilder but why define the columns when you already have a <thead> section? It would only make sense if you need to use sorting plugins etc on specific columns. And / or not is specifying the header in the markup.
Moreover, when you are rendering with angular it is not necessary to $compile anything, in fact is is very wrong, angular already does that. So
remove your dt-columns or replace it with a dt-column-defs literal
remove your $compile from the createdRow callback
Then it works. I would also remove the ng-true-value="{{user.id}}" attribute. You want an array representing the checkboxes state, why set the state to the user.id and not true or false?
vm.dtOptions = DTOptionsBuilder.newOptions()
.withPaginationType('full_numbers')
.withColumnFilter({
aoColumns: [{
}, {
type: 'text',
bRegex: true,
bSmart: true
}]
});
and
<input type="checkbox" id="user-{{ $index }}" ng-model="Ctrl.checkboxValue[user.id]" ng-click="Ctrl.checkValue(user.id)" />
Is really all you need.
forked plunkr -> https://plnkr.co/edit/Z82oHi0m9Uj37LcdUSEW?p=preview

Using AngularJS, how can I match items from two separate ng-repeats?

Using AngularJS, I am creating a table that is pulling data with two web requests.
Each web request has it's own ng-repeat in the HTML, ng-repeat="user in users" and ng-repeat="app in apps". Right now all existing apps are showing in every repeat of user. What I'd like to do is some kind of match, lookup, or filter and only show apps that the user is associated with. So, when user.Title == app.Title.
Here is the HTML:
<div ng-app="myApp">
<div ng-controller="MainCtrl">
<div class="ProfileSheet" ng-repeat="user in users">
<h3 class="heading">User Profile</h3>
<table id="Profile">
<tr>
<th>User</th>
<td>{{user.Title}}</td>
</tr>
<tr>
<th>First Name</th>
<td>{{user.FirstName}}</td>
</tr>
<tr>
<th>Last Name</th>
<td>{{user.LastName}}</td>
</tr>
<tr>
<th>Job Title</th>
<td>{{user.JobTitle}}</td>
</tr>
<tr>
<th>Emp ID</th>
<td>{{user.EmployeeID}}</td>
</tr>
<tr>
<th>Officer Code</th>
<td>{{user.OfficerCode}}</td>
</tr>
<tr>
<th>Email</th>
<td>{{user.Email}}</td>
</tr>
<tr>
<th>Telephone</th>
<td>{{user.WorkPhone}}</td>
</tr>
<tr>
<th>Fax Number</th>
<td>{{user.WorkFax}}</td>
</tr>
<tr>
<th>Location Description</th>
<td>{{user.LocationDescription}}</td>
</tr>
<tr>
<th>Mailstop / Banking Center #</th>
<td>{{user.Mailstop}}</td>
</tr>
</table>
<br>
<h3 class="heading">User Applications</h3>
<div style="border:3px solid #707070; padding-right:12px;">
<h4 style="padding-left:5px;">User Applications</h4>
<table id="Apps">
<tr id="AppsHeading">
<th>Application</th>
<th>User ID</th>
<th>Initial Password</th>
<th>Options / Comment</th>
<th>Setup Status</th>
</tr>
<tr ng-repeat="app in apps">
<td>{{app.Application.Title}}</td>
<td>{{app.Title}}</td>
<td>{{app.InitialPassword}}</td>
<td>{{app.OptionsComments}}</td>
<td style="border-right:3px solid #707070;">{{app.SetupStatus}}</td>
</tr>
</table>
</div>
</div>
</div>
The JS:
var app = angular.module('myApp', ['ngSanitize']);
var basePath = "https://portal.oldnational.com/corporate/projecthub/anchormn/associates"
app.controller('MainCtrl', function($scope, $http, $q){
var supportList;
$(document).ready(function() {
$scope.getAdminList();
$scope.getAppsList();
});
// $scope.selectedIdx = -1;
// $scope.showResults = false
$scope.prepContext = function(url,listname,query){
var path = url + "/_api/web/lists/getbytitle('" + listname + "')/items" + query;
console.log(path);
return path;
}
$scope.getAdminList = function() {
supportList = $http({
method: 'GET',
url: this.prepContext(siteOrigin+"/corporate/projecthub/anchormn/associates","User Administration","?$orderBy=LastName"),
headers: {
"Accept": "application/json; odata=verbose"
}
}).then(function(data) {
//$("#articleSection").fadeIn(2000);
console.log("adminlist", data.data.d.results);
$scope.users = data.data.d.results;
});
};
$scope.getAppsList = function() {
supportList = $http({
method: 'GET',
// url: this.prepContext(siteOrigin+"/corporate/projecthub/anchormn/associates","User Applications","?$select=Title,InitialPassword,OptionsComments,SetupStatus,Application/Title&$orderBy=Application&$expand=Application"),
url: this.prepContext(siteOrigin+"/corporate/projecthub/anchormn/associates","User Applications","?$select=Title,InitialPassword,OptionsComments,SetupStatus,Application/Title,ParentUserLink/ID&$orderBy=Application&$expand=Application,ParentUserLink"),
headers: {
"Accept": "application/json; odata=verbose"
}
}).then(function(data) {
//$("#articleSection").fadeIn(2000);
console.log("appslist", data.data.d.results);
$scope.apps = data.data.d.results;
});
};
});
app.config([
'$compileProvider',
function($compileProvider){
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|chrom-extension|javascript):/)
}
]);
How can I do this?
There's a lot of extraneous code in the controller. For the purposes of this answer I removed it. Also I understand that users and apps are related by a property called Title but the name was confusing me - forgive me if the data doesn't make sense.
Suggestion: Only use $(jQuery) where strictly necessary. Angular provides a lot of functionality that replaces jQuery functionality. Instead of using $.ready like:
$(document).ready(function() {
$scope.getAdminList();
$scope.getAppsList();
});
wait to bootstrap your application until the document is ready using the following code:
(function () {
angular.element(document).ready(function () {
angular.bootstrap(document, ['myApp']);
});
})();
Then you don't have to burden the controller with the responsibility of waiting until the document is loaded. Note: ng-app was removed from the markup.
Suggestion: Use $q.all() to wait for multiple promises to resolve. $q.all() will wait until all promises resolve to call .then(). This helps to ensure that all data is available when you start to use it.
Suggestion: Only assign properties and functions to $scope if they will be used by the view. I removed the functions that are not used by the view from the $scope object.
How does it work?
When the controller loads, we use $q.all() to invoke and wait for fetchAdminList() and fetchAppsList() to fetch data from an API. Once each API request resolves we unwrap the data in .then() callbacks and return it (read more on promise chaining to understand what happens when a value is returned from .then()). When both promises resolve, we store the data on $scope to make it available to the view. We also pre-compute which applications each user can use and store that data in $scope.userApps to make it available to the view.
I did not have access to the APIs you are fetching data from. I substituted $http calls with an immediately resolving promise using $q.resolve() and static data. When you are ready just replace $q.resolve(...) with the original $http(...) calls in the fetch functions.
Run the snippet to see it in action.
var app = angular.module('myApp', []);
(function () {
angular.element(document).ready(function () {
angular.bootstrap(document, ['myApp']);
});
})();
var USERS = [
{
Title: 'Software Engineer',
FirstName: 'C',
LastName: 'Foster',
EmployeeID: 1
},
{
Title: 'Software Engineer',
FirstName: 'J',
LastName: 'Hawkins',
EmployeeID: 2
},
{
Title: 'CEO',
FirstName: 'Somebody',
LastName: 'Else',
EmployeeID: 3
}
];
var APPS = [
{
Application: { Title: 'StackOverflow' },
Title: 'Software Engineer'
},
{
Application: { Title: 'Chrome' },
Title: 'Software Engineer'
},
{
Application: { Title: 'QuickBooks' },
Title: 'CEO'
}
]
app.controller('MainCtrl', function ($scope, $http, $q) {
$q.all({
users: fetchAdminList(),
apps: fetchAppsList()
})
.then(function(result) {
// Store results on $scope
$scope.users = result.users;
$scope.apps = result.apps;
// Pre-compute user apps
$scope.userApps = $scope.users.reduce(
function(userApps, user) {
userApps[user.EmployeeID] = getUserApps(user.Title);
return userApps;
},
[]
);
});
function fetchAdminList() {
return $q.resolve({ data: { d: { results: USERS } } })
.then(function (data) { return data.data.d.results; });
}
function fetchAppsList() {
return $q.resolve({ data: { d: { results: APPS } } })
.then(function (data) { return data.data.d.results; });
}
// Get a list of apps that apply to user title
function getUserApps(userTitle) {
return $scope.apps.filter(function(app) {
return app.Title === userTitle;
});
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.js"></script>
<div>
<div ng-controller="MainCtrl">
<div class="ProfileSheet" ng-repeat="user in users">
<h3 class="heading">User Profile</h3>
<table id="Profile">
<tr>
<th>User</th>
<td>{{user.Title}}</td>
</tr>
<tr>
<th>First Name</th>
<td>{{user.FirstName}}</td>
</tr>
<tr>
<th>Last Name</th>
<td>{{user.LastName}}</td>
</tr>
<tr>
<th>Job Title</th>
<td>{{user.JobTitle}}</td>
</tr>
<tr>
<th>Emp ID</th>
<td>{{user.EmployeeID}}</td>
</tr>
<tr>
<th>Officer Code</th>
<td>{{user.OfficerCode}}</td>
</tr>
<tr>
<th>Email</th>
<td>{{user.Email}}</td>
</tr>
<tr>
<th>Telephone</th>
<td>{{user.WorkPhone}}</td>
</tr>
<tr>
<th>Fax Number</th>
<td>{{user.WorkFax}}</td>
</tr>
<tr>
<th>Location Description</th>
<td>{{user.LocationDescription}}</td>
</tr>
<tr>
<th>Mailstop / Banking Center #</th>
<td>{{user.Mailstop}}</td>
</tr>
</table>
<br>
<h3 class="heading">User Applications</h3>
<div style="border:3px solid #707070; padding-right:12px;">
<h4 style="padding-left:5px;">User Applications</h4>
<table id="Apps">
<tr id="AppsHeading">
<th>Application</th>
<th>User ID</th>
<th>Initial Password</th>
<th>Options / Comment</th>
<th>Setup Status</th>
</tr>
<tr ng-repeat="app in userApps[user.EmployeeID]">
<td>{{app.Application.Title}}</td>
<td>{{app.Title}}</td>
<td>{{app.InitialPassword}}</td>
<td>{{app.OptionsComments}}</td>
<td style="border-right:3px solid #707070;">{{app.SetupStatus}}</td>
</tr>
</table>
</div>
</div>
</div>

Adding form values to a table with backbone and handlebars

The scenario is to add the values entered in form fields into a table on click of Add button. I am new to this both and not sure how data binding works.
My initial html is
<table class="table table-bordered">
<thead>
<tr>
<th>Model</th>
<th>Brand</th>
<th>Year</th>
<th>Action</th>
</tr>
<tr>
<td><input class="form-control" id="modelname"></td>
<td><input class="form-control" id="brandname"></td>
<td><input class="form-control" id="yearname"></td>
<td><button class="btn btn-primary add">Add</button></td>
</tr>
</thead>
<tbody class="product-list">
</tbody>
</table>
</div>
<script type="text/x-handlebars-template" id="product-template">
{{#each []}}
<tr>
<td>{{ model }}</td>
<td>{{ brand }}</td>
<td>{{ year }}</td>
<td><div class="btn btn-primary">Edit</div> <div class="btn btn-danger">Delete</div></td>
</tr>
{{/each}}
</script>
I messed up in js for purpose of using handlebars as
var Product=Backbone.Model.extend({
model:'',
brand:'',
year:''
});
var ProductCollection=Backbone.Collection.extend({
model:Product
});
var modelname= document.getElementById('modelname').value;
var brandname=document.getElementById('brandname').value;
var yearname= document.getElementById('yearname').value;
var ProductView = Backbone.View.extend({
el: '.product-list',
tagName: 'tr',
events:{
"click .add": "create"
},
initialize:function(){
this.render();
},
render:function()
{
var source=$('#product-template').html();
var template=Handlebars.compile(source);
var html=template(this.products.toJSON());
},
create: function(e){
var product=new Product({
model:modelname,
brand:brandname,
year:yearname
})
console.log(product.toJSON);
products.add(product);
modelname="";
yearname="";
brandname="";
}
});
var products=new ProductCollection();
Share me an idea how to proceed. I don't get an error and at the same time, nothing happens! I am very new to backbone. Please tolerate blunders.
I make and example how can achieve that with underscore template and handlebars. Use it for iterating over a collection of models to display a list of products.
Underscore.js
<tbody class="product-list">
<script type="text/template" id="product-template">
<% _.each(products.models, function(product){ %>
<tr>
<td><%= product.get('modelName') %></td>
<td><%= product.get('brand') %></td>
<td><%= product.get('year') %></td>
</tr>
<% }) %>
</script>
</tbody>
In script file define model:
var Product = Backbone.Model.extend({});
Next, define a collection and add those model to the collection:
var ProductList = Backbone.Collection.extend({
model: Product
});
Most of the time we use view in Backbone application to do the rendering:
var ProductView = Backbone.View.extend({
el: '.product-list',
template: _.template($('#product-template').html()),
render: function(products){
this.$el.html(this.template({
products: products
}));
}
});
You can see from working code full app, and see that we call render method from productView and pass it collection as argument: this.productView.render(this.collection)
Now we can use it as a list in template to iterate and display modelName, brand and year for each product in lists.
Working code: jsFiddle
I am particularly asked to use handlebars
Handlebars.js
You have many errors in your code:
define instance of view var products = new ProductView();, instead of that you define instance of ProductCollection();
var html=template(this.products.toJSON()); Cannot read property 'toJSON' of undefined , check initialize method in working example, you need to define collection and listen to him, because every time we add something to collection we want to render ProductView
el: '.table' not el: '.product-list',
var modelname= document.getElementById('modelname').value; var brandname ... - you make them as a global variables, instead of that place that variables inside create() method
replace var html=template(this.products.toJSON()); with $('.product-list').html(template(this.products.toJSON()))
First of all read documentation if something isn't clear: backbone.js and check working example: jsFiddle

Backbonejs -collection is not defined

I'm getting a collection is not defined error in backbone.
var CategoryList = Backbone.View.extend({
el:'#content',
render: function() {
var that = this;
var cats = new Categories();
cats.fetch({
success: function(cats) {
var template = _.template($('#category-list-template').html(), {cats: cats.models});
cats.each(function(it) {
console.log(it.toJSON());
});
that.$el.html(template);
}
})
}
});
And in this script I'm running an each loop to add data to the table, but I'm getting an 'cats is not defined error'.
<script type="text/template" id="category-list-template">
<table class="table striped">
<thead>
<tr>
<td>Id</td>
<td>Name</td>
</tr>
</thead>
<tbody>
<% _.each(cats, function(category) { %>
<tr>
<td><%= category.name %> </td>
</tr>
<% }); %>
</tbody>
</table>
</script>
You have to transform collection to plain javascript object using toJSON method.
var template = _.template($('#category-list-template').text());
var result = template({collection: cats.toJSON()});
that.$el.html(result);

Breaking the ice on angular

I have a view:
<div id="productList" ng-controller="ProductController">
<table class="table table-bordered">
<thead>
<tr>
<th></th>
<th width="100%">Item ID</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in productItems">
<td><input type="checkbox" /></td>
<td><% item.ID %></td>
</tr>
</tbody>
</table>
</div>
I have a controller:
var TestApp = angular.module('TestApp', [], function( $interpolateProvider ) {
$interpolateProvider.startSymbol('<%');
$interpolateProvider.endSymbol('%>');
});
TestApp.controller('ProductController', function ( $scope ) {
ProductRepository.GetPaginatedProducts();
$scope.productItems = ProductRepository.Model;
});
I have a model:
var ProductRepository = function ()
{
return {
Model: null,
GetPaginatedProducts: function()
{
$.ajax( {
"url": Test.URL + "/json/products/paginated",
"dataType": "json",
"method": "post",
"success": function( data )
{
ProductRepository.Model = data;
}
} );
},
}
}
When the ajax finished, it updates the ProductRepository.Model data variable which I want the angular controller scope.productItems to work off of.
This is my first time using angular and i think I've missed the point,
Why is the table not updating with the information?
Please see here for sample ajax call http://plnkr.co/edit/5CiC8MWiwo010nwu32he?p=preview
var TestApp = angular.module('plunker', []);
TestApp.factory('ProductRepository', function($http, $q) {
var Model = [];
return {
Model: Model,
GetPaginatedProducts: function() {
var deferred = $q.defer();
$http.get('paginated.json').then(
//sucess
function(result) {
deferred.resolve(result.data)
}
//error
, function() {});
return deferred.promise;
}
};
})
TestApp.controller('ProductController', function($scope, ProductRepository) {
$scope.productItems = [];
ProductRepository.GetPaginatedProducts().then(function(data) {
//success
$scope.productItems = data;
},
//error
function() {
alert("can't get data");
})
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="plunker">
<div id="productList" ng-controller="ProductController">
<table class="table table-bordered">
<thead>
<tr>
<th></th>
<th width="100%">Item ID</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in productItems">
<td>
<input type="checkbox" />
</td>
<td>{{item.ID}}</td>
</tr>
</tbody>
</table>
</div>
</div>
As #tymeJV stated in the comment, you should use $http or $resource (or restangular) to perform the data access. If you are updating values outside of Angular's framework, it has no way of knowing that data has changed. By using something like $http or $resource, Angular is aware when an event has occurred that can change values and automatically checks for updates.
Alternately, you could manually issues an $apply() to essentially handle the digest update manually (ensuring Angular goes through a digest cycle when you've changed values outside of Angular). Take a look at the documentation on $apply() at https://docs.angularjs.org/api/ng/type/$rootScope.Scope .
However, your best bet is to use the Angular approach in the first place and avoid using jQuery for this.

Resources