简体   繁体   中英

angular unable to $watch ng-model in custom directive

I have custom directives tagPickerTag , validationMessageTag and check-valid-article-meta-tags . Usage:

  <div class="my-form-group">
    <lable for="create_tags">Tags</lable>
    <tag-picker-tag ng-model="entityInfo.meta.tags" editable="true" name="tags" check-valid-article-meta-tags></tag-picker-tag>
    <validation-message-tag ctrl="form.tags"></validation-message-tag>
  </div>

This is how I define these 3 directives

tagPickerTag:

<div class="tag-picker-tag">
  tags
  <ui-select ng-model="$parent.ngModel" ng-disabled="! editable" multiple tagging tagging-tokens="SPACE|," tagging-label="(custom 'new' label)" title="Select tags" sortable="true" theme="bootstrap" >
    <ui-select-match placeholder="Enter Tags...">{{$item}}</ui-select-match>
    <ui-select-choices repeat="tag in suggestedTags | filter:$select.search">
      {{tag}}
    </ui-select-choices>
  </ui-select>
  <p>Selected: {{ngModel}}</p>
</div>



'use strict'
var helper = require('../../helper.js')
var app = angular.module('custom_directive')

app.directive('tagPickerTag', [function() {
    return {
        restrict: 'E',
        scope: {
            editable: '='
        },
        templateUrl: '/public/common/directive/tag_picker_tag.html',
        require: 'ngModel',
        link:
function(scope, element, attrs, ngModelCtrl) {
},
        controller:
['$scope',
function($scope) {
    //todo: get popular tags from server
    $scope.suggestedTags = ['superbowl', '2016election']

}]}}])

checkValidArticleMetaTags:

app.directive('checkValidArticleMetaTags', helper.simpleValidationDirective('article', 'meta', 'tags'))


exports.simpleValidationDirective = function(module, nestedInParent, field) {
    return function() {
        return {
            restrict: 'A',
            require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
    ctrl.$validators.checkValid = function(modelValue, viewValue) {
        var validationFunction = exports.validation[module]
        if (nestedInParent)
            validationFunction = validationFunction[nestedInParent]
        validationFunction = validationFunction[field]

        var message = validationFunction(modelValue)
        ctrl.data = exports.dataAppendedWithMessage({}, 'error', message)
        return ! message
    }
}}}}

In case you are curious what validationFunction in above code is (it should be irrelevant, since the validation directive correctly get the validation error message):

....
,meta: {
    tags: passIfListFulfill('tags', 10, 5, 10, false)
}

var passIfListFulfill = function(fieldName, amount, min, max, required) {
    return function(input) {
        if (!input || input === [])
            return messageForNoInput(fieldName, required)

        for (var i = 0; i < input.length; i++) {
            var token = input[i]
            if (token.length < min)
                return token + ' is shorter than min: ' + min
            else if (token.length > max)
                return token + ' is longer than max ' + max
        }

        return messageForNoMoreThan(fieldName, input, amount)
    }
}

ValidationMessageTag:

app.directive('validationMessageTag', [function() {
    return {
        restrict: 'E',
        scope: {
            ctrl: '=ngModel'
        },
        templateUrl: '/public/common/directive/validation_message_tag.html',
        controller:
['$scope',
function($scope) {

    $scope.$watch('ctrl.data', function(newValue, oldValue) {
        $scope.success = newValue ? newValue.success : []
        $scope.info = newValue ? newValue.info : []
        $scope.warning = newValue ? newValue.warning : []
        $scope.error = newValue ? newValue.error : []
    }, true)

}]}}])



<div class="validation-message-tag" ng-show="ctrl.$touched && ctrl.data">
  <p ng-repeat="message in success track by $index" class="validation-success">{{message}}</p>
  <p ng-repeat="message in info track by $index" class="validation-info">{{message}}</p>
  <p ng-repeat="message in warning track by $index" class="validation-warning">{{message}}</p>
  <p ng-repeat="message in error track by $index" class="validation-error">{{message}}</p>
</div>

When I enter tags ['a'] , in my validation directive, I am able to return false and assign a string "a" is too short to ctrl (which means my validation directive is correct).

but this message does not get passed into my validation_message_tag to display. ie the $watch callback is not invoked.

validtion_message_tag works fine for and tags, so i think the problem maybe the implementation of my tagPickerTag custom directive.

the $watch callback is not invoked.

So I couldn't get a simple scenario of $watch to work.

My thought was that ng-model="" is two way bound, you pick it up in the directive as scope: { "ngModel" : "=" } which is a two way binding, so when the value is changed you should see it reflected. So you wouldn't need a $watch . But I tried both however, neither worked.

So I used events instead.

plnkr

$scope.$broadcast(EventNames.statusChange, vm.success)

scope.$on(EventNames.statusChange, function (e, val) { scope.show = val });

A side note, to prevent 'Magic Strings' I made a constant out of the event names. This should help eliminate developer spelling mistakes

Done it with a factory as well. With a factory you don't rely on the $scope so doing controllerAs stays less verbose in the controller dependency list. Secondly, if it ain't working, you know its because the factory hasn't been used, or a callback hasn't been registered. Rather then messing around with the complex thing that is angular eventing system.

A side note on the Event Aggregator pattern (angular $broadcast and $on ), in my opinion this creates code that is far too loosely coupled leading to a lot of speghetti code. Another bad point about it to consider is that event enforces these rules:

  1. Everyone can listen to the event
  2. No one has too listen to the event

By creating a service you can enforce developers to listen to things they're broadcasting. In the registercallbacks function you can throw errors if no one has registered a listener. Also, there is now a dependency on StatusService meaning we are a bit tighter coupling the components. IMHO the sweet spot in code coupling.

Setting and listening:

StatusService.setState(vm.success);

StatusService.registerCallbacks(function (val) {  scope.show2 = val });

Implementations of the factory functions:

function setState(value) {
  for (var i = 0; i < cbs.length; i++) {
    cbs[i](value);
  }
}

function registerCallbacks(cb) {
  cbs.push(cb);
}

Essentially, they are the same thing, however using a factory in my opinion is safer, and you can potentially abstract some logic out into the factory. Rather than do it in the callback functions.

It turns out that I have a cookie flag isTesting set up and forgot to turn it off. When it's testing, I simply return true for all validators.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM