updating template of child scope from parent scope in angular js - binding

I have this fiddle example where I am trying to set a value in an ng-repeat that lives in a different scope. This is a very basic example of a much bigger issue I am trying to solve. Basically I need to set a variable in the ng-repeat so that angular will update the template accordingly. The problem is that the template is in a child controller. So i use the $controller inject-able to access the variable. However, updating this variable does not cause the template to update. Even if I do a scope.$apply(). Anyone have any ideas? I am unsure on another way to do this...
var myApp = angular.module('myApp', []);
myApp.directive("custdirective", function() {
return {
restrict: 'A',
scope: 'false',
link: function(scope, element, attr) {
element.on("click", function() {
anotherscope.addList();
});
}
}
});
function AnotherController($scope) {
$scope.listtwo = new Array();
$scope.addList = function() {
$scope.listtwo = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
}
}
function MyCtrl($scope, $controller, $rootScope) {
anotherscope = $rootScope.$new();
$scope.anothercontroller = $controller(AnotherController, {
$scope: anotherscope
});
}​
To do this correctly, one would create a service. I made an updated fiddle of the correct way to do this here or:
var myApp = angular.module('myApp', []);
myApp.factory("mySharedService", function($rootScope) {
var sharedService = {};
sharedService.message = '';
sharedService.prepForBroadcast = function() {
this.message = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
this.broadcastItem();
};
sharedService.broadcastItem = function() {
$rootScope.$broadcast('handleBroadcast');
};
return sharedService;
});
myApp.directive("custdirective", function() {
return {
restrict: 'A',
scope: 'false',
link: function(scope, element, attr) {
element.on("click", function() {
debugger;
scope.handleClick();
});
}
}
});
function AnotherController($scope, sharedService) {
$scope.listtwo = new Array();
$scope.addList = function() {
$scope.listtwo = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
}
$scope.$on('handleBroadcast', function() {
$scope.listtwo = sharedService.message;
$scope.$apply();
});
}
function MyCtrl($scope, sharedService) {
$scope.handleClick = function() {
sharedService.prepForBroadcast();
};
}
MyCtrl.$inject = ['$scope', 'mySharedService'];
AnotherController.$inject = ['$scope', 'mySharedService'];​

Passing scopes around like that is a little wonky, and is almost guaranteed to break the testability of your Angular application.
I think you'd be better off creating a service that brokered changes between your controllers and your directive. The service would contain either the arrays you wished to update, or the functions you wished to call from your directive.
I'm afraid it's hard to write up an example of such a service, as I don't really understand what your ultimate goal is.

Related

JqGrid searchoptions with select2 existing value

I'm trying to integrate select2 for JqGrid filter form. I'm using JqGrid min 4.6 & Select2 min 4.0.1. The filter works fine but I'm unable to retrieve the value that has been set through select2 once the filter form is closed and reopened. i.e. dataInit e1 does not return the existing value of the select input. I must be doing something wrong?
JqGrid Column Model:
{
name: 'CurrencyID', hidden: true, search: true, stype: 'select', searchtype: 'number', searchoptions: {
searchhidden: true,
sopt: ['eq', 'ne'],
dataInit: function (el) {
intiGridFilterSelecr2Field(el, paramFromView.CurrencyOptions);
}
},
searchrules: { required: true }
},
Parameters:
#section scripts{
<script>
var paramFromView = {
CurrencyOptions: {
searchURL: '#Url.Action("GetCurrency", "Controller")',
detailURL: '#Url.Action("CurrencyDetailsJson", "Controller")',
idField: 'CurrencyID',
txtField: 'Description'
}
};
</script>
}
Select2 Helper:
function intiGridFilterSelecr2Field(element, options) {
var comboPageSize = 15;
var quietMillis = 200;
var placeHolderText = 'Choose...'
var defaults = {
searchURL: '',
detailURL: '',
idField: '',
txtField: ''
};
var options = $.extend({}, defaults, options);
var select2Element = $(element);
select2Element.select2({
width: 'element',
minimumInputLength: 1,
placeholder: placeHolderText,
ajax: {
url: options.searchURL,
dataType: 'json',
quietMillis: quietMillis,
cache: false,
data: function (params) {
return {
name: params.term,
page: params.page,
pageSize: comboPageSize
};
},
processResults: function (data) {
var more = (data.page * comboPageSize) < data.total;
var resultsArr = [];
for (var i = 0; i < data.result.length; i++) {
resultsArr.push({ id: data.result[i][options.idField], text: data.result[i][options.txtField] });
}
return { results: resultsArr, more: more };
}
},
}).each(function (index, element) {
var idCombo = $(this);
// The problem is that idCombo.val() is always empty.
// element:select2-hidden-accessible
if (idCombo.val() != null && idCombo.val().length > 0) {
$.ajax(options.detailURL, {
data: {
id: idCombo.val()
},
dataType: 'json',
cache: false
}).done(function (data) {
var optselected = select2Element.find('option').filter(function () { return this.value == data[idField] && this.text == data[txtField] && this.selected })
if (optselected == undefined || optselected.length == 0) {
var $optionContact = $("<option selected></option>").val(data[idField].toString()).text(data[txtField]);
var toBeRemoved = select2Element.find('option').filter(function () { return this.value == data[idField] });
if (toBeRemoved != undefined) {
toBeRemoved.remove();
}
select2Element.append($optionContact).trigger('change.select2');
}
});
}
});
}
When the filter is being set...
When Loading the existing filter. How do I pass this CurrencyID = 1 to select2 helper?
Update:
With Oleg's answer, I updated my code as below.
{
name: 'CurrencyID', hidden: true, searchtype: 'number', search: true,
stype: "select", searchoptions: {
searchhidden: true,
sopt: ["eq", "ne"],
dataUrl: paramFromView.CurrencyOptions.searchURL,
buildSelect: function (data) {
var obj = jQuery.parseJSON(data);
var i, options = [];
for (i = 0; i < obj.result.length; i++) {
options.push("<option value='" + obj.result[i][paramFromView.CurrencyOptions.idField] + "'>" +
obj.result[i][paramFromView.CurrencyOptions.txtField] + "</option>");
}
return "<select>" + options.join("") + "</select>";
},
noFilterText: "Any",
selectFilled: function (options) {
setTimeout(function () {
$(options.elem).select2({
width: 'element',
});
}, 0);
}
},
searchrules: { required: true }
},
I'm almost there with what I wanted to achieve. However I'm still facing some difficulties.
When the filter is initially loaded, value is selected on the dropdown but query value is empty. i.e. if the user clicks on the find button soon after the filter form is loaded, no filter will be set.
I still cannot get select2 styles working.
I can demonstrate how to use select2 with free jqGrid fork of jqGrid, which I develop. I get the demo from the README of the old version 4.14.1 (the current released version is 4.15.3) and modified it to demonstrate the usage of select2.
The main part of the code could be
stype: "select",
searchoptions: {
sopt: ["eq", "ne"],
...
selectFilled: function (options) {
setTimeout(function () {
$(options.elem).select2({
width: "100%"
});
}, 0);
}
}
See https://jsfiddle.net/Lae6kee7/2/. You can try to choose an option in the filter toolbar in "Shipped via" column and the open the search dialog. You will see that the select2 will have the same option selected.
If you would load the data via Ajax request posted by select2 than your code will be much more complex as it could be. It's important to understand that such way is really required only for really large set of possible value. I means the number of items larger as 100000 items for example. On the other side, the most use cases required less as 1000 options. In the case it would be more effective to load all the data as options of select and then convert the select to select2. select2, which uses local select works much more quickly from the users point of view.
The code will be easier in my opinion if you will use dataUrl instead of ajax option of select2. You can use dataUrl to return from the server all different values, which can be used in select2 and to use buildSelect to build <select> from JSON data returned from the server. The demo https://jsfiddle.net/Lae6kee7/23/ demonstrates that. I made the demo for JSFiddle, which supports Echo service (see here), which allows to simulate server responses. Your real code should contains mostly only dataUrl, buildSelect and the code of selectFilled, which I included above.
Additionally, I'd recommend you to consider to use <datalist> (see here for example), which could be good alternative to select2. All modern web browsers contains native support of <datalist> and thus <datalist> works very quickly. Try to search in the first Client column of my demos. You will see control, which will be very close to select2. Additional advantage of <datalist>: one will be able not search for only exact predefined values like test10, test11 or test12, but for substrings like 1. Compare
with
or
with

AngularJS and ui-grid interaction using $resource

