angularjs ng-show binding doesn't work with jQuery - javascript

I have some ng-repeat
<tr ng-repeat="Picture in Pictures">
<td>{{Picture.Id}}</td>
<td>
<div ng-hide="Picture.editorEnabled">
{{Picture.Name}}
</div>
<div ng-show="Picture.editorEnabled">
<input ng-model="Picture.editName" class="form-control" ng-show="Picture.editorEnabled">
</div>
</td>
<td>
<button type="button" name="editButton" class="btn btn-warning" ng-hide="Picture.editorEnabled"
ng-click="enableEditor(Picture)">Edit
</button>
<div ng-show="Picture.editorEnabled">
<button type="button" name="saveEditButton" class="btn btn-success"
ng-click="saveEditor(editName,editImageUrl,Picture)">Save
</button>
<button type="button" name="cancelEditButton" class="btn btn-warning" ng-click="disableEditor(Picture)">
Cancel
</button>
</div>
</td>
</tr>
and here is my controller code:
$scope.enableEditor = function(picture) {
picture.editorEnabled = true;
picture.editName = picture.Name;
picture.editImageUrl = picture.ImageUrl;
};
$scope.disableEditor = function(picture) {
picture.editorEnabled = false;
picture.editName = null;
picture.editImageUrl = null;
};
$scope.saveEditor = function(name, url, picture) {
$.ajax({
url: 'admin/pictures/edit/' + picture.Id,
type: 'POST',
data: {
ImageUrl: url,
Name: name
},
success: function() {
picture.Name = picture.editName;
picture.ImageUrl = picture.editImageUrl;
$scope.disableEditor(picture);
}
});
};
when I click 'Edit' appears editing fields and Save,Cancel buttons.
when I click 'Cancel' it dissapears.
when I click 'Save' fields saves to DB, method disableEditor executes on same object, I checked Id property, and in debugger it showes that editorEnabled is false, but in my browser still exist Save,Cancel buttons and fields for editing. I click again on 'Save' and it dissapears.

You should try using $http instead of $.ajax. jQuery's $.ajax executes outside of Angular's scope. Any code in a callback would not be reflected on your scope because this occurs outside of Angular's digest cycle. Try the following instead:
var url = 'admin/pictures/edit/' + picture.Id;
var data = $httpParamSerializerJQLike({
ImageUrl: url,
Name: name
});
var config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
$http.post(url, data, config).then(function() {
picture.Name = picture.editName;
picture.ImageUrl = picture.editImageUrl;
$scope.disableEditor(picture);
});
For this to work, you'll need to inject the $http and $httpParamSerializerJQLike services into your controller. I'm assuming you need to submit this as form, since that's how $.ajax works by default. This requires Angular 1.4+.
Note, while it is possible to use $.ajax and then wrap your success code in a $timeout or $scope.$apply(), you're better off using Angular's own $http.

This does not work because everything that you do in $scope.saveEditor happens outside of angular (I assume $ is jQuery). This is simply not the angular way.
You should have a deeper look into https://docs.angularjs.org/api/ng/service/$http the angular HTTP service, and use that instead.

You need to use $apply every time you use something that is not "angular way", like Anid has already mentioned.
For example if you use jQuery's http instead of angular's $http, you will have to add $scope.$apply.

Related

Cannot work out how to access property of self in ViewModel

