Mousetrap key bindings updating $scope but not detected by $watch. - javascript

Basically I have created a function to update a scope variable
// controller.js
$scope.variable = 0;
$scope.$watch('variable', function(){
console.log("change from watch");
});
$scope.increment = function(){
$scope.variable++;
console.log($scope.variable)
}
When I bind a key to this function,
Mousetrap.bind('j', $scope.increment)
the console.log in the browser shows that the variable is incremented when the key, in this case "j", is being pressed, but the $scope.$watch function above is never called and the console.log message "change from watch" is not fired.
When I attach a click handler, however, on the html file,
// index.html
<a ng-click="increment()">{{ variable }}</a>
The variable increases, the console.log shows that the $scope.variable is incremented and the $watch function is being fired as well.
In addition to this problem, {{ variable }} does not change when I use the key binding, yet it changes when I click on it.
My guess is that there is something lacking in Mousetrap that causes the function to fire but not in sync with AngularJS $scope?

Make sure you read about Databinding in AngularJS. Basically, in order for AngularJS to react to changes, something needs to trigger a $digest. The built-in ngClick directive does that under the hood.
When using non-AngularJS APIs (such as Mousetrap), you need to manually wrap the callback in $apply, e.g.:
Mousetrap.bind('j', () => $scope.$apply($scope.increment));

Related

Attach a regular JavaScript variable to Angular 2 model (input)?