I am brand new to angular JS and obviously to ui-grid as well. I got data to display in a grid using $resource and am trying to move to the next level by allowing editing and saving of rows etc.
I used Saving row data with AngularJS ui-grid $scope.saveRow as an example and created the Plunker http://plnkr.co/edit/Gj07SqU9uFIJlv1Ie6S5 to try it. But, for some reason I can't fathom, mine doesn't work and in fact it generates an exception at the line:
gridApi.rowEdit.on.saveRow(self, self.saveRow);
And I am at a total loss to understand why. I realize that the saveRow function is empty, but the goal at this stage is simply to get it called when the row has been edited.
Any help would be greatly appreciated.
The code of the Plunker follows:
(function() {
var app = angular.module('testGrid', ['ngResource', 'ui.grid', 'ui.grid.edit', 'ui.grid.rowEdit' /*, 'ui.grid.cellNav'*/ ]);
app.factory('Series', function($resource) {
return $resource('/api/series/:id', {
id: '#SeriesId'
});
});
var myData = [{
SeriesId: 1,
SeriesName: 'Series 1'
}, {
SeriesId: 2,
SeriesName: 'Series 2'
}];
app.directive('gridContent', function() {
var deleteTemplate = '<input type="button" value="Delete" ng-click="getExternalScopes().deleteRow(row)" />';
var commandheaderTemplate = '<input type="button" value="Add Series" ng-click="getExternalScopes().addNew()" />';
return {
restrict: 'E',
templateUrl: 'grid.html',
controllerAs: 'gridseries',
controller: function(Series) {
var self = this;
this.saveRow = function(rowEntity) {
i = 0;
};
this.gridOptions = {};
this.gridOptions.columnDefs = [{
name: 'SeriesId',
visible: false
}, {
name: 'SeriesName',
displayName: 'Name',
enableCellEdit: true
}, {
name: 'Command',
displayName: 'Command',
cellTemplate: deleteTemplate,
headerCellTemplate: commandheaderTemplate
}];
this.gridOptions.onRegisterApi = function(gridApi) {
self.gridApi = gridApi;
gridApi.rowEdit.on.saveRow(self, self.saveRow);
};
this.gridOptions.data = myData;
this.gridScope = {
deleteRow: function(row) {
var index = myData.indexOf(row.entity);
self.gridOptions.data.splice(index, 1);
},
addNew: function() {
self.gridOptions.data.push({
SeriesName: 'Add a name'
});
}
};
}
};
});
})();
I have no idea why the code didn't cut and paste properly but all the code is in the Plunker any way.
Thanks in advance.
I think the main problem here is that you're using a controller as syntax, rather than the $scope setup. Registering an event requires a $scope, as the event handler is then removed again upon the destroy event of that $scope.
A shorthand workaround is to use $rootScope instead, but this may over time give you a memory leak.
gridApi.rowEdit.on.saveRow($rootScope, self.saveRow);
Refer: http://plnkr.co/edit/Gj07SqU9uFIJlv1Ie6S5?p=preview
Since this code was also a bit old, I had to update to the new appScope arrangements rather than externalScope.

mottie tablesorter, custom filter synched over multiple tables

I have multiple tables with exactly the same custom filters. How do I sync the filters amongst all my tables so that if the user changes the filter on one of my table, it changes the same filters on all the other table as well? Is there an easy way to do this?
There is a demo on the home wiki page that demonstrates the method to use. I hope this is what you are requiring:
var ts = $.tablesorter,
sorting = false,
searching = false;
$('table')
.on('sortBegin filterEnd', function (e, filters) {
if (!(sorting || searching)) {
var table = this,
c = table.config,
filters = ts.getFilters(table),
$sibs = c.$table.siblings('.tablesorter');
if (!sorting) {
sorting = true;
$sibs.trigger('sorton', [c.sortList, function () {
setTimeout(function () {
sorting = false;
}, 500);
}]);
}
if (!searching) {
$sibs.each(function () {
ts.setFilters(this, filters, true);
});
setTimeout(function () {
searching = false;
}, 500);
}
}
})
.tablesorter({
theme: 'blue',
widthFixed: true,
widgets: ['filter']
});

Using jQuery UI datepicker with hour / minute dropdowns with KnockoutJS

I've got a view model that looks like this:
var ViewModel = function() {
var self = this;
self.hourOptions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23];
self.minuteOptions = [0, 15, 30, 45];
self.formatTimeOption = function(hour) {
if (hour < 10) return "0" + hour;
return hour.toString();
};
self.startDate = ko.observable(null);
self.startDateHour = ko.computed({
read: function() {
return new Date(self.startDate()).getHours();
},
write: function(value) {
var newDate = new Date(self.startDate());
newDate.setHours(value);
self.startDate(newDate);
}
});
self.startDateMinute = ko.computed({
read: function() {
return new Date(self.startDate()).getMinutes();
},
write: function(value) {
var newDate = new Date(self.startDate());
newDate.setMinutes(value);
self.startDate(newDate);
}
});
};
As you can see, I've got a writeable computed observable that updates the startDate hours / minutes when updated.
This is working, however when it does so, the datepicker input field displays the long form of the date, rather than (for example)
01/03/2013
JSFiddle of this is available here: http://jsfiddle.net/alexjamesbrown/2kSpL/9/
I solved this by adding the following custom binding handler, as taken from this answer
ko.bindingHandlers.datepicker = {
init: function(element, valueAccessor, allBindingsAccessor) {
//initialize datepicker with some optional options
var options = allBindingsAccessor().datepickerOptions || {};
$(element).datepicker(options);
//handle the field changing
ko.utils.registerEventHandler(element, "change", function() {
var observable = valueAccessor();
observable($(element).datepicker("getDate"));
});
//handle disposal (if KO removes by the template binding)
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
$(element).datepicker("destroy");
});
},
update: function(element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor()),
current = $(element).datepicker("getDate");
if (value - current !== 0) {
$(element).datepicker("setDate", value);
}
}
};
Here's a working JSFiddle:
http://jsfiddle.net/alexjamesbrown/v6hdS/

