I am having an issue saving changes back to my observable viewmodel using KnockoutJS. To keep this simple I will just show the basics....My viewmodel has a class called EmployeeSkill, which has two properties and the EmployeeSkillsViewModel is an array that contains zero to many of these skills as shown below in my Index.cshtml page. I have a variable: ctrl which holds a reference to the input control next to the button I click to open a modal dialog which allows the user to enter an existing OR new value in its input control. When the save button is clicked the database is checked or updated and an integer value is returned. I would like to save this returned value to my model.
The viewmodel is populated with JSON that looks like this:
[{"EmployeeSkillId": 1, "SkillsId": 999}, {"EmployeeSkillId": 2, "SkillsId": 777}, {"EmployeeSkillId": 3, "SkillsId": 888}]
<script type="text/javascript">
var ctrl;
$(document).on("click", ".open-editSkillName", function () {
ctrl = $(this).closest('.input-group').find('input');
$(".modal-body #edit_skill_name").val($(this).closest('.input-group').find('input').val());
});
$(function () {
function EmployeeSkillsViewModel() {
var self = this;
self.EmployeeSkillId = ko.observable("");
self.SkillsId = ko.observable("");
var EmployeeSkill = {
EmployeeSkillId: self.EmployeeSkillId,
SkillsId: self.SkillsId,
};
self.EmployeeSkill = ko.observable();
self.EmployeeSkillsArray = ko.observableArray();
self.updateSkillName = function (ctrl) {
var skillName = $(".modal-body #edit_skill_name").val();
$(ctrl).val(skillName);
// Check to see if this skill exists...
$.ajax({
url: '#Url.Action("UpsertSkill", "EmployeeSkills")',
cache: false,
type: 'POST',
contentType: 'application/json; charset=utf-8',
data: ko.toJSON({ 'skillName': skillName }),
success: function (data) {
// alert(data.SkillsId) --> alerts expected int value
self.SkillsId(data.SkillsId);
ko.applyBindings(self.EmployeeSkillsArray);
}
}).fail(function (xhr, textStatus, err) {
alert(err);
});
}
var viewModel = new EmployeeSkillsViewModel();
ko.applyBindings(viewModel);
});
</script>
I have a modal popup that allows the user to enter a value into the input and when they click the btnSaveSkillName the function updateSkillName (above) is called
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="close-editSkillName btn btn-primary" data-dismiss="modal" id="btnSaveSkillName" data-bind="click: $root.updateSkillName(ctrl)">Update Skill Name</button>
</div>
I pass this value in the skillName property to my controller and return in my response an integer value with the id of the skill name entered. I would like to update the row of underlying view model with this new integer value but I am not finding an easy way to do so.
Related
I'm sure this is probably easy, but I am messing up ajax calls here. I am pretty new to javascript so I don't really know what I'm doing wrong. I've tried to look it up online and can't get my calls to work at the correct time. Any help is really appreciated.
All i am trying to do is get NHL player data from a json to a table i created using angularjs. Right now the table is displayed when $scope.players is undefined, but once the ajax completes it has data. I am not displaying it at the right time, my table is always empty
RosterController related code:
(function () {
'use strict';
angular.module("DevilsFanApp")
.controller("RosterController", RosterController);
function RosterController($rootScope, $scope, RosterService) {
$scope.players = RosterService.players;
$scope.addPlayer = addPlayer;
$scope.updatePlayer = updatePlayer;
$scope.deletePlayer = deletePlayer;
$scope.selectPlayer = selectPlayer;
$scope.fetchPlayers = fetchPlayers;
function init() {
fetchPlayers(function(res){
$scope.players = RosterService.players;
console.log("Goalies Now", $scope.players);
});
}
init();
function fetchPlayers(callback) {
var players = RosterService.updatePlayers(function(res){
callback(players);
});
}
}
})();
RosterService:
function RosterService($rootScope) {
var model = {
players: [],
updatePlayers: updatePlayers,
setCurrentPlayer: setCurrentPlayer,
getCurrentPlayer: getCurrentPlayer,
findPlayerByName: findPlayerByName,
findAllPlayers: findAllPlayers,
createPlayer: createPlayer,
deletePlayerById: deletePlayerById,
updatePlayer: updatePlayer,
findPlayerById: findPlayerById
};
return model;
function updatePlayers(callback){
$.ajax({
url: URL,
dataType: 'json',
type: 'GET',
}).done(function(response) {
var data = angular.fromJson(response);
for (var g = 0; g < data.goalie.length; g++) {
var player = model.findPlayerByName(data.goalie[g].name);
if (player == null) {
var newPlayer = {
_id: (new Date).getTime(),
name: data.goalie[g].name,
position: data.goalie[g].position,
height: data.goalie[g].height,
weight: data.goalie[g].weight,
birthday: data.goalie[g].birthdate,
age: 25,
birthPlace: data.goalie[g].birthplace,
number: data.goalie[g].number
};
model.players.push(newPlayer);
}
else{
callback(null)
}
}
return callback(model.players)
});
}
RosterView table code:
<tr ng-repeat="player in players">
<td>{{player.number}}</td>
<td>{{player.name}}</td>
<td>{{player.position}}</td>
<td>{{player.height}}</td>
<td>{{player.weight}}</td>
<td>{{player.birthday}}</td>
<td>{{player.age}}</td>
<td>{{player.birthPlace}}</td>
<td>
<div class="col-sm-4 btn-group">
<button ng-click="deletePlayer($index)" type="button" class="btn btn-table">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</button>
<button ng-click="selectPlayer($index)" type="button" class="btn btn-table">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
</button>
</div>
</td>
</tr>
They way you're trying to implement it seems too much of the "JQuery way", which is very different from how angular works. First of all, avoid callbacks, use promises instead. Also, if it is an option, use $http or restangular instead.
An example of the service following my suggestions would be like this (the example is only for the fetchPlayers funcionality):
angular.module('myModule', [])
.service('playersService', ['$http', function($http){
this.fetchPlayers = function(){
return $http.get(url);
}
}])
.controller('playerCtrl', ['$scope', 'playersService', function($scope, playersService){
$scope.players = []; //Start as an empty array
this.fetchPlayers = function(){
playersService.fetchPlayers.then(function(players){
//here you can process the data as you need
$scope.players = players; //assign to a property in scope so template can see it
});
};
this.fetchPlayers(); //Invoke fetch on load
}])
Here you can find a controller in a project that performs CRUD operations with $http and handles the response to show them in a table, and here is the implementation of the service to perform the calls to the backend API.
I have a form to enter a list of key value pairs. When I enter data into the input fields or add a new keyvalue pair everything displays fine on the client side and I can see the Model updating. The problem comes when I go to save the newly created list. The list get passed to the angular save function just fine with all the data in place. The problem occurs between the angular save function and my MVC controller save function. The right number of key value pairs get sent to the MVC controller functions parameters, but all of the information has now been set to null.
So on the client side my model looks like this.
[{"Key":"this","Value":"has"},{"Key":"data","Value":"in"},{"Key":"it","Value":"see"}]
which is what I want, but when it reaches my MVC controller as a parameter it looks like this.
[{"Key":null,"Value":null},{"Key":null,"Value":null},{"Key":null,"Value":null}]
Which is not what I want. Any help with this will be greatly appreciated. Thank You.
Here is my MVC controller function.
public JsonResult SaveSettings(List<KeyValuePair<string, string>> replacementPaths)
{
JobSchedulerConfig config;
config = Biz.GetConfig();
config.ReplacementPaths = replacementPaths;
return null;
}
My Angular controller and save logic.
$scope.save = function () {
SettingsService.saveSetting($scope.settings)
.success(function (data) {
//alert(data);
})
.error(function (error) {
$scope.status = 'Unable to load data: ' + error.message;
});
};
SettingsApp.factory('SettingsService', ['$http', function ($http) {
var SettingsService = {};
SettingsService.getSettings = function () {
return $http.get('#Url.Action("GetReplacementPaths", "Setting")');
};
SettingsService.saveSetting = function (data) {
//alert(data);
alert(data.toSource());
//data = angular.stringify(data);
return $http.post('#Url.Action("SaveSettings", "Setting")', data );
};
return SettingsService;
}]);
And my view markup.
<div ng-app="SettingsApp">
<div ng-controller="SettingsController">
<div ng-repeat="kvp in settings">
<input ng-model="kvp.Key" />
<input ng-model="kvp.Value" />
<button ng-click="delete(settings, kvp)">Delete</button>
</div>
<button class="btn btn-default" ng-click="addSetting()">Add Setting</button>
<button type="submit" name="config" ng-click="save()" class="btn btn-default btn-primary">Save Config</button>
{{settings}}
</div>
</div>
Again thanks for any help.
This is my HTML:
<button type="button" class="btn btn-primary" ng-hide="ctrl.userProfile.following">Follow</button>
<button type="button" class="btn btn-primary" ng-show="ctrl.userProfile.following">Unfollow</button>
So basically, I want to hide the "Follow" button and show the "Unfollow" button if ctrl.userProfile.following is true, and vise-versa.
This is my back-end:
self.userProfile = {};
function fetchUserProfile() {
$http.get('/users/' + username)
.then(function(response) {
self.userProfile = response.data;
}, function(response) {
self.cerrorMessages = BaseService.accessErrors(response.data);
});
};
fetchUserProfile();
So I get the userProfile and then update self.userProfile with the watching variable (this occurs when I do self.userProfile = response.data. With that said, is there a way for me to tell HTML to not display the buttons until self.userProfile is filled with watching variable?
Create a userProfile.ready flag and use that to control the showing of the Follow and Unfollow buttons.
HTML
<div ng-show="ctrl.userProfile.ready">
<button type="button" ng-hide="ctrl.userProfile.following">Follow</button>
<button type="button" ng-show="ctrl.userProfile.following">Unfollow</button>
</div>
JS
self.userProfile = {};
self.userProfile.ready = false;
function fetchUserProfile() {
self.userProfile.ready = false;
$http.get('/users/' + username)
.then(function(response) {
self.userProfile = response.data;
self.userProfile.ready = true;
}, function(error) {
self.cerrorMessages = BaseService.accessErrors(error.data);
});
};
fetchUserProfile();
There are a few ways of doing this. Here is one:
If you start with an empty object, and are waiting for the promise to resolve, you can set a scope variable that checks the length of the object.
e.g.
self.userProfileLength = Object.keys(self.userProfile).length;
And in the view, do: ng-if="ctrl.userProfileLength"
(Note: Or use ng-show if you want to keep the element in the DOM)
Object.keys returns the keys from the object in an array. Anything over a length of 0 is a truthy value so it will pass the ng-if condition.
I am having some difficulties understanding the properties/functions available through ui-grid. I often get confused with its previous version ng-grid. I am trying to find what the best way of deleting a checked-entry would be. Here is my markup, but due to not quite understanding if I have an index, or count available through a row entity:
HTML:
<div class="form-group">
<button type="button" id="addData" class="btn btn-success" ng-click="addData()">Add Data</button>
<button type="button" id="removeData" class="btn btn-success" ng-click="removeData()">Remove First Row</button>
<br>
<br>
<div id="grid1" ui-grid="gridOptions" ui-grid-edit ui-grid-selection external-scopes="myViewModel" class="grid"></div>
</div>
which lies under my controller:
$scope.removeData = function () {
console.log($scope.gridApi.selection.getSelectedRows());
var items = $scope.gridApi.selection.getSelectedRows();
angular.forEach($scope.myData, function (data, index) {
angular.forEach(items, function (item) {
if (item.displayValue = data.displayValue)
{
$scope.myData.splice(index, 1);
}
});
where displayValue is a property of my column and $scope.myData is my data. Is there a different way to send that selection to the controller for removal? The current way I have it does NOT work (obviously). Any help will be appreciated. If my markup is incomplete, I'll update it as necessary. Thank you!
Your solution looks a little complicated. Here is my delete function:
$scope.deleteSelected = function(){
angular.forEach($scope.gridApi.selection.getSelectedRows(), function (data, index) {
$scope.gridOptions.data.splice($scope.gridOptions.data.lastIndexOf(data), 1);
});
}
Here is a plunker based on the 210_selection tutorial.
ui-grid has problem with array.splice() method
This method is giving a problem which is making array replaced by the deleted row or item.
$scope.gridOptions.data.splice(index,1)
So the better way to handle delete of a row is by doing two things
Step 1:
$scope.gridApi.core.setRowInvisible(row)
The line above will hide the selected row
Step 2: Call the service which deletes the data at the database
I don't know how proper my solution is, but here is my code (maybe it can guide someone in the right direction or if they have a better method, please share :) )
$scope.removeData = function () {
angular.forEach($scope.gridOptions.data, function (data) {
angular.forEach($scope.gridApi.selection.getSelectedRows(), function (entity, index) {
if (entity.$$hashKey == data.$$hashKey) {
$scope.gridApi.selection.unSelectRow(entity);
//timeout needed to give time to the previous call to unselect the row,
//then delete it
$timeout(function () {
$scope.gridOptions.data.splice($scope.gridOptions.data.indexOf(entity), 1);
}, 0,1);
}
});
});
};
Hope it helps somebody!
I have a button that looks like this, which I specify in the cellTemplate value in my grid columnDefs...
columnDefs: [
// snipped other columns
{ name: '_delete', displayName: '', width: '5%', cellTemplate: '<div class="ui-grid-cell-contents ng-binding ng-scope"><button class="btn btn-danger btn-xs btn-block" ng-click="getExternalScopes().delete($event, row)"><span class="glyphicon glyphicon-trash"></span></button></div>', enableFiltering: false, disableColumnMenu: true }
My controller has a line which (IIRC) enables the getExternalScopes() call to work
$scope.$scope = $scope;
Then I handle the delete event I'm triggering like this in my controller...
$scope.delete = function (event, row) {
MyService.Delete({ action: row.entity.MyIdField }); // tells the server to delete the entity
$scope.gridApi.core.setRowInvisible(row); // hides that row in the UI
}
Perhaps this helps?
// $scope.gridApi.grid.cellNav.lastRowCol = null;
$scope.gridApi.grid.cellNav.focusedCells = [];
var cells = $(".ui-grid-cell");
// var focused = $(".ui-grid-cell-focus");
for (var i = 0; i < cells.length; i++) {
var div = $(cells[i].children[0]);
div.removeClass('ui-grid-cell-focus');
}
To answer #dileep's query extending on #Kevin Sage answer. This approach uses a service to send a delete request to the server and only delete the row once a successful response has been received. You do not want to delete the row from the grid if something went wrong and the record is still on the database.
$scope.deleteSelected = function(){
angular.forEach($scope.gridApi.selection.getSelectedRows(), function (data, index) {
YourService.delete({
id: data.id
}, function(response) {
$scope.gridOptions.data.splice($scope.gridOptions.data.lastIndexOf(data), 1);
}, function(error) {
// Run any error code here
});
});
}
I'm having trouble submitting a form with knockout js.
I receive the error "Pass a function that returns the value of the ko.computed."
The code is as follows:
(function(records,$,undefined){
records.models={
student:function(data){
var self=this;
self.id=ko.observable(data.id);
self.fname=ko.observable(data.fname);
self.lname=ko.observable(data.lname);
if(data.initial==='undefined'||data.initial===null){
self.initial=ko.observable("");
}else{
self.initial=ko.observable(data.initial);
}
self.fullname=ko.computed(function(){
return self.fname()+" "+" "+self.initial()+" "+self.lname();
});
},
students_model:function(){
var self=this;
self.selectedStudent=ko.observable();
self.students=ko.observableArray([]);
getStudents();
self.save=function(){
var form=$("#student-form");
$.ajax({
type:"POST",
url:"/Student/create",
data:ko.toJSON(form[0]), //This line here is the exact point of failue
success:function(response){
records.general.handleSuccess(response);
if(response.status){
getStudents();
}
}
});
return false;
};
function getStudents(){
$.getJSON("/Student/data",function(result){
var mapped=$.map(result,function(item){
return new records.models.student(item);});
self.students(mapped);
});
}
}
};
return records;
}(window.records=window.records||{},jQuery));
HTML
#using (Ajax.BeginForm("Create", "Student",
new AjaxOptions
{
HttpMethod = "Post"
},
new { #class = "student-form", name = "student-form", id = "student-form" }))
{
<input type="text" data-bind="value:$root.fname" id="student.fname" name="student.fname" />
<input type="text" data-bind="value:$root.lname" id="student.lname" name="student.lname"/>
<input type="text" data-bind="value:$root.initial" id="student.initial" name="student.initial"/>
<input type="text" data-bind="value:$root.dob" id="dob" name="dob" />
<button data-bind="click:save">Save</button>
}
<script type="text/javascript">
ko.applyBindings(new records.models.students_model());
</script>
What am I doing wrong here? I'm aware of this question here:Pass a function that returns the value of the ko.computed
But it seems like that individual had a different problem. My code fails when starting in the save method. Specifically the line:
data:ko.toJSON(form[0])
ko.toJSON is expecting you to pass it your viewModel, but you're passing it an element from the DOM, thus the error.
You need to pass a javascript object (a viewmodel or part of your viewmodel) to ko.toJSON. For example, if you wanted to send up the array of students, you could do this:
ko.toJSON(self.students);
I see that your form has some inputs bound to $root.fname, $root.lname, $root.initial, and $root.dob, but I'm not sure where those exist in your viewmodel, so I can't tell you exactly what to pass. But I can give you an example of one way could could solve this.
If you have a viewmodel that looks like this:
var data = ...;
var vm = {
newStudent : {
fname : ko.observable(data.fname),
lname: ko.observable(data.lname),
initial: ko.observable(data.initial ?? ""),
dob: ko.observable(data.dob)
}
}
and then you bound this to your dom by calling
ko.applyBindings(vm);
You could then call ko.toJSON like this:
...
data:ko.toJSON(vm.newStudent),
...