0

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:

  1. User presses escape from input box made by custom directive
  2. Code in parent scope is called through ngKeyup in order to change the value, by updating the bound model
  3. This update is not reflected in the data displayed by the directive
  4. 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.
  5. 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.
  6. And it still doesn't work.
  7. ???

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.

Community
  • 1
  • 1
std''OrgnlDave
  • 3,912
  • 1
  • 25
  • 34
  • 1
    please consider making a runnable plunker/jsfiddle. – Pengyy May 06 '17 at 03:23
  • I would love to, but it's too complicated and I'm not good enough at Angular to distill it. I didn't even write most of the Angular-specific code in the webapp. I was hoping someone with experience in AngularJS could guide me on the problem. – std''OrgnlDave May 06 '17 at 03:24
  • Plus I'm not sure how I would simulate the behavior of the asynchronous messaging controller correctly updating the model in a plunker/jsfiddle – std''OrgnlDave May 06 '17 at 03:31
  • you cant pass a callback to the directive that changes the model at the parent level? dunno if this is at all helpful http://embed.plnkr.co/oe3zknkd39cB6lp5AD1S/ – Daniel Lizik May 06 '17 at 04:16

0 Answers0