AngularJS: trigger ng-change, ng-keyup or $scope.$watch while composing Korean characters

All we need is an easy explanation of the problem, so here it is.

I’m building a Korean vocabulary trainer and I want to compare user input as people type.

In Korean and some other Asian languages, you compose letters with multiple keyup events. In Chrome, $scope.$watch, ng-keyup and ng-change only get triggered after the letter has been fully composed and either a new letter or a space has been entered. I don’t mind AngularJS not triggering anything until the last letter has been fully composed but once the letter has been completed, it should trigger without having to add a whitespace or starting the next word.

HTML:

<form name="forms.vocabularyForm">
  <input name="answer" id="answer" ng-model="vocabularyCtrl.answer" ng-change="vocabularyCtrl.checkChange()" ng-keyup="vocabularyCtrl.checkKeyUp($event)" type="text" />
</form>

Controller:

.controller('VocabularyCtrl', [
  '$scope',
  '$location',
  function($scope, $location) {

    this.checkChange = function () {
      console.log("answer change: " + this.answer); 
    };

    this.checkKeyUp = function ($event) {
      console.log("answer keyUp: " + this.answer);
    };

    $scope.$watch('vocabularyCtrl.answer', function (answerNew, answerOld) {        
      console.log('answerOld: ' + answerOld + ', answerNew: ' + answerNew);         
    }, true);        

  };     
]);        

Example:

Input: ㄱ 
Console:
answerOld: , answerNew:
answer keyUp:

Input: 가
Console:
answerOld: , answerNew:    
answer keyUp:

Input: 감 (character is now fully composed)
Console:
answerOld: , answerNew:    
answer keyUp:

Input: 감ㅅ (starting the next character, same behaviour with space bar)
Console:
answerOld: 감, answerNew:
answer change: 감
answer keyUp: 감                  

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

As explained by a helpful member of the Angular team, all triggers are intentionally suppressed while composing characters. More details here.

As suggested, I created a custom directive that manually updates the model while composing characters:

Directive:

(function() {
  'use strict';

  angular.module('myApp', [])

  // Angular's ng-change, ng-keyup and $scope.$watch don't get triggered
  // while composing (e.g. when writing Korean syllables).
  // See: https://github.com/angular/angular.js/issues/10588  
  // This custom directive uses element.on('input') instead, which gets
  // triggered while composing.
  .directive('cstInput', function() {
    return {
      restrict: 'A',
      require: '^ngModel',
      scope: {
        ngModel: '=', // sync model
      },      
      link: function (scope, element, attrs, ngModel) {
        element.on('input', function() {          
          scope.ngModel = element.val();       
        });
      }
    };
  });  
})();

Controller: (as suggested by ippi)

$scope.$watch('quizzesCtrl.answer', function (answer) {
  console.log(answer);
});

HTML:

<form ng-controller="QuizzesController as quizzesCtrl">
  <input cst-input name="answer" id="answer" ng-model="quizzesCtrl.answer" type="text" />
</form>

Update

I had to change the code to the following to make it work in FireFox (Chrome & Safari work fine with the code above).

Directive:

(function() {
  'use strict';

  angular.module('myApp', [])

  // Angular's ng-change, ng-keyup and $scope.$watch don't get triggered
  // while composing (e.g. when writing Korean syllables).
  // See: https://github.com/angular/angular.js/issues/10588  
  // This custom directive uses element.on('input') instead, which gets
  // triggered while composing.
  .directive('cstInput', function() {
    return {
      restrict: 'A',
      require: '^ngModel',    
      link: function (scope, element, attrs, ngModel) {
        element.on('input', function() {                  
          scope.$apply(function(){            
            scope.ngModel = element.val();
            scope.$eval(attrs.cstInput, {'answer': scope.ngModel}); // only works if no scope has been defined in directive
          });
        });
      }
    };
  });

})();

Controller:

this.checkAnswer = function (answer) {        
  if (answer === this.quiz.answer) {        
    this.isCorrect = true; 
  }
};

HTML (note that any passed in argument needs to be listed in cst-input-callback):

<form ng-controller="QuizzesController as quizzesCtrl">
  <input cst-input="quizzesCtrl.checkAnswer(answer)" ng-model="quizzesCtrl.answer" name="answer" id="answer" type="text" />
</form>

Method 2

I found this error in Angular.js 1.2.27, I tried other versions but i coulnd’t get any problem. But I found a solution and this will solve your problem.

Take a look at this solution
https://github.com/mbenford/ngTagsInput/issues/303

angular.module('angularApp')
.directive('ignoreCompositionEvent', function () {
    return {
        restrict: 'A',
        link: function postLink(scope, element) {
            //element.val('this is the ignoreCompositionEvent directive');
            element.off('compositionstart').off('compositionend');
        }
    };
});

This is an example . Just open your console and type 한글 in Input.

Method 3

Using a $watch you’ll be able to catch all updates to the model:

Working example (jsfiddle):

HTML:

<div ng-app ng-controller="myctrl">
    <input type="text" ng-model="answer" />
</div>

JS:

function myctrl($scope) {
    $scope.$watch('answer',function(oldVal,newVal){
        console.log(oldVal,newVal);
    });
}

And for reference, it looks like it could be possible to use ng-model-options and assign a compositionupdate-event to updateOn. I had no luck with it and resorted to a $watch instead.

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply