Following issue: Let's say, we have an object like this:
$scope.Something = { 'a' : { object... }, 'b' : { another object... } }
This Something-object is also rendered in the view as follows:
<div ng-repeat="s in Something">{{ Something[s].someProperty }}</div>
The user wants to edit Something.a. For this, we show him a form. Before the form is shown, I save the current Something.a as a copy:
$scope.copyForUndo= angular.copy($scope.Something.a);
Now, if the user clicks "Cancel", it gets:
$scope.Something.a = angular.copy($scope.copyForUndo);
But since then, the association seems to disappear. No matter, what changes the user now makes to Something.a, the view doesn't get updated.
Why?
I know, what the equality of objects is (for example, that { object1: true } != {object1 : true} but still I cannot understand, why it doesn't work.
If you can make $scope.Something an array, then you can edit the copy and then update the array when the changes are saved. It still provides an undo, but in reverse of how you presented it.
fiddle here: http://jsfiddle.net/GYeSZ/1/
function MyCtrl($scope) {
$scope.Something = [
{ name: "Aye", desc: new Date() },
{ name: "Bee", desc: new Date() },
{ name: "See", desc: new Date() }
];
$scope.edit = function(idx) {
$scope.copy = angular.copy($scope.Something[idx]);
$scope.idx = idx;
}
$scope.save = function() {
$scope.Something[$scope.idx] = angular.copy($scope.copy);
$scope.cancel();
}
$scope.cancel = function() {
$scope.copy = null;
$scope.idx = -1;
}
}
Update
There is an alternate syntax for ng-repeat that can be used to enumerate dictionaries to get their key. Using this syntax, you can use the data structure you describe in the question
fiddle here: http://jsfiddle.net/GYeSZ/3/
function MyCtrl($scope) {
$scope.edit = function(key) {
$scope.copy = angular.copy($scope.Something[key]);
$scope.key = key;
}
$scope.Something = {
"a": { name: "Aye", desc: new Date() },
"b": { name: "Bee", desc: new Date() },
"c": { name: "See", desc: new Date() }
};
$scope.save = function() {
$scope.Something[$scope.key] = angular.copy($scope.copy);
$scope.cancel();
}
$scope.cancel = function() {
$scope.copy = null;
$scope.key = null;
}
}
Html
<div ng-repeat="(key, value) in Something" ....>
If the copy source is ng-repeat iterative item, you should use
angular.copy($scope.copyForUndo, $scopy.sourceItem)
insteads of
$scope.sourceItem = angular.copy($scope.copyForUndo)
Otherwise, your data-binding dom is not tracking because the $$hashkey of
the iterative item was erased by the misuse copy statement.
https://github.com/angular/angular.js/blob/g3_v1_2/src/Angular.js#L777
it seems a bit odd but if you can save the original array $scope.Something then on canceling you can rebind it.
// saving original array to get the original copy of edited object
var originalArrayCopy = angular.copy($scope.Something);
............
// When user clicks cancel then simply filter the originalArray to get the original copy, here i am assuming there is a field in object which can uniquely identify it.
// var originalObject = originalArrayCopy .filter(function(elm)
{
if(editedObject.Id == elm.Id)
return elm;
} );
// once i get the original object , i can rebind it to the currentObject which is being edited.
Non destructive form editing: http://egghead.io/lessons/angularjs-angular-copy-for-deep-copy
Synopsis:
create a pointer to the object that the user clicked edit
create a copy of object that user edits and can decide to save|cancel
I'm not sure if it has to do with the fact that you are copying a copy, or what, but here is a working plunker
Copy Example Plunker
Related
I've been working a project that allows a user to manage Option Types and Options. Basically user can add a new Option Type, let's say they name it Color and then they add the options - Black, Red, Purple, etc. When the collection first loads up the existing records, an empty option should be added at the end
When a user starts typing in the text field, I want to add a new empty option , thereby always giving the user a new field to work with.
I have this almost working, but can't figure how to properly add new empty option to a new Option Type or to existing option types. The push method keeps crashing Plunkr. Any input is appreciated, short sample review of the plunkr is below
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.optionTypeId = 0;
$scope.productOptionId = 0;
$scope.productEditorModel = {
"ProductOptions": [0],
"OptionTypes": [0]
};
$scope.productEditorModel.optionTypeName = '';
$scope.addEmptyOption = function (optionTypeId) {
var emptyOption = { ProductOptionId: 3, ProductId: '1066', OptionTypeId: 1, OptionValue: '', Price: '', IsStocked: true };
console.log(emptyOption);
//$scope.productEditorModel.ProductOptions.push(emptyOption);
};
$scope.loadData = function () {
$scope.productEditorModel.OptionTypes = [{ OptionTypeId: 1, OptionName: 'Color' },{ OptionTypeId: 2, OptionName: 'Size' },];
$scope.productEditorModel.ProductOptions = [{ ProductOptionId: 1, ProductId: '1066', OptionTypeId: 2, OptionValue: 'Medium', Price: '', IsStocked: true, },{ ProductOptionId: 2, ProductId: '1066', OptionTypeId: 1, OptionValue: 'Black', Price: '', IsStocked: true }];
angular.forEach($scope.productEditorModel.ProductOptions, function (item) {
//console.log(item.OptionTypeId);
$scope.addEmptyOption(item.OptionTypeId);
});
};
$scope.loadData();
$scope.removeOption = function (option) {
var index = $scope.productEditorModel.ProductOptions.indexOf(option);
$scope.productEditorModel.ProductOptions.splice(index, 1);
};
$scope.filterEmptyElements = function (optionTypeId) {
$scope.emptyElements = $.grep($scope.productEditorModel.ProductOptions, function (k) { return k.OptionValue === "" || angular.isUndefined(k.OptionValue) && k.OptionTypeId == optionTypeId });
};
$scope.update = function (option, index) {
var optionTypeId = option.OptionTypeId;
$scope.filterEmptyElements(optionTypeId);
if (!angular.isUndefined(option.OptionValue) && $scope.emptyElements.length == 1 && option.OptionValue.length > 0) {
$scope.addOption(optionTypeId);
} else if (angular.isUndefined(option.OptionValue)) {
$scope.removeOption(option);
}
};
$scope.addOptionType = function () {
var optionTypeId = --$scope.optionTypeId;
var optionName = $scope.productEditorModel.optionTypeName;
var newOptionType = { OptionTypeId: optionTypeId, OptionName: optionName };
$scope.productEditorModel.OptionTypes.push(newOptionType);
$scope.addEmptyOption(optionTypeId);
};
$scope.editOptionType = function (optionType) {
$scope.editing = true;
};
$scope.saveOptionType = function (optionType) {
$scope.editing = false;
};
$scope.trackOptionTypesCount = function () {
if ($scope.productEditorModel.OptionTypes.length == 3) {
$scope.isMaxOptionTypes = true;
} else {
$scope.isMaxOptionTypes = false;
}
};
$scope.removeOptionType = function (optionType) {
var index = $scope.productEditorModel.OptionTypes.indexOf(optionType);
$scope.productEditorModel.OptionTypes.splice(index, 1);
$scope.trackOptionTypesCount();
};
});
See the plunker below:
http://plnkr.co/edit/YHLtSwQWVb2swhNVTQzU?p=info
The error you get that $ is not defined is because you haven't included jQuery. You don't need jQuery for this though, array.map should be able to perform the same functionality.
$scope.emptyElements = $scope.productEditorModel.ProductOptions.map(function (k) {
return k.OptionValue === "" || angular.isUndefined(k.OptionValue) && k.OptionTypeId == optionTypeId
});
And it crashes because inside $scope.loadData you have
angular.forEach($scope.productEditorModel.ProductOptions, function (item) {
$scope.addEmptyOption(item.OptionTypeId);
});
and then inside $scope.addEmptyOption you try to
$scope.productEditorModel.ProductOptions.push(emptyOption);
So the foreach will loop for each item in $scope.productEditorModel.ProductOptions, which you keep adding options to so....? Infinite loop.
Non-crashing version: http://plnkr.co/edit/5Sc2sWfhKBs9kLCk83f1?p=preview
What you really should do though is look over your data structure. Make the ProductOptions a sub-object of OptionTypes and just rename it Options. Remove ALL code about creating id's here in your GUI, that should be handled by the backend. Instead in the GUI there should be a Sortorder property on the Options (which also of course gets stored by the backend). Then when you store, the ones without an id get inserted, the ones with an id get updated. Much easier to handle everything that way.
I'd also break out optionTypes and options to their own services/providers. Much easier to keep track of what needs to be done. And each just basically contains add, remove and maybe a find/getJSON or something.
Here's a restructured version. Much easier to keep track of what belongs where. And it has more features than the original with less code. http://plnkr.co/edit/BHcu6vAfcpEYQpZKHc5G?p=preview
So I am pulling in an object that I want to "edit", with a bit of help I have a function that finds the item i'm looking for and replaced the value. What I did no account for when building this was if the items don't exist yet.
So right now the function looks like this :
myFunction = function(moduleName, stateName, startVar){
//currentState brought in from factory controlling it
var currentState = StateFactory.current();
_.each(currentState, function(item) {
if (item.module === moduleName) {
_.each(item.customUrl, function(innerItem) {
if (_.has(innerItem, stateName)) {
innerItem[stateName] = startVar;
}
});
}
});
}
So - this does a great job of replacing the startVar value, assuming it already exists. I need to add some levels of checks to make sure the items exist (and if they don't add them in).
So, for reference, this is what the currentState looks like
[{
"module": "module1",
"customUrl": [
{ "mod1": "2" },
{ "mod2": "1" }
]
}, {
"module": "module2",
"customUrl": [
{ "mod3": "false" },
{ "mod4": "5" }
]
}
];
And so if i passed
myFunction("module1","mod1",3);
This works great, however if I pass
myFunction("module5","mod8","false");
Or maybe something in between like
myFunction("module1","mod30","false");
This function will not handle that scenario. I could use some helpe wrapping my head around how to tackle this problem. Also, am using underscore (if it is required to help). Thank you for taking the time to read!
As mentioned by Phari - something to the effect of this
currentState[moduleName].customUrl[stateName] = startVar;
I was thinking I could just create the object and just _.extend, but because it is an array of objects that wont quite work.
Here's what I mean :
var tempOb = {"module" : moduleName, "customUrl" : [{stateName : startVar}]};
_.extend(currentState, tempOb);
Doesn't quite work right with an array of objects.
It seems that all you need to do is remove the if statement:
if (_.has(innerItem, stateName)) {
innerItem[stateName] = startVar;
}
should become simply:
innerItem[stateName] = startVar;
Then if the property is not present, it will be added. If it is already present, it will be overwritten.
EDIT: to handle absence at top level:
myFunction = function(moduleName, stateName, startVar){
//currentState brought in from factory controlling it
var currentState = StateFactory.current();
var found = false;
_.each(currentState, function(item) {
if (item.module === moduleName) {
found = true;
_.each(item.customUrl, function(innerItem) {
if (_.has(innerItem, stateName)) {
innerItem[stateName] = startVar;
}
});
}
});
if ( ! found ) {
var newItem = {
module: moduleName,
customUrl: []
};
var newInnerItem = {};
newInnerItem[stateName] = startVar;
newItem.customUrl.push(newInnerItem);
currentState.push(newItem);
}
}
I am quite new to knockout.js, and I am enjoying learning how to make interfaces with it. But I have a bit of a wall while trying to make my interface more efficient. What I am trying to achieve is remove only the elements selected by $('.document_checkbox').serializeArray(), which contains the revision_id. I will then re-add the entries to the view model with a modified call to self.getDocument(), passing only the modified records which will be re-added. Can anyone help me how to remove the entries from the arrays based on the 'revision_id' values of $('.document_checkbox').serializeArray()
?
function Document(data) {
this.line_id = data.line_id
this.revision_id = ko.observable(data.revision_id);
this.status_id = ko.observable(data.status_id);
}
function DocumentViewModel() {
var self = this;
self.documents = ko.observableArray([]);
self.getDocument = function(){
//Reset arrays
self.documents.removeAll();
//Dynamically build section arrays
$.getJSON("/Documentation/Get-Section", function(allData) {
$.map(allData, function(item) {
var section = { name: item.array_name, display_name: item.display_name, documents: ko.observableArray([])};
self.documents.push(section);
})
//Add document objects to the arrays
$.getJSON("/Documentation/Get-Document", function(allData){
$.map(allData, function(item) {
var section = ko.utils.arrayFirst(self.documents(), function(documentSection) {
return documentSection.name === item.array_name;
});
section.documents.push(new Document(item));
});
});
});
}
self.updateStatusBatch = function(data,event){
$.post('/Documentation/Update-Status-Batch',
{
revision_id : $('.document_checkbox').serializeArray(),
status_id : event.currentTarget.value
}).done(
function(){
//This is where I get confused.
});
}
}
You should modify the /Documentation/Update-Status-Batch in order that it returns the deleted item id. So you will be able to remove it on the client side.
Try this "done" function:
function(removedItemId) {
self.documents.remove(function(doc){
return doc.status_id == removedItemId;
})
}
Take a look at the remove function.
I hope it helps.
In Angular, I have in scope a object which returns lots of objects. Each has an ID (this is stored in a flat file so no DB, and I seem to not be able to user ng-resource)
In my controller:
$scope.fish = [
{category:'freshwater', id:'1', name: 'trout', more:'false'},
{category:'freshwater', id:'2', name:'bass', more:'false'}
];
In my view I have additional information about the fish hidden by default with the ng-show more, but when I click the simple show more tab, I would like to call the function showdetails(fish.fish_id).
My function would look something like:
$scope.showdetails = function(fish_id) {
var fish = $scope.fish.get({id: fish_id});
fish.more = true;
}
Now in the the view the more details shows up. However after searching through the documentation I can't figure out how to search that fish array.
So how do I query the array? And in console how do I call debugger so that I have the $scope object to play with?
You can use the existing $filter service. I updated the fiddle above http://jsfiddle.net/gbW8Z/12/
$scope.showdetails = function(fish_id) {
var found = $filter('filter')($scope.fish, {id: fish_id}, true);
if (found.length) {
$scope.selected = JSON.stringify(found[0]);
} else {
$scope.selected = 'Not found';
}
}
Angular documentation is here http://docs.angularjs.org/api/ng.filter:filter
I know if that can help you a bit.
Here is something I tried to simulate for you.
Checkout the jsFiddle ;)
http://jsfiddle.net/migontech/gbW8Z/5/
Created a filter that you also can use in 'ng-repeat'
app.filter('getById', function() {
return function(input, id) {
var i=0, len=input.length;
for (; i<len; i++) {
if (+input[i].id == +id) {
return input[i];
}
}
return null;
}
});
Usage in controller:
app.controller('SomeController', ['$scope', '$filter', function($scope, $filter) {
$scope.fish = [{category:'freshwater', id:'1', name: 'trout', more:'false'}, {category:'freshwater', id:'2', name:'bass', more:'false'}]
$scope.showdetails = function(fish_id){
var found = $filter('getById')($scope.fish, fish_id);
console.log(found);
$scope.selected = JSON.stringify(found);
}
}]);
If there are any questions just let me know.
To add to #migontech's answer and also his address his comment that you could "probably make it more generic", here's a way to do it. The below will allow you to search by any property:
.filter('getByProperty', function() {
return function(propertyName, propertyValue, collection) {
var i=0, len=collection.length;
for (; i<len; i++) {
if (collection[i][propertyName] == +propertyValue) {
return collection[i];
}
}
return null;
}
});
The call to filter would then become:
var found = $filter('getByProperty')('id', fish_id, $scope.fish);
Note, I removed the unary(+) operator to allow for string-based matches...
A dirty and easy solution could look like
$scope.showdetails = function(fish_id) {
angular.forEach($scope.fish, function(fish, key) {
fish.more = fish.id == fish_id;
});
};
Angularjs already has filter option to do this ,
https://docs.angularjs.org/api/ng/filter/filter
Your solutions are correct but unnecessary complicated. You can use pure javascript filter function. This is your model:
$scope.fishes = [{category:'freshwater', id:'1', name: 'trout', more:'false'}, {category:'freshwater', id:'2', name:'bass', more:'false'}];
And this is your function:
$scope.showdetails = function(fish_id){
var found = $scope.fishes.filter({id : fish_id});
return found;
};
You can also use expression:
$scope.showdetails = function(fish_id){
var found = $scope.fishes.filter(function(fish){ return fish.id === fish_id });
return found;
};
More about this function: LINK
Saw this thread but I wanted to search for IDs that did not match my search. Code to do that:
found = $filter('filter')($scope.fish, {id: '!fish_id'}, false);
I am trying to bind a computed observable which internally uses a observable array.
The "read" method does get called while loading.
But the "write" method does not get called when the values in the table are changed and the focus is moved.
Note that for simple computed observables , which do not wrap a array but a simple string, the "write" method works. However for this scenario it does not work.
I looked on the Knockout api documentation and in online forums but could not find anything about this. Can someone please advice?
Following is the HTML code
<thead>
<tr><th>People Upper Case Name</th></tr>
</thead>
<tbody data-bind="foreach: uppercasepeople">
<tr>
<td ><input type="text" data-bind="value: name"/></td>
</tr>
</tbody>
</table>
Following is the Java Script code
<script type="text/javascript">
var MyViewModel = function () {
var self = this;
var self = this;
//Original observable array is in lower case
self.lowercasepeople = ko.observableArray([
{ name: "bert" },
{ name: "charles" },
{ name: "denise" }
]);
//coputed array is upper case which is data-bound to the ui
this.uppercasepeople = ko.computed({
read: function () {
alert('read does get called :)...yaaaa!!!');
return self.lowercasepeople().map(function (element) {
var o = { name: element.name.toUpperCase() };
return o;
});
},
write: function (value) {
alert('Write does not get called :(... why?????');
//do something with the value
self.lowercasepeople(value.map(function (element) { return element.toLowerCase(); }));
},
owner: self
});
}
ko.applyBindings(new MyViewModel());
I have put the code similar to an example shown on the Knockout API documentation so people easily relate.
The computed observable that you have only deals with the array itself. The write function would only get called if you tried to set the value of uppercasepeople directly.
In this case, you would likely want to use a writeable computed on the person object itself and make the name observable. The writeable computed would then convert the name to upper-case and when written would populate the name observable with the lower-case value.
var MyViewModel = function () {
var self = this;
//Original observable array is in lower case
self.lowercasepeople = ko.observableArray([
{ name: "bert" },
{ name: "charles" },
{ name: "denise" }
]);
self.recententlyChangedValue = ko.observable();
//coputed array is upper case which is data-bound to the ui
this.uppercasepeople = ko.computed({
read: function () {
alert('read does get called :)...yaaaa!!!');
return self.lowercasepeople().map(function (element) {
var o = { name: element.name.toUpperCase() };
return o;
});
},
write: function (value) {
alert('It will be called only when you change the value of uppercasepeople field');
//do something with the value
self.recententlyChangedValue(value);
},
owner: self
});
}