I am very new to js and html - trying to make a basic front end for a C# web api.
I'm making a simple app for tracking bugs. I have a panel for the list of bugs, where I can click "Details" to see more info on each bug (I would post an image, but my reputation is too low). Then a new panel opens with with the details of the bug, including a button to close the bug, ie change set the status to "closed". It's with this button that I have the problem.
I have this in my Index.cshtml:
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title">Bugs</h2>
</div>
<div class="panel-body">
<ul class="list-unstyled" data-bind="foreach: bugs">
<li>
<strong><span data-bind="text: Title"></span></strong>: <span data-bind="text: Description"></span>
<small>Details</small>
</li>
</ul>
</div>
</div>
<div class="alert alert-danger" data-bind="visible: error"><p data-bind="text: error">
</p></div>
<!-- ko if:detail() -->
#* Bug Detail with Close Button *#
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title">Detail</h2>
</div>
<table class="table">
<tr><td>Title</td><td data-bind="text: detail().Title"></td></tr>
<tr><td>Description</td><td data-bind="text: detail().Description"></td></tr>
<tr><td>Status</td><td data-bind="text: detail().Status"></td></tr>
<tr><td>Created</td><td data-bind="text: detail().Created"></td></tr>
<tr><td>Owner</td><td data-bind="text: detail().Owner"></td></tr>
</table>
<div class="panel-body">
<form class="form-horizontal" data-bind="submit: closeBug(detail())">
<button type="submit" class="btn btn-default">Close bug</button>
</form>
</div>
</div>
</div>
<!-- /ko -->
Then this is the relevant stuff in app.js:
var ViewModel = function () {
var self = this;
self.bugs = ko.observableArray();
self.error = ko.observable();
self.detail = ko.observable();
self.getBugDetail = function (item) {
ajaxHelper(bugsUri + item.Id, 'GET').done(function (data) {
self.detail(data);
});
}
var bugsUri = '/api/bugs/';
function ajaxHelper(uri, method, data) {
self.error(''); // Clear error message
return $.ajax({
type: method,
url: uri,
dataType: 'json',
contentType: 'application/json',
data: data ? JSON.stringify(data) : null
}).fail(function (jqXHR, textStatus, errorThrown) {
self.error(errorThrown);
});
}
// get open bugs
function getAllBugs() {
ajaxHelper(bugsUri, 'GET').done(function (data) {
self.bugs(data);
});
}
// Fetch the initial data.
getAllBugs();
//close bug
self.closeBug = function (localDetail) {
var closedBug = {
OwnerId: self.localDetail.OwnerId,
Description: self.localDetail.Description,
Status: "closed",
Title: self.localDetail.Title,
Created: self.localDetail.Created
};
ajaxHelper(bugsUri + self.localDetail.Id, 'DELETE', self.localDetail.Id);
ajaxHelper(bugsUri, 'POST', closedBug).done(function (item) {
self.bugs.push(item);
});
}
};
To update the status of a bug, I want to take the Id of the bug currently open in the detail panel and create an identical bug except with Status set to "closed". The trouble is that there's always a problem access self.localDetail in the new variable closedBug. I've tried it here by parameterizing the closeBug method, but I've also tried accessing self.Detail, but it's done no good, so I'm here. My next step, if this fails, is to create a separate panel entirely with a form for bugId which closes the bug when you submit, but it would be better to be in the bug details window.
you're already passing localDetail as the param in the closeBug fn, so you don't need to refer to it by adding self. Try this (removed all references to self.):
//close bug
self.closeBug = function (localDetail) {
var closedBug = {
OwnerId: localDetail.OwnerId,
Description: localDetail.Description,
Status: "closed",
Title: localDetail.Title,
Created: localDetail.Created
};
ajaxHelper(bugsUri + localDetail.Id, 'DELETE', localDetail.Id);
ajaxHelper(bugsUri, 'POST', closedBug).done(function (item) {
self.bugs.push(item);
});
}
Your first issue is in your submit binding itself. It's being called as soon as it's rendered, not on submit. You want to pass the function object (optionally with bound arguments) instead of calling it in your html.
Explicit Bound Arguments
<form class="form-horizontal" data-bind="submit: closeBug.bind(null, detail)">
<button type="submit" class="btn btn-default">Close bug</button>
</form>
which will bind null as the this value of the function and pass the detail observable as the first argument. With this, your closeBug looks like
self.closeBug = function (localDetail) {
var closedBug = {
OwnerId: localDetail().OwnerId
}
}
Note, you want to unwrap it in the handler, not the html so you get the latest value and not the initial value.
Binding Context as this
Alternatively (and in more idiomatic knockout fashion) you can bind to the function object and it will be called with the binding context as this (the same as explicitly using closeBug.bind($data)).
<form class="form-horizontal" data-bind="submit: closeBug">
<button type="submit" class="btn btn-default">Close bug</button>
</form>
self.closeBug = function () {
var closedBug = {
OwnerId: this.localDetail().OwnerId
}
}
Aside: this may be helpful for better understanding this, self, and function.bind

Reload changed content created via ng-repeat without refreshing whole page

I'm currently trying to do the following, unfortunately without any success:
Basically, I have an array of objects, where for every object a button is created dynamically via the ng-repeat directive. When clicking a button, a new object is appended to the mentioned array (data will be sent to the server via api calls, the server will update the list internally and send the new array with the new object appended back to the view).
The same goes for deleting elements from that array.
Here's a little sample code of what I mean:
<span ng-repeat="x in allElements">
<button class="btn btn-primary" id="elementButtons">{{x.id}}</button>
</span>
There will be as many buttons as elements in $scope.allElements.
Then there's also this button, which basically causes the array to be reset to 0 elements:
<button id="clearAllElements" type="button" class="btn btn-danger"
data-toggle="button" ng-click="deleteAllElements()">Clear all</button>
The $scope.deleteAllElements() function calls the api to delete all elements from the server and fetches the new (empty) array from the server assigning it to $scope.allElements.
Now, how can I update the view without having to refresh the whole page such that only the created buttons are reloaded?
Thanks for any answers in advance,
a.j.stu
EDIT:
This is the function that is called when an element is to be added:
$scope.addElement = function(elementName) {
if ($scope.checkElementName(elementName)) {
var data = {"name": elementName.toUpperCase(),
"type": /*retrieve type of element from html element*/}
$http.post("api/addElement/", JSON.stringify(data))
.then(function(response) {
$scope.allElements = response.data; //the api call adds the element in the backend and returns an array with all elements appended the new one. This SHOULD refresh the view of all element buttons, but doesn't.
})
.catch(function(response) {
console.log("something went wrong adding element " + elementName);
})
.then(function(response) {
$('#newElementModal').modal('hide'); //#newElementModal is the modal that pops up when clicking a button to add a new element. here the name and type of the element are set and used in the api call above to add the element. Basically, when the modal hides, the view of all buttons should be refreshed.
});
} else {
console.log("invalid identifier of element.");
}
As far as I've understood, the .then() calls are asynchronous. But, if there are only .then() calls following the api call, this should not be a problem, right?
You should not have to worry about resfreshing the page. If your view is connected to the controller whose $scope is updated by the API calls adding and deleting elements, your view will adapt and display the new content.
For what it's worth, here's a snippet showing how it could work. Minus the API calls that add / delete data.
angular.module('dummyApp', [])
.controller('DummyController', function($scope) {
$scope.allElements = [
{ id : 1, name : "Joe"},
{ id : 2, name : "John"},
{ id : 3, name : "Jane"},
{ id : 4, name : "Alice"},
];
$scope.deleteAllElements = function () {
// deleting elements empties the $scope.allElements array
$scope.allElements.length = 0;
};
$scope.addElement = function () {
var element = {
id : generateId(),
name : 'whatever'
}
// new elements are pushed into the $scope.allElements array
$scope.allElements.push(element);
};
function generateId() {
return Math.floor(Math.random()*1000);
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.2/angular.js"></script>
<div ng-app="dummyApp" ng-controller="DummyController">
<span ng-repeat="x in allElements">
<button class="btn btn-primary" id="elementButtons">{{x.id}}</button>
</span>
<br/><br/>
<button id="clearAllElements" type="button" class="btn btn-danger"
data-toggle="button" ng-click="deleteAllElements()">Clear all</button>
<button id="clearAllElements" type="button" class="btn btn-danger"
data-toggle="button" ng-click="addElement()">Add</button>
</div>
use trackby x.id to update the view without refreshing the whole page
Just assign new response data from your server to $scope.allElements and it will be refreshed without reloading page.

Table not getting displayed on a button click in AngularJS

Hi I'm learning AngularJS and I have a question. I want to display a table on a button click. On clicking the button, JSON data gets fetched but I have to press the button twice for the data to be displayed.
This is the HTML page.
<html>
<body>
<div class="container">
<label for="tags" style="margin-top: 30px;margin-left: 15px;">GSTIN </label>
<input id="tags">
<button ng-click="searchfunction()">search</button>
</div>
<br/>
<hr>
<div class="container">
<div ng-show="tshow" ng-repeat="x in searchdata">
<table class="table table-bordered table-responsive">
<thead>
<tr>
<th>MON</th>
<th>SGST</th>
<th>CGST</th>
<th>IGST</th>
<th>CESS</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="i in x">
<td>{{i.mon}}</td>
<td>{{i.sgst}}</td>
<td>{{i.cgst}}</td>
<td>{{i.igst}}</td>
<td>{{i.cess}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
This is the controller:
app.controller("searchcontroller", function ($scope,$http) {
$scope.tshow=false;
function make_base_auth(user, password) {
var tok = user + ':' + password;
var hash = btoa(tok);
return "Basic " + hash;
}
$scope.searchfunction=function() {
$scope.tshow=true;
var tf=document.getElementById("tags");
var value=tf.value;
var auth = make_base_auth("gstadmn112","Gstn#123");
var url6 = "http://164.100.148.67:8080/wsgstr3B/rest/payment/gstinsearch?gstin="+value+"&year=201718";
xml = new XMLHttpRequest();
// jQuery
$.ajax({
url : url6,
method : 'GET',
beforeSend : function(req) {
req.setRequestHeader('Authorization', auth);
},
success:function(response) {
console.log(response);
scope.searchdata=response;
},
failure:function() {
window.alert("wrong input data doesn't exist");
}
});
}
});
I need to click twice on the search button for the table to be displayed. I want the table to be hidden initially and once the search is successful the table should be displayed. The table is hidden initially and after clicking twice correct data gets displayed.
Maybe, you try to add $scope.tshow=true; in function success:
success:function(response) {
console.log(response);
$scope.tshow=true;
$scope.searchdata=response;
},
P.S. I advise to use $http instead of $ajax.
This problem is related to the digest loop of angularjs which keeps all changes sync between your view and controller.
When you invoke the searchfunction(), angularjs will know whats happening inside the method and sync the changes made with the view when its completed.
The problem is that your method uses $.ajax which has some async callback methods.
When these methods gets invoked angularjs have already left the party (digest loops is over) and don't know what these methods have done to your controller $scope.
The jQuery success callback will however set the $scope.searchdata=response; and this change gets notified the next time angularjs is in the party (the next time you click).
So basically you need to make sure angularjs is aware of the async methods which makes changes to your $scope.
To solve this I would inject angularjs own $http service (which takes care of async changes to the scope) and use that instead.
var req = {
method: 'GET',
url: url6,
headers: {
'Authorization': auth
}
}
$http(req).then(function(response){
console.log(response);
$scope.searchdata=response;
}, function(){
window.alert("wrong input data doesn't exist");
});
You can use this way.
$scope.searchfunction=function(){
$scope.tshow=true;
var tf=document.getElementById("tags");
var value=tf.value;
$http.get("http://164.100.148.67:8080/wsgstr3B/rest/payment/gstinsearch?gstin="+value+"&year=201718")
.success(function(result) {
$scope.searchdata=response;
$scope.tshow=false;
})
.error(function() {
window.alert("wrong input data doesn't exist");
});
}

Child view model modying a different child viewmodel

I have a main View Model for my screen. It consists of 2 child view models.
One handles the registration section.
One handles the login section.
One handles the menu section (If authenticated and what menu items can appear, as well as the "Welcome "Username" type stuff).
$(document).ready(function () {
// Create the main View Model
var vm = {
loginVm: new LoginViewModel(),
registerVm: new RegisterViewModel(),
layoutVm: new LayoutViewModel()
}
// Get the Reference data
var uri = '/api/Reference/GetTimezones';
$.getJSON({ url: uri, contentType: "application/json" })
.done(function (data) {
vm.registerVm.Timezones(data);
});
// Bind.
ko.applyBindings(vm);
});
Once my Login model's "Login" method completes, I want to set the "IsAthenticated" value within the Menu model, as well as some other user info.
So in my login model, I have a SignIn method.
$.post({ url: uri, contentType: "application/json" }, logindata)
.done(function (data) {
toastr[data.StatusText](data.DisplayMessage, data.Heading);
if (data.StatusText == 'success') {
alert($parent.layoutVm.IsAuthenticated());
}
else {
}
})
.fail(function () {
toastr['error']("An unexpected error has occured and has been logged. Sorry about tbis! We'll resolve it as soon as possible.", "Error");
});
The alert code is my testing. I am hoping to access (and set) the IsAuthenticated property of the layoutVm model. That's one of the child models on my main View model.
However, "$parent" is not defined.
How can I update values in the layoutVm, from my loginVm?
$parent is part of the binding context, which is only available during the evaluation of the data-bind (i.e. to the binding handler).
In your viewmodel structure, you'll have to come up with a way to communicate between models yourself. For example, by passing parent view models, or by passing along shared observables. The problem you're describing can be solved by using data-bind="visible: $root.userVM.IsAuthenticated", like I answered in your previous question.
If you'd like to go with the other approach, here's an example on how to share an observable between viewmodels.
var ChildViewModel = function(sharedObs) {
this.myObs = sharedObs;
this.setObs = function() {
this.myObs(!this.myObs());
}.bind(this);
}
var RootViewModel = function() {
this.myObs = ko.observable(false);
this.vm1 = new ChildViewModel(this.myObs);
this.vm2 = new ChildViewModel(this.myObs);
this.vm3 = new ChildViewModel(this.myObs);
}
ko.applyBindings(new RootViewModel());
div { width: 25%; display: inline-block; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="with: vm1">
<h4>vm1</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
<div data-bind="with: vm2">
<h4>vm2</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
<div data-bind="with: vm3">
<h4>vm3</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
Note that each of the child view models also have write permission, so you'll have to be careful to not accidentally update the observable

AJAX not working with Knockout.JS

For some reason, I cannot get an AJAX function
$('#queueSubmit').on('click', function() {
$.ajax({
type: 'POST',
url: nodeApiUrl + 'twitter/update_status/',
contentType: 'application/json',
data: JSON.stringify({'status': $('#queuedTweet').val() }),
dataType: 'json',
error: $.osf.handleJSONError
});
});
to be called when I click the button with id queueSubmit. This is the knockout code that generates the buttons and content.
<div id = "foo">
<!-- ko foreach: tweets -->
<input id = "queuedTweet" data-bind="value: tweet"/>
<a class="btn btn-primary" id = "queueSubmit" >
Send
</a>
<a id = "removeFromQueue" data-bind = "click: $parent.removeTweet, value:index" class="btn btn-danger">
Delete
</a>
</br>
<!-- /ko -->
</div>
It does not register that the button is being clicked. I believe it has something to do with the bindings but not sure how to remedy. Any help would be appreciated!
You want do do this instead of your current implementation:
<a class="btn btn-primary" data-bind="click: $parent.queueSubmit">Send</a>
Then in your viewModel do this:
self.queueSubmit = function() {
$.ajax({
type: 'POST',
url: nodeApiUrl + 'twitter/update_status/',
contentType: 'application/json',
data: JSON.stringify({'status': $('#queuedTweet').val() }),
dataType: 'json',
error: $.osf.handleJSONError
});
};
Use knockout bindings any time you can. If a native binding doesn't exist, create a custom binding.
IDs are supposed to be unique, but you are creating them within a ko foreach loop. That means you actually have an array of "queueSubmit" IDs. jQuery assumes you will only have one, so it always retrieves the first one in the list.
You can see this in action by using the native JavaScript functions:
document.querySelector("#queueSubmit"); // <== returns the first one in the list
document.querySelectorAll("#queueSubmit"); // <== return the array of elements with that id
document.querySelectorAll("#queueSubmit")[n]; // <== returns the element at index 'n'
Duplicating IDs is a really bad idea (I've always been surprised that this is even allowed, but it is).
That leads to the question "why are you using an ID"? If you are going to replicate it like this, then either make the IDs unique or replace them with a class. (The class option is much better).
Even better, like #Cameron suggested, use a click binding to a function inside your KO ViewModel instead. This will also have the added benefit of automatically passing the bound data item (the current instance of the foreach loop) to the function.
<div id = "foo">
<!-- ko foreach: tweets -->
<input id = "queuedTweet" data-bind="value: tweet"/>
<a class="btn btn-primary" click="$parent.queueSubmit" >
Send
</a>
<a id="removeFromQueue" data-bind="click: $parent.removeTweet, value:index" class="btn btn-danger">
Delete
</a>
</br>
<!-- /ko -->
And then in your ViewModel:
self.queueSubmit = function(item) { // <== current tweet is passed in automatically
$.ajax({
type: 'POST',
url: nodeApiUrl + 'twitter/update_status/',
contentType: 'application/json',
data: JSON.stringify({'status': item.tweet }), // <== use the item, not jQuery
dataType: 'json',
error: $.osf.handleJSONError
});
};
You can learn more about the click binding at the KO website
You can't use IDs in this way. You should register a click binding.

Categories