I am creating a simple CRUD using AngularJS and facing a minor issue. While editing a form I am getting json string returned by server in model and I have created my custom directive to format that date. It formats the date but scope is not applied and when I submit the form I get the old json date in alert.
MY Js goes like this:
var myApp = angular.module('myApp', ['toaster']).controller('MyController',function($scope){
$scope.old = { ship_date : '/Date(1359743400000)/' };
$scope.UpdateShipment = function () {
alert($scope.old.ship_date);
}
}).directive('formatDate', function () {
return {
require: 'ngModel',
scope : {'ngModel' : '='},
link: function (scope, element, attr, ngModelController) {
ngModelController.$formatters.unshift(function (valueFromModel) {
if (angular.isUndefined(valueFromModel)) {
return valueFromModel;
}
var date = new Date(parseInt(valueFromModel.substr(6)));
console.log(valueFromModel);
return date.toLocaleDateString();
});
}
};
});
View:
<div ng-app="myApp">
<div ng-controller="MyController">
<input type="text" ng-model="old.ship_date" /> <form ng-submit="UpdateShipment()">
<input type="text" format-Date ng-model="old.ship_date" />
<input type="submit" id="submit" value="Save" />
</form>
</div>
</div>
PLEASE HELP , MANY THANKS.
I had similiar issues, but i handled it in $http interceptor. You can transform the date strings into javascript objects. No need for custom directive. I think it is a lot cleaner approach.
Here is example implementation:
http://aboutcode.net/2013/07/27/json-date-parsing-angularjs.html
app.config(["$httpProvider", function ($httpProvider) {
$httpProvider.defaults.transformResponse.push(function(responseData){
convertDateStringsToDates(responseData);
return responseData;
});
}]);
All previous Answers about you needing to call $scope.apply() are correct, but as you have seen it can throw an error when another digest/apply is already in progress so to call it safely use this
if(!$scope.$$phase){$scope.$apply();}
this will only call it if it is not in progress and hopefully might fix your issue.
Use $scope.$apply() when your operating a function outside the angular way.
Try after the ngModelController({...}) function add $scope.$apply();
You need to use $scope.$apply. Refer this post about modify scope inside directive.
just remove the scope-defintion from your directive
enter code here
http://jsfiddle.net/2vxCA/
To build off of the answer from 'stride', you can also implement this using an interceptor. There's a few critical change you need to make when implementing it as an interceptor.
First, your .config section would look like this:
.config([
$httpProvider", function($httpProvider) {
$httpProvider.interceptors.push("httpInterceptorTransformResponseService");
}
])
With that in place, you would build a factory service to intercept and transform the data similar to the example from http://aboutcode.net/2013/07/27/json-date-parsing-angularjs.html. The regex is somewhat arbitrary and depending on the data you're actually retrieving via your requests, you may want/need to change it.
(function () {
"use strict";
angular.module("yourAppNameHere")
.factory("httpInterceptorTransformResponseService",
[
function () {
// Purpose: This interceptor is intended to convert Json strings that match the ISO8601 date format into DateTime objects.
// This is necessary in many cases because there is not an object type embedded into serialized Json that represents a DateTime object.
// There are numerous variations of this regex. Choose the one that works best for you, based on what data you expect
var regexIso8601 = /^(\d{4}|\+\d{6})(?:-(\d{2})(?:-(\d{2})(?:T(\d{2}):(\d{2}):(\d{2})\.(\d{1,})(Z|([\-+])(\d{2}):(\d{2}))?)?)?)?$/;
function convertDateStringsToDates(input) {
// Ignore things that aren't objects.
if (typeof input !== "object")
return;
for (var key in input) {
if (!input.hasOwnProperty(key))
continue;
var value = input[key];
var match;
// Check for string properties which look like dates.
if (typeof value === "string" && (match = value.match(regexIso8601))) {
var milliseconds = Date.parse(match[0])
if (!isNaN(milliseconds)) {
input[key] = new Date(milliseconds);
}
} else if (typeof value === "object") {
// Recurse into object
convertDateStringsToDates(value);
}
}
}
// declare the service
var transformResponseService = {
response: function (response) {
// convert all parsable date strings returned from the data into Date objects
convertDateStringsToDates(response.data);
return response;
}
};
return transformResponseService;
}
]);
})();
CRITICAL PART: Notice near the end where the service is declared and the response for the transform is declared. The version above for the interceptor is called as follows:
convertDateStringsToDates(response.data);
and NOT
convertDateStringsToDates(response);
If you send response instead of response.data to the function when using an interceptor, what happens is that it will parse EVERYTHING that comes back from the entire http request. This means that in addition to the data it retrieves, it will also apply this to things like the http configuration, headers, etc.
I ran into an issue where it was running the regex against the config section of the http request. Because it's a recursive service and objects are treated like references, it entered an infinite loop and would overflow the stack. It's highly likely that you will only want to apply this to the data that's returned, not the configuration, headers, etc.
Related
I want to build a directive that checks for duplicate username. So I have created an index.html file and an uniqueId directive. Now in the directive I am not able to do ngModel.setValidity(). It is getting undefined.
Also I am fetching data from a local json file username.json.
When I console log console.log(ngModel.$setValidity('unique', unique)) I get undefined.
I have created a plunk for the code::
https://embed.plnkr.co/XTPf9PjiMn9if5Y0DaHt/
You need to iterate through the users present in JSON. And, if your currentValue matches any of those, you need to set it as invalid using $setValidity. Like this:
dataService.getUsers().then(function(currentusers) {
console.log(currentusers)
//Ensure value that being checked hasn't changed
//since the Ajax call was made
if (currentValue == element.val()) {
currentusers.forEach(function(user) {
if (currentValue === user.property) {
ngModel.$setValidity('unique', false)
}
});
}
}, function() {
//Probably want a more robust way to handle an error
//For this demo we'll set unique to true though
ngModel.$setValidity('unique', true);
});
});
Also, your service was getting the JSON every time. Alternatively, you can store your JSON response in a variable inside your angular service (which is a singleton) so it's faster than before. Like this,
dataFactory.getUsers = function() {
var deferred = $q.defer()
if (angular.isDefined(savedResults)) {
deferred.resolve(savedResults.data);
return deferred.promise;
} else {
return $http.get(serviceBase).then(
function(results) {
savedResults = results
return results.data;
});
}
};
Here, we return a resolved promise if data is already available. It will get the JSON for the first time and will use from within.
working plunker
please check plnkr link
If you type nitesh#gmail.com, it will display email already in use.
If you type nitesh#gmail.com1, it won't show error message.
I have changed condition.
I need to transform objects coming from $http call to an api. My code adds some fields (functions) to the object coming from the api, here the constructor of this object :
(function () {
window.TransformedObject = function (obj) {
var self = this;
self = {};
if (obj) {
self = angular.copy(obj);
}
self.hasChanged = function () {
// return true or false if the object has changed
}
return self;
}
}());
The $http transform code looks like this :
$http({
url: 'api/...',
method: 'GET',
transformResponse: function(value) {
return new TransformedObject(JSON.parse(value));
})
}).success(function(data){
vm.obj = angular.copy(data);
});
Note that the value in the transformResponse callback is stringified, and need to be parsed to get the object
All this is working fine, suppose the object coming from the api contains a key called title, doing obj.title = 'some title' will update the object.
The problem :
Binding the title field with an input tag will not update the object if the change is coming from the view.
I use a regular ng-model to do it:
<input type="text" placeholder="Title" ng-model="vm.obj.title"/>
even using $rootScope.$watch will never be triggered if the change is coming from the view aka the input tag.
$rootScope.$watch(function () {
return vm.obj;
}, function () {
console.log('watch');
// this log will never appear in the console
});
Am I doing something wrong, why transforming the object coming from the api is breaking angulars binding ???
Thanks.
http://www.bennadel.com/blog/2605-scope-evalasync-vs-timeout-in-angularjs.htm
Sometimes, in an AngularJS application, you have to explicitly tell
AngularJS when to initiate it's $digest() lifecycle (for dirty-data
checking). This requirement is typically contained within a Directive;
but, it may also be in an asynchronous Service. Most of the time, this
can be easily accomplished with the $scope.$apply() method. However,
some of the time, you have to defer the $apply() invocation because it
may or may not conflict with an already-running $digest phase. In
those cases, you can use the $timeout() service; but, I'm starting to
think that the $scope.$evalAsync() method is a better option.
...
Up until now, my approach to deferred-$digest-invocation was to
replace the $scope.$apply() call with the $timeout() service (which
implicitly calls $apply() after a delay). But, yesterday, I discovered
the $scope.$evalAsync() method. Both of these accomplish the same
thing - they defer expression-evaluation until a later point in time.
But, the $scope.$evalAsync() is likely to execute in the same tick of
the JavaScript event loop.
I have a checkbox, like:
<input type="checkbox" ng-model="isPreCheckIn" />
I'm getting isPreCheckin (boolean) from a service which uses $q and either returns from the server or localStorage (if it exists).
The call in the controller looks like:
deviceSettings.canCheckIn().then(function (canCheckIn) {
$scope.isPreCheckin = !canCheckIn ? true : false;
});
And deviceSettings.canCheckIn looks like:
function canCheckIn() {
var dfrd = $q.defer();
LoadSettings().then(function (success) {
return dfrd.resolve(localStorage.canCheckIn);
});
return dfrd.promise;
};
So, on first page load, the checkbox doesn't bind correctly to isPreCheckIn; in fact, if I do a {{isPreCheckIn}}, it doesn't either. If I switch off of that page and go back, it works.
It appears that canCheckIn is outside of angular, based on that assumption, you need to wrap your assignment within $scope.apply:
deviceSettings.canCheckIn().then(function (canCheckIn) {
$scope.$apply(function(){
$scope.isPreCheckin = !canCheckIn ? true : false;
});
});
This tells angular to recognize the changes on your $scope and apply to your UI.
I think you should wrap the following in a $apply:
function canCheckIn() {
var dfrd = $q.defer();
LoadSettings().then(function (success) {
scope.$apply(function() {
dfrd.resolve(localStorage.canCheckIn);
}
});
return dfrd.promise;
};
It sounds like a timing issue. You may need to put a resolve clause in your route to give this call time to run and then pass in the result as a DI value. Without knowing which router you are using it is impossible to give you an accurate answer, but you might look at the video on egghead.io regarding routes and resolve.
HTML:
<div class="span10" ng-controller="GroupFieldsCntl" ng-init="init()">
<div ng-repeat="field in fields"></div>
</div>
GroupFieldCntl:
function GroupFieldsCntl($scope) {
$scope.fields = [];
$scope.init = function() {
// Get fields.
$.get(ctx + 'admin/fields/fieldsJSON', function(data) {
for(var i in data) {
$scope.fields.push(data[i]);
}
});
}
}
I'm sure the ajax call get correct response, but the html page doesn't display those data.
Like the commentors here say:
1. Use $http.get instead of $.get. This is Angular's Ajax and you should be using it. [it needs to be injectecd into the controller]
2. If you loop in var i in data you might loop through methods of non-data properties, so as was suggested, use
for (var i in data) {
if data.hasOwnProperty(i)
//do something
}
}
And if you don't think there will be issues with bad data, you can always use the following syntax to have the resolved promise (get request) resolve itself to the $scope variable:
$scope.fields = $http.get(tx + 'admin/fields/fieldsJSON');
When the data arrives fields will automatically contain the JSON response after it was resolved. This is a shortcut which doesn't handle error responses though.
The changes done to data will trigger view changes only if you do it with angular functions or use $apply on other functions. So as the comments suggest you can (should) either use angular's $http service, or call your function inside $apply.
I'm just starting to play with angularJS, so maybe I'm asking something easy to do, but I can't find the way to do it.
The situation is the following: I have a list that's populated by an ng-repeat taking the values from a scoped controller variable. This variable is loaded on page load by an jsonp call, and this works fine.
The problem comes when I need to reload this list based on another select. For example, if a select 'day' value in the select I need to show some values and when I select 'week' I need to show others (also loaded via ajax).
What I've tried is to have a service that loads the data and returns it, and in the controller have two methods, one for the first load and another for the second one that does $scope.apply with the variable. I then call this second method on select value change (I've done it with jquery to simplify it until I can fix this).
This is part of my HTML
<div x-ng-controller='LeaderboardCtrl'>
<select id='leaderboard-select'>
<option value='day'>day</option>
<option value='week'>week</option>
<option value='month'>month</option>
</select>
<div x-ng-repeat='p in leaderboard'>
<p>{{p}}</p>
</div>
</div>
And this is part of the code that affects this functionality
var lead = angular.module("lead",[]);
function LeaderboardCtrl($scope,$attrs,$http,jtlanService) {
$scope.leaderboard = [];
$scope.period = 'day';
var data = {
period:$scope.period
};
$scope.loadLeaderboard = function(){
myService.loadLeaderboard(data).then(function(leaderboard) {
$scope.leaderboard = [];
$scope.leaderboard.push.apply($scope.leaderboard,leaderboard);
});
}
$scope.reloadLeaderboard = function() {
myService.loadLeaderboard(data).then(function(leaderboard) {
$scope.$apply(function() {
$scope.leaderboard = [];
$scope.leaderboard.push.apply($scope.leaderboard,leaderboard);
});
});
}
$scope.loadLeaderboard()
}
lead.service("myService",["$http", function($http) {
var myService = {
loadLeaderboard : function(data) {
var promise = $http.jsonp("/widget/leaderboardJSONP?callback=JSON_CALLBACK&_="+new Date(),{
params:data,
cache:false,
ajaxOptions: { cache: false }
}).then(function(response) {
return response.data;
});
return promise;
}
};
return myService;
}]);
$("#leaderboard-select").change(function(){
scope.period = $("#leaderboard-select").val();
scope.reloadLeaderboard();
});
Here's a fiddle with the code: http://jsfiddle.net/WFGqN/3/
Your fiddle is riddled with issues:
There's no ng-app in your mark-up
You need to change the second Framework Extensions dropdown to one of the "No wrap" options
Your service needs to be defined above your controller
Your controller is referencing "jtlanService" but you've defined "myService"
Your $http.jsonp call isn't going to work as is, but you could use can use the echo service (see Ajax Requests on the left side) to emulate requests
You can't and shouldn't be using jQuery events to call Angular controllers. You should use ng-change and not $().change (and even if you were using jQuery for event binding, you should be using $().on('change')).
You didn't need to use $scope.$apply in your loadLeaderboard function, since when you're calling it, you were already inside of of an $apply call.
There's no need for 2 load+reload leaderboard methods.
And after all that, you don't actually need jQuery.
Here's a fiddle that fixes things up and I think gets you what you want: http://jsfiddle.net/WFGqN/5/. You'll of course need to fix the service on your end, but you get the idea.
I recommend reading this SO answer: "Thinking in AngularJS" if I have a jQuery background?