Attach ngChange to all inputs? - javascript

Maybe I'm doing this wrong so please tell me...
I have several views that use one Controller, we'll call it the Page controller.
Inside the views, I have a pile of input/selects/textarea elements. When they change, I want to execute a function inside the controller scope. Now, I know I can apply ng-change attribute to every single input but we're talking... many inputs.
In my pre-angular days, I would just do something like
$("#pageOne input").on("change", function(){
parentScopeFunction();
});
I've been trying to get the
$scope.$watch()
to work but I can't seem to get it to work... Here's some code examples of what I'm trying to achieve.
I feel like I'm missing something about how $watch works. In my real application, that Device service is a connection with a web socket. But I've simplified it for this question.
https://jsfiddle.net/t1pcgkux/5/

Your issue is due to the shallow watching done on the object. It will not track for any changes in the properties of the object unless you set it for deep watch using the third argument in the watch function. Watch iterator function will run only if both the object (references) are different, which does not happen in your case since it is just property change.
$scope.$watch(function () {
return $scope.Device.stageDevice;
}, function (n, o) {
console.log("Device changed", n, o); // This never seems to happen.
}, true); //<-- here
But you really do not need to add a watch instead you could use ng-change and bind to a function on the scope as well.
Example:-
$scope.deviceChange = function(){
console.log("Device changed", $scope.Device.stageDevice);
}
and
<input class="form-control" ng-model="Device.stageDevice.name"
ng-change="deviceChange()"/>
<textarea class="form-control" ng-model="Device.stageDevice.desc"
ng-change="deviceChange()"></textarea>
Both examples Demo

Related

How to ignore Angular $watch event when I myself have caused it?