My code scenario:
I have Angular 2 app which has, out of multiple fields, one input field (for lookup), that opens a decade old URL in a popup window (using window.open()) to get some lookup data. And it calls back a function from the window object of the parent page.
The function is defined in index.html page of the Angular 2 application like this:
<script>
function handler(res) {
var value = res;
}
</script>
The need:
The variable value now has to be tied/bound to the input's model named inputModel. Is there way by which this can be achieved?
Please Note: I am aware that this is not a good practice of having <script> tags, everything has to be component based. But its how I have received the task :-P
Thank you!
You can store the value on the window object like this: window['value'] = res;
Now, you can access the value of res inside your angular components like this window['value']
But, the problem now is that this will only work if the handler has already been invoked by the time the angular component reads the value from the window object. Your angular component will not be notified of any subsequent handler calls which update the value property in the window object.
So, you need a mechanism for the global handler function to notify your angular component that the value property on your window object has been updated.
Here is one way of communicating with your angular component from outside angular:
Inside your angular component's template, create a hidden input
Attach a (click) handler to this.
In your global handler function, use document.getElementById('hidden input's id').click() to simulate a click.
Now, the (click) handler you created in step 2, which is inside angular's context will get invoked.
Now, every time the global handler function is invoked, the (click) handler within angular's context will be invoked, essentially telling angular to get the updated value from the window object.
Here is a working Plunker
In this plunker, I've attached the global handler function to the html's onclick handler which is outside angular's context to simulate your requirement. This click event will be communicated to the angular component using the approach I mentioned above.

AngularJS doesn't update two-way bound model used in custom directive when model is changed from code called by that directive

You'll have to bear with me on this one, because it's a little involved and isn't answered by the following questions:
Two way binding Angularjs directives isn't working
AngularJS: Parent scope is not updated in directive (with isolated scope) two way binding
Both of those answers are more than 3 years old and no longer apply. Please do not mark this as a duplicate of those questions.
I wish that I could make a simple, reproducible version of this problem, but my Angular is honestly not good enough, and it is a fairly complex setup with lots of meddling from the outside. But the title is correct.
TL;DR version:
User presses escape from input box made by custom directive
Code in parent scope is called through ngKeyup in order to change the value, by updating the bound model
This update is not reflected in the data displayed by the directive
Putting code to do that update inside the directive itself, which directly updates its OWN bound model, to escape the problem of parent-scope bound-model changes not working, does not work.
In fact, even though it is changed inside its own directive controller, it sets the parent-scope model and its own model to the input value instead of the one I'm trying to overwrite with.
And it still doesn't work.
???
Long version with code:
I had a bunch of code I was repeating over and over again in my HTML, and decided to make a custom directive. The code uses many functions from the parent scope. You don't need to know what all these functions do, just note there's a ton of them that happen on click, on blur, etc. So, it looks like this:
template html file
<div ng-if="should_display" class="myclass" ng-class="cssSizeClass">
<div ng-if="allow_edits">
<span
class="text myapp-edit-hover"
ng-click="showEditField = true; captureValue(propertyString,binValue, $event)"
ng-hide="showEditField"
{{ binValue | customNumber: numberPlaceholder }}{{ numberType }}
</span>
<input
ng-show="showEditField"
class="myapp-editable-input"
type="text"
focus-on-show
ng-focus="selectText($event)"
ng-model="binValue"
ui-number-mask="numberPlaceholder"
ui-hide-group-sep
ng-blur="showEditField = false; inputUpdate(propertyString, binValue, 27, false )"
ng-keyup="inputUpdate(propertyString, binValue, $event, false)">
<span class="numberSuffix">{{ numberSuffix }}</span>
</div>
In my directive controller, in order to use those pesky parent-scope functions...
.directive('appDetailCardInput', function() {
return {
restrict: 'E',
templateUrl: '/static/js/directives/mytemplate.html',
scope: { // # = string, = = object, & = function
propertyName: '#',
cssSizeClass: '#',
numberPlaceholder: '#',
numberType: '#',
binValue: '=',
editable: '=',
numberSuffix: '#'
},
controller: function($scope) {
// define local properties
$scope.should_display = $scope.$parent.thing;
$scope.propertyString = 'thing.' + $scope.propertyName;
// HERE, outside functions are "captured"
$scope.captureValue = $scope.$parent.captureValue;
$scope.inputUpdate = $scope.$parent.inputUpdate;
$scope.selectText = $scope.$parent.selectText;
// check if field is editable
if (typeof $scope.editable === 'undefined') {
$scope.allow_edits = true;
}
else {
$scope.allow_edits = $scope.editable;
}
}}
};
});
This actually works 90%! When the bound model in the parent scope is updated outside the Angular digest cycle (the usual case is during a callback from an async messaging service), it updates the displayed value properly. Other things that occur "outside" the directive update it properly.
However. When escape is pressed or a blur happens, or any code is called that updates the value from "inside" the custom directive, no such luck.
I'd like to take a moment here to note that the $event target will blur properly when programmatically told to.
In the case of the escape key, the inputUpdate() function is supposed to set the value back as if someone hadn't typed something in the input. The problem comes here:
// (in inputUpdate function)
$event.target.blur(); // This works
$event.target.value = $scope.previouslyCapturedValue; // This works
setstr = "$scope.thing." + ser + " = " + $scope.previouslyCapturedValue + ";"; // This does not work
eval(setstr);
The eval() changes the parent scope model back, but that isn't reflected in the child scope.
So I tried both methods from the answers above. Adding a $watch in a link function only fires the SECOND time escape is pressed, not the first, so the first time someone hits escape on a field, it doesn't help! Fail.
The Angular docs suggest wrapping the changes to the value in a $timeout, so that the changes are applied outside of the digest cycle. Doing so doesn't help...
$timeout(function() {
console.log('evaluating outside digest supposedly');
$scope.nexttarget = $scope.nextval;
eval($scope.nextstr);
}, 0, false);
Nothing changes, the same behavior happens. The displayed value does not change (although the value that you change when clicking on it does).
So with some more digging, I find out that $event.target.value = $scope.previouslyCapturedValue; is just setting the value of the input, and not updating the actual bound model. That's fine. The bound model just needs to be updated too.
If I could somehow directly overwrite the text displayed on the , that would almost be a worthwhile solution compared to going back to how things were before.
But like I said before, when it is updated exactly the same as eval($scope.nextstr); from an external messaging service, the update happens perfectly correctly.
So at this point in the story I get it into my head: why not 'intercept' the ngKeyup directive and set the model right there inside the directive? I put this in my controller:
$scope.inputUpdate = function(ser, value, $event, dropdown) {
var keyCode = $event.keyCode;
console.log('got inputupdate!');
if ((parseInt(keyCode) == 27)
|| (typeof keyCode === 'undefined')) {
console.log('escape or deblur!');
if (ser !== $scope.dontBlur) {
console.log('going to set it!!!')
$scope.binValue = $scope.$parent.previouslyCapturedValue; // This is the correct value confirmed by console logs
}
}
$scope.$parent.inputUpdate(ser, value, $event, dropdown);
}
Nope! It gets there in code, but it still doesn't display the correct value! In fact, the bound model is updated to what had been typed in, even though it had been programmatically changed back right there!
Why does it update fine from outside its own code from another controller, but NOT from its own code, even if it's being done directly in the directive controller itself in the "intercepted" function?
Can't wrap my head around this. It's like the bound model is "locked" during this time period or something.
Thanks for any guidance.