Linked jQuery sortable lists and Backbone collections

I'm still finding my way with Backbone and I've always use Prototype instead of jQuery in the past so please forgive me if I'm doing something stupid.
I'm trying to develop a UI containing several connected unordered lists where each sortable list is represented by a separate Backbone collection. I'm using ICanHaz and Mustache templates but that's not of importance for my question.
When dragging items between the lists, how would I best achieve the automatic updating of the collections (remove a model from one and insert it into another)? I'm currently trying to use the receive and remove methods in the jQueryUI Sortable interaction — am I at least on the right lines?
var WS = {};
(function(ns) {
ns.Item = Backbone.Model.extend();
ns.Content = Backbone.Collection.extend({
model: ns.Item,
url: location.href,
initialize: function(el) {
this.el = $(el);
this.deferred = this.fetch();
},
recalculate: function() {
var count = this.length;
this.el.next(".subtotal").html(count);
},
setOrder: function() {
$.ajax({
url: this.url + "/reorder",
type: "POST",
data: "tasks=" + $(this.el).attr("id") + "&" + this.el.sortable("serialize")
});
}
});
ns.ContentRow = Backbone.View.extend({
tagName: "li",
className: "item",
events: {
"click .delete": "destroy"
},
initialize: function(options) {
_.bindAll(this, "render", "destroy");
this.model.bind("change", this.render);
this.model.view = this;
},
render: function() {
var row = ich.item(this.model.toJSON());
$(this.el).html(row);
return this;
},
destroy: function() {
if (confirm("Really delete?")) {
this.model.destroy({
success: function(model, response) {
$(model.view.el).remove();
},
error: function(model, response) {
console.log(response);
}
});
}
}
});
ns.ListView = Backbone.View.extend({
initialize: function(collection) {
this.el = collection.el;
this.collection = collection;
this.collection.bind("add", this.addOne, this);
_.bindAll(this, "addOne");
this.el.sortable({
axis: "y",
connectWith: ".tasks",
receive: _.bind(function(event, ui) {
// do something here?
}, this),
remove: _.bind(function(event, ui) {
// do something here?
}, this),
update: _.bind(function(event, ui) {
var list = ui.item.context.parentNode;
this.collection.setOrder();
}, this)
});
},
insert: function(item) {
var prefix = this.el.parentsUntil('ul').parent().attr("id"),
view = new ns.ContentRow({
model: item,
id: prefix + "_" + item.id
});
this.el.append(view.render().el);
},
addOne: function(item) {
if (item.isNew()) {
item.save({}, {
success: _.bind(function(model, response) {
// I should set id from JSON response when live
model.set({ id: this.collection.length });
this.insert(model);
}, this)
});
} else {
this.insert(item);
}
},
addAll: function() {
this.collection.each(this.addOne);
},
render: function() {
this.collection.deferred.done(_.bind(function() {
this.addAll();
}, this));
}
});
ns.AppView = Backbone.View.extend({
lists: [],
initialize: function(holder) {
holder.find("ul").each(_.bind(function(index, list) {
var Items = new WS.Content(list),
App = new WS.ListView(Items);
App.render();
this.lists.push(Items);
}, this));
}
});
})(WS);
$(document).ready(function() {
var App = new WS.AppView($("#tasks"));
});
You are on the right track. You will probably want to add the id of each sortable element into the template somewhere. Then when you receive the event, you know which model to add or remove from the collection. For example add...
<div data-id={{id}}> ... my thing ... </div>
And in the sortable call get the target's id attribute and call Collection.add() or remove()
Just use Backbone.CollectionView.. it has this functionality built in out of the box.
var listView = new Backbone.CollectionView( {
el : $( "#list1" ),
sortable : true,
sortableOptions : {
connectWith : "#list2"
},
collection : new Backbone.Collection
} );
var listView = new Backbone.CollectionView( {
el: $( "#list2" ),
sortable : true,
sortableOptions : {
connectWith : "#list1"
},
collection : new Backbone.Collection
} );

Resources