I have a custom UI element with link to ngModel:
scope:
{
ngModel : "="
}
There are two ways how the attached model might change:
it is changed from outside - in this case I want to update UI of my custom element
it is changed from inside - in this case I want to ignore the changes because my UI is already up-to-date
I have a watch:
$scope.$watch("ngModel", function(newValue){
// here I have complex logic to traverse newValue
// to see if it matches my current UI state
// if it matches, then I return
// if it does not match, then I sync my UI to the newValue
});
and I have a function which pushes current UI state to the model:
function pushSelectionToModel() {
// some code
$scope.ngModel = newState;
// some code
}
Everything works fine, but in cases when user is scrolling through my UI directive fast, ngModel watch is being triggered each time. My code to detect if newValue matches my current UI state is fairly complex, thus causing performance issues.
A natural solution seems to be somehow to ignore the $watch in case when I have just called pushSelectionToModel. I cannot just disable the watch in pushSelectionToModel before update and enable after that because the watch is executed later, after pushSelectionToModel has exited.
How do I tell Angular that for some particular model assignment operation I don't want to trigger some particular $watch?
Here is the relevant simplified Plunkr example
Essentially I want to prevent updateUi() from being called twice when I click Apply button. But the example is simplified, in reality I can't directly assign or compare innerName and ngModel values because in my actual code the values differ because of some transformations.
I've sovled a similar problem, by adding the following statement:
$scope.$watch('ngModel', function(newValue){
if($scope.innerName !== newValue){
// now execute code
http://plnkr.co/edit/r9sQax4VNqBraimQi9pz
but its more of an workaround...

Is there a way to "watch" an IndexedDB record so I can respond when it's been changed?

I am building an Angular + TypeScript application that uses IndexedDB for storing data locally.
I have an Angular directive that sets the value of a scope variable to be some data that was returned from an IndexedDB request. It's pretty simple and does something like:
Directive A:
// getGroup() is making the requests to IndexedDB:
this.dataService.getGroup().then((groupOne) => {
this.scope.items = groupOne.items;
});
The view for my directive loops through each of these items:
<div ng-repeat="item in items">{{ item }}</div>
I have another Angular directive (let's call it Directive B) that is updating/inserting the items associated with groupOne.
Directive B:
groupOne.items.push("a new item");
this.dataService.updateGroup(groupOne).then(() => {
// groupOne has been changed!
// how can I let Directive A know about this, so Directive A can update its scope.items?
});
Of course, Directive A does not know about the changes Directive B made to groupOne unless it does another request. And therefore, my view is "static".
I know I could wrap Directive A's IndexedDB request into an interval and be checking for updates, but that seems like a strange way to solve this problem.
Is there a way with IndexedDB to be notified of this change? Is there something Angular provides that could help with this (something similar to $scope.$watch()) ?
It's not quite what I wanted (which is why I accepted dgrogan's answer), but in case anyone who stumbles upon this question is curious about what I ended up doing:
Manually broadcasting a custom event whenever I change groupOne (or anything else I care about) solves my problem for now.
I used $scope.$broadcast() from a controller (that's managing
a few interactions between directives) to let Directive A know about
the change Directive B made using $scope.$on().
This article was really helpful: http://toddmotto.com/all-about-angulars-emit-broadcast-on-publish-subscribing/
There is not yet a way to do this with pure IndexedDB but it is being prototyped. The name is "Observers". You can try to use the polyfill if you are so inclined.

How to take value from input and take it to AJAX request link?

So I have done this AJAX request with angularJS:
http://jsfiddle.net/c0Lkja0h/1/
When I use link like for example $http.get('http://api.wunderground.com/api/KEY/forecast/geolookup/conditions/q/San_Francisco.json') (without '+ city +' in the link) it works great. But when I add that variable, add to ng-model="city" and hit submit, then it says
city is not defined
How can I make it take city name from that input and use it in my newAJAXreq function when I click "Find Weather" button ? Also it would be cool that default link would be with static link (when person first visited the site), and after that city is taken from input if user wants. THANKS!
There are a couple of things wrong with your approach.
Don't name your controller. It should be an anonymous function that take as parameters the modules you want to inject, such as .controller(function($http){ //do something }); or .controller(['$http',function($http){ //do something }]) to avoid losing reference to your variables when you minify your js files.
Inside your controller, define the function you want that takes as parameter the city, such as $scope.sendAjax = function(city){ //do something }. In your view you are gonna call this function with only the argument you need ng-submit="sendAjax(city)"
Avoid using scope. From the great book AngularJS:Up and Running, by Shyam Seshadri and Brad Green:
If you used AngularJS prior to 1.2, you might have expected the $scope variable to be injected into the controller, and the variables helloMsg and goodbyeMsg to be set on it. In AngularJS 1.2 and later, there is a new syntax, the controllerAs syntax, which allows us to define the variables on the controller instance using the this keyword, and refer to them through the controller from the HTML.
The advantage of this over the earlier syntax is that it makes it explicit in the HTML which variable or function is provided by which controller and which instance of the controller. So with a complicated, nested UI, you don’t need to play a game of “Where’s Waldo?” to find your variables in your codebase. It becomes immediately obvious be‐ cause the controller instance is present in the HTML.
The alternative is in your view use the controller as syntax: "ng-controller="WeatherController as weather", and then always refer to the alias when you need to access Weather Controller scope, such as ng-submit="weather.sendAjax()".
In your controller, you will assign the function to this, as in this.sendAjax = function(){ \\do something }. A good practice is to assign this to another variable, since this can get overriden. So in the first line of your controller you can do var self=this and refer to the controller as self from this point on, as in self.sendAjax = function(){ \\do something }

Angular, setting up a callback function for updating between factory and controller

I'm not sure if i have completely wrapped my head around this idea - but I'll try my best to clearly describe what I am trying to do here.
I have a factory that changes and parses a URL for me, so I can pass params into a controller for use (that were stored in the url). This is sort of so I can save a state for the user and they can share it via copy'ing of a URL (send it to their friends or bookmark it or w/e).
I am trying to set up a factory (or service) that listens for locationChangeSuccess - so that if the user mofies the url and presses enter, it will refresh the scopes in the controllers. So here is what I have:
.factory("urlFactory", function($location, freshUrl, StateString){
//request to change new url
requestObj.requestState = function(moduleName, stateName, startVar){
}
//request item from url, via your module and state name
requestObj.parseState = function(moduleName, stateName){
}
I dropped the center out (if it is needed im happy to link), but those just get and set the url for me.
So in the controllers I do something like
$scope.mod2m3 = urlFactory.parseState("module2", "mod3");
$scope.mod2m4 = urlFactory.parseState("module2", "mod4");
So when they land on the page, they pull their state. This works great. However, now i'm trying to solve some edge case scenarios where maybe the user modifies the url.
So I can latch onto that even pretty easily with
.factory("urlWatcher", function($location, $scope){
var urlWatcher = {};
$scope.$on('$locationChangeSuccess', function(event) {
console.log("Asdsa");
});
return urlWatcher
});
However, where I am struggling is trying to determine a way where when this fires, it would connect the new value to the scope in the controller. It was suggested to me that a callback of some sort in the parse (set) function, but I am struggling with how to approach that. It would be super cool if I could set a way for this factory/service to re send the new value when it changes to the right place. Callback sounds good, however I don't know how to config this correct.
The easiest route would be to just do an
$scope.$on('$locationChangeSuccess', function(event) {
console.log("Asdsa");
});
In each controller and manually bind to each scope, but I am trying to make this as modular as possible (and thats also a ton of watchers on the locationchangesuccess). would be fantastic if I could figuire out a clean way to set the service/factory to listen once, and on change find the right module/controller and change the value.
I can't seem to think a clear route, so I would be very greatful for any insight to this issue. Thank you very much for reading!
If what you want is a publish/subscribe architecture, where publications are global and subscriptions have the same lifecycles as Angular scopes... then Angular events are what you're looking for. There's no point setting up an ad hoc communication system with callbacks and whatnut, that would just be partially reinventing events.
However, if you want to make the semantics more obvious / add flexibility, you can listen once to $locationChangeSuccess in a service and broadcast a custom event.
$rootScope.$on("$locationChangeSuccess", function (event) {
$rootScope.$broadcast('myCustomeEvent', {message: "Guys, time to refresh!"});
});
Then listen to this event in each of the scopes where it is relevant.
$scope.$on('myCustomeEvent', function (event) {
console.log("Asdsa");
});
If setting up the listening gets repetitive, by all means, factor it out in a function, which you can for example put in a service:
myApp.factory('someFactory', [function () {
return {
listenToLogAsdsa: function (scope) {
scope.$on('myCustomeEvent', function (event) {
console.log("Asdsa");
});
}
};
}]);
Then all you have to write in your controller is:
someFactory.listenToLogAsdsa($scope);
You can assign a variable in the scope to an object in the factory, that way it's bound to a reference instead of a value. Then, in your HTML you bind the reference to the DOM. urlFactory.parseState() should then save the result to said object, and return the key where it was saved.
For example:
In urlFactory:
requestObj.parseState = function(moduleName, stateName){
var key = moduleName+stateName;
this.urlContainer[key] = "www.example.com";
return key;
}
In the controller:
$scope.urls = urlFactory.urlContainer;
$scope.mod2m3 = urlFactory.parseState("module2", "mod3");
In your HTML:
{{urls[mod2m3]}}
This way, "urls" is bound to a reference, which angular watches for changes, and whenever you change urls[mod2m3], it will affect the DOM.
You can also just react to changes in the scope variables by watching them:
$scope.$watch('urls', function() {
//do something
});
NOTE: Since this is an object, you might need to use $watchCollection instead of $watch.

Angular.js: In child controller inside ng-repeat, how to propagate up model changes

In my angular application, I am using an ng-repeat and, inside each, a child controller with a form for each item. I'm also using a debounce so that I can auto-save the data. But the issue is that after saving the data (with Restangular), the changes are not propagated back up to the array in the parent. A simple example will work using angular.copy, but this has issues with Restangular and its replacement, Restangular.copy, does not function exactly the same way. I also tried explicitly setting the item to the right index on the array, but it causes my cursor to lose focus in the input.
Here is a simplified version of my child controller's code. And here is a full JS Bin. Is there another approach to solve this problem?
$scope.personCopy = angular.copy($scope.person);
// Debounce and auto-save changes
$scope.$watch('personCopy', function(newVal, oldVal) {
if (newVal && newVal != oldVal) {
if (timeout) $timeout.cancel(timeout);
timeout = $timeout(savePerson, 1000);
}
}, true);
var savePerson = function() {
// (In my real app, the following is inside a save callback)
// Method 1: (doesn't work at all)
$scope.person = $scope.personCopy
// Method 2: (works with angular.copy, but not Restangular.copy)
// angular.copy($scope.personCopy, $scope.person);
// Method 3: (works, but cursor loses focus)
// $scope.people[$scope.$index] = $scope.personCopy;
};
Method 3 will work for you if you add "track by" to your ng-repeat:
<li ng-repeat='person in people track by $index' ng-controller='EditPersonController'>
Here's that working: http://jsbin.com/jitujaro/3/edit
The reason you're losing focus is the DOM for that ngRepeat is currently being recreated when you update people. So the element that the focus was on is gone. When using track by Angular knows it doesn't need to recreate those DOM elements.
The reason method 1 and 2 don't work is Javascript's prototypal inheritance. Upon writing to a variable that is on a parent scope a new local copy of that variable is made on the local scope. However, when writing to a property of an object that is on a parent scope (as you do in method 3), the write occurs on the parent object as you expect.
Try
angular.extend($scope.person, $scope.personCopy);
instead of
$scope.person = $scope.personCopy;

Categories