Difference between on-change and ng-change

Below is a pretty straightforward setup for a form drop down list. However, the on-change event refuses to fire... unless it's changed to ng-change.
This had me stuck for about an hour, since we use the same setup elsewhere in our site (i.e. model property/list/on-change directive) without fail.
I'm wondering what the difference is between on-change and ng-change and why one works in this instance and the other doesn't?
View
<select id="grArchive"
name="grArchive"
class="form-control"
options="userList"
ng-model="selectedUser"
ng-options="user as user.name for user in userList"
on-change="showSelected()">
</select>
Controller
scope.selectedUser = {};
scope.userList = [];
scope.showSelected = function(){
scope.columns = addColumns;
};
var loadUserList = function(data){
scope.userList = $.map(data, function(item){
return {id: item.id, name: item.firstName + ' ' + item.lastName};
});
}
Just in case: the user drop down populates as expected (screen shot below)
ng-change
ng-change is a directive provided by Angular core API which internally registers an expression to be called when any change happens in $viewValue of ng-model variable (here is the code); assign it an expression such as myFunction(). That provided expression will evaluate inside the underlying controller scope. After calling an expression it runs a digest cycle to make sure bindings are updated on the view. Besides ng-change there are other directives used for events, like ng-focus,ng-mousedown,ng-submit, ng-blur, etc. Here is a list of such directives
on-change
It is a way of calling a JavaScript function on change of input element value. It will search for the function which is globally available in context (obviously it will not call the function registered in angular controller) and evaluate it.

Is there a Ready function for ng-bind-html in AngularJS?

I'm using ng-bind-html of AngularJS. I have to initialize a plugin after my Binding function is over. So I would like to know if there is a method or a function to do that work ?
Thanks for you answers !
One way you can do this is initialise a $watch event over the variable you are using in your 'ng-bind'. Let m explain:
Suppose you have a span with an ng-bind like this:
<span ng-bind="val"></span>
Now, 'ng-bind' works in a way that whenever 'val' changes(within the digest cycle) it updates the value of the span, that is, it keeps a 'watch' on the 'val'. Similarly, we can use our own watch on the 'val' in the controller like this:
$scope.val; //your binded variable
$scope.$watch('val', function(newval, oldval) {
//here you can initialise your plugin and also access the old and new values of 'val'
})

angularjs directives using templateUrl - initialisation sequence

I have scenario where I want to send a broadcast event in one controller and have the controller for a directive receive the message. The event is sent immediately on controller startup, and the issue is that because the directive is loading the view using templateUrl, it happens asynchronously, so the event is broadcast before the directive controller is intialised. This problem doesn't happen if the view is in the main page body, but I guess there could still be an issue with controller initialisation order.
I am using the following code in my main controller:
$rootScope.$broadcast("event:myevent");
I have reproduced the issue here: http://jsfiddle.net/jugglingcats/7Wf8N.
You can see in the Javascript console that the main controller is initialised before the controller for the directive, and it never sees the event.
So my question is whether there is a way to wait until all controllers are initialised before broadcasting an event?
Many thanks
I have created a working version. I actually feel that it is a very unclean way to do it, but I could not come up with something better: http://jsfiddle.net/7Wf8N/3/
What I did is this: In the directive I added some code which will increase a counter in $rootScope upon initialization. I use a counter because as you said, you want to wait for more than one controller:
$rootScope.initialized = ( $rootScope.initialized||0 ) +1;
In the "RegularCtrl" I added a watch on this counter and if the counter reaches the correct value (everything is initialized) I send the event:
$rootScope.$watch('initialized', function() {
if ( $rootScope.initialized == 1 ) {
$rootScope.$broadcast("event:myevent");
}
});
Are you ng-view? If so, you have the $viewContentLoaded event available. This will fire after all the dom is loaded.
http://docs.angularjs.org/api/ngRoute.directive:ngView
function MyCtrl($scope, $rootScope) {
$scope.$on('$viewContentLoaded', function() {
$rootScope.$broadcast('event:myevent');
});
}
If you aren't using ng-view, you could just set a variable and use data-binding to your directive.

Categories

Resources