I basically want the equivalent to binding to 'add' and 'remove' events in Backbone's Collections. I see basically no way of doing this in AngularJS, and the current workaround we've settled for is $watch()ing the array's length and manually diffing/recalculating the whole thing. Is this really what the cool kids do?
Edit: Specifically, watching the array's length means I don't easily know which element has been changed, I need to manually "diff".
I think using $watch is a good solution, but $watchCollection can be better for you. $watchCollection doesn't perform deep comparison and just watchs for array modification like insert, delete or sort (not item update).
For exemple, if you want to keep an attribut order synchronize with the array order :
$scope.sortableItems = [
{order: 1, text: 'foo'},
{order: 2, text: 'bar'},
{order: 3, text: 'baz'}
];
$scope.$watchCollection('sortableItems', function(newCol, oldCol, scope) {
for (var index in newCol) {
var item = newCol[index];
item.order = parseInt(index) + 1;
}
});
But for your problem, I do not know if there is a better solution than manually browse the array to identify the change.
The way to watch an array in Angular is $watch(array, function(){} ,true)
I would create child scopes and watch them individually.
here is an example:
$scope.myCollection = [];
var addChild = function()
{
var Child = $scope.$new();
Child.name = 'Your Name here';
Child.$watch('name', function(newValue) {
// .... do something when the attribute 'name' is changed ...
});
Child.$on('$destroy', function() {
//... do something when this child gets destroyed
});
$scope.myCollection.push(Child); // add the child to collection array
};
// Pass the item to this method as parameter,
// do it within an ngRepeat of the collection in your views
$scope.deleteButtonClicked = function(item)
{
var index = $scope.myCollection.indexOf(item); //gets the item index
delete $scope.myCollection[index]; // removes the item on the array
item.$destroy(); // destroys the original items
}
Please tell more about your usecase. One of the solutions of tracking element persistance is using ngRepeat directive with custom directive that listening element's $destroy event:
<div ng-repeat="item in items" on-delete="doSomething(item)">
angular.module("app").directive("onDelete", function() {
return {
link: function (scope, element, attrs) {
element.on("$destroy", function () {
scope.$eval(attrs.onDelete);
});
}
}
});
Perhaps the solution is to create the collection class ( like backbone does ) and you can hook into events pretty easily as well.
The solution I have done here isnt really comprehensive, but should give you a general guidance on how this could be done perhaps.
http://beta.plnkr.co/edit/dGJFDhf9p5KJqeUfcTys?p=preview
Related
I have an array that looks like this:
items: [
{title: 'First Title', completed: false},
{title: 'Second Title', completed: false},
{title: 'Third Title', completed: false}
];
I'd like to set each item to true. For this I have a button that fires an event on-tap that executes the following code snippets.
The Polymer team sets the Boolean value with a for loop:
for (var i = 0; i < this.items.length; ++i) {
this.set(['items', i, 'completed'], true);
}
I personally prefer to use a forEach loop, because I'd like to compare Polymer to different frameworks and it happens to happen that I am using forEach loops in similar cases.
My working solution:
var that = this;
this.items.forEach(function(item) {
var i = that.items.indexOf(item);
that.set('items.' + i + '.completed', true);
// or
// that.set(['items', i, 'completed'], true);
});
Specifically the part where I use dots to connect with i seems hacky to me.
Same code with Vue:
this.items.forEach(function(item) {
return item.completed = true;
});
The Polymer API states:
set(path, value, root) path (string|Array<(string|number)>)
Path to the value to read. The path may be specified as a string (e.g. foo.bar.baz) or an array of path parts (e.g. ['foo.bar', 'baz']). Note that bracketed expressions are not supported; string-based path parts must be separated by dots. Note that when dereferencing array indicies, the index may be used as a dotted part directly (e.g. users.12.name or ['users', 12, 'name']).
value *
Value to set at the specified path.
root Object=
Root object from which the path is evaluated.
Question:
Because the part where I use an index seems just a bit hacky for a lack of a better term, I wonder, if there is a more convenient way to use a forEach loop in Polymer to update all items in the Array.
Thanks.
forEach's callback function can have a second parameter that refers to the current index. This also goes for Array's map, every, some, and filter methods.
ES5 Version
this.items.forEach(function(item, index) {
this.set('items.'+index+'.completed', true);
}.bind(this));
ES6 Version
this.items.forEach((item, index) => this.set(`items.${index}.completed`, true));
Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#Parameters
This may not be the completely ideal optimized way of accomplishing the task. I'm open for suggestions on any better ways. So far the loads work fine performancewise.
I have my knockout app working via ajax load. Inside the binding calls, I have a nested loop that includes a function that updates points based on a setting value.
When I attempt to add a new item, no errors are thrown, however the UI does not update and I can't seem to figure out why.
Here's a fiddle of what I'm trying to do.
http://jsfiddle.net/hjchvawr/2/
The addCombatant method does work, but for whatever reason the table will not rebind. You can see the added value in the VM json outputed to the console.
self.addCombatant = function(combatant){
ko.utils.arrayForEach(self.divisions(), function(d){
if(d.name() == combatant.division){
d.combatants().push({name: combatant.name,
ID: combatant.ID,
swords:{points: 0, time:'none', kills: 0}
});
}
console.log(ko.toJSON(self.divisions));
}
)}.bind(this);
EDIT:
I've applied some updates suggested below and added another list to sort. It binds and updates however, when I add a combatant, it only binds to one event and the sorting is off. If I can't use sortDivision(combatants, 'swords'), how do would I make the automatic sorting work? In this fiddle (http://jsfiddle.net/4Lhwerst/2/) I want the event sorted by kills, then time. Is it possible to get this multilevel sorting done client side without creating another observeableArray?
This is the foreach binding in your table.
<!-- ko foreach: $root.sortDivision(combatants, 'swords') -->
sortDivision is defined:
self.sortDivision = function (div, evt) {
return div.sortBy(evt, 'time', 'asc').sortBy(evt, 'kills', 'desc');
};
Your sortBy function creates a new observableArray. That is not the same observableArray as is being pushed to.
ko.observableArray.fn.sortBy = function (evt, fld, direction) {
var isdesc = direction && direction.toLowerCase() == 'desc';
return ko.observableArray(this.sort(function (a, b) {
a = ko.unwrap(evt ? a[evt][fld]() : a[fld]());
b = ko.unwrap(evt ? b[evt][fld]() : b[fld]());
return (a == b ? 0 : a < b ? -1 : 1) * (isdesc ? -1 : 1);
}));
};
You should use computeds (or pureComputeds) for things that are a re-presentation or re-combination of data. Store any data item in one place.
You are pushing into the underlying combatants array and thus bypassing change tracking. Either remove the parentheses (d.combatants.push) or call valueHasMutated after you are done.
You need either:
if(d.name() == combatant.division){
d.combatants.push({name: combatant.name,
ID: combatant.ID,
swords:{points: 0, time:'none', kills: 0}
});
}
Or:
if(d.name() == combatant.division){
d.combatants().push({name: combatant.name,
ID: combatant.ID,
swords:{points: 0, time:'none', kills: 0}
});
d.combatants.valueHasMutated();
}
I am trying to customize this working http://jsfiddle.net/markcoleman/JNqqU/ ,in current working fiddle directly object is assigned . where i am trying to change it to $scope.obj.items . passing object to directive is not working .
do i need to write some $watch fo r the variable ??? i am getting dynamic value that's why i am trying to pass Object value with this .
Code ::
<a href="#" pop-over items="obj.items", title="Mode of transport">
Show Pop over</a>
javascript Directive part ::
scope: {
items: '=',
title: '#'
}
Any suggestion ,
I am trying Below Fiddle
http://jsfiddle.net/JNqqU/652/
You can change your controller to this:
bootstrap.controller('maincnt', function ($scope) {
$scope.obj = { // declare the scope object here with a blank items
items: []
};
$scope.updateitem = function () {
alert('scope update called');
$scope.obj.items = ['car', 'truck', 'plane', 'bike']; // now update here
}
});
Checkout fiddle.
Yes you should be making a watcher.
$scope.$watchCollection('items', function (newValue, oldValue) {
if (newValue) {
buildTemplate(newValue);
}
});
Note: I used watchCollection because it is an array. If it were an object or simple value $watch would be used instead.
You don't need to wrap it into object, but don't 'rewrite' whole array in 'update' method, but push values into it:
bootstrap.controller('maincnt',function($scope){
$scope.items = [];
$scope.updateitem=function(){
alert('scope update called');
$scope.items.push('car', 'truck', 'plane', 'bike');
}
});
http://jsfiddle.net/btfu30k2/1/
$watch isn't necessary too.
You need two changes:
Change in HTML items :: {{obj.items}}
Change in Controller default obj items should be assigned with empty array ( $scope.obj={items:[]}; ) as popOver's $compile is looking for scope.items
See this Working fiddle
Also your testing code {{items | json }} in template can be removed after your observation.
What is the best way to view all jQuery data key-value pairs across every element (in jQuery 2.x)?
A selection-oriented approach ( e.g. $('*').data() ) obviously does not work, because the return value is tied to a single element.
I know that I can iterate over every element, checking each for data:
var allData = [];
$('html *').each(function() {
if($.hasData(this)) {
allData.push({ el: this, data: $(this).data() })
}
})
JSFiddle
This does produce the expected output, but iterating over each possible data key feels like a backwards approach to this problem.
Is there some way to find all element data directly?
N.B. I'm interested for debugging, not production code.
You could select every element within the body with $("body *") and apply jQuery's .filter() to it. Working example:
var $elementsContainingData $("body *").filter(function() {
if($.hasData(this)) return this;
});
console.log($elementsContainingData);
Edit
As #spokey mentioned before, there's an internal variable named "cache" within the jQuery object: $.cache.
This variable consists of a bunch of objects which contain keys like "data" or "events":
5: Object
data: Object
events: Object
handle: function (a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)}
__proto__: Object
You can iterate through that object and filter for the data:
var filteredCache = $.each($.cache,function() {
if(typeof this["data"] === "object") return this;
});
Here's an working example plus a function to merge that stuff into a single and more handy object consisting only of dataKey => dataValue pairings: Fiddle
Edit
As mentioned in comments this solution does not work in jQuery version 2.x since $.cache is deprecated.
My last suggestion is creating a hook for jQuerys data function in order to extend an own object$.dataCache = {}; each time data() is called.
Extending, replacing or adding jQuerys functions is done by accessing $.fn.functionName:
$.fn.data = function(fn,hook) {
return function() {
hook.apply(this,arguments);
return fn.apply(this,arguments);
}
}($.fn.data,function(key,value) {
var objReturn = {};
objReturn[key] = value;
$.extend($.dataCache,objReturn);
});
This also works great in jQuery version 2: Fiddle
What I'm trying to do:
I am trying to dynamically update a scope with AngularJS in a directive, based on the ngModel.
A little back story:
I noticed Angular is treating my ngModel strings as a string instead of an object. So if I have this:
ng-model="formdata.reports.first_name"
If I try to pull the ngModel in a directive, and assign something to it, I end up with $scope["formdata.reports.first_name"]. It treats it as a string instead of a nested object.
What I am doing now:
I figured the only way to get this to work would be to split the ngModel string into an array, so I am now working with:
models = ["formdata", "reports", "first_name"];
This works pretty good, and I am able to use dynamic values on a static length now, like this:
$scope[models[0]][models[1]][models[2]] = "Bob";
The question:
How do I make the length of the dynamic scope dynamic? I want this to be scalable for 100 nested objects if needed, or even just 1.
UPDATE:
I was able to make this semi-dynamic using if statements, but how would I use a for loop so I didn't have a "max"?
if (models[0]) {
if (models[1]) {
if (models[2]) {
if (models[3]) {
$scope[models[0]][models[1]][models[2]][models[3]] = "Bob";
} else {
$scope[models[0]][models[1]][models[2]] = "Bob";
}
} else {
$scope[models[0]][models[1]] = "Bob";
}
} else {
$scope[models[0]] = "Bob";
}
}
This is an answer to
I noticed Angular is treating my ngModel strings as a string instead of an object
Add the require property to your directive then add a fourth ctrl argument to your link function
app.directive('myDirective', function() {
return {
require: 'ngModel',
link: function(scope, element, attributes, ctrl) {
// Now you have access to ngModelController for whatever you passed in with the ng-model="" attribute
ctrl.$setViewValue('x');
}
};
});
Demonstration: http://plnkr.co/edit/Fcl4cUXpdE5w6fHMGUgC
Dynamic pathing:
var obj = $scope;
for (var i = 0; i<models.length-1; i++) {
obj = obj[models[i]];
}
obj[models[models.length-1]] = 'Bob';
Obviously no checks are made, so if the path is wrong it will fail with an error. I find your original problem with angular suspicious, perhaps you could explore a bit in that direction before you resort to this workaround.