How do I reconcile Angular's "always use points with ngModel" rule with margins?

I am working on an Angular application using Bootstrap.

To minimize the Bootstrap footprint on my HTML, I introduced two directives for the forms:

form-control.js

module.directive('formControl', function() {
  return {
    restrict : 'E',
    templateUrl : 'form-control.tmpl.html',
    scope: {
      label: '@'
    },
    transclude : true
  };
});

      

form-control.tmpl.html

<div class="form-group">
  <label class="control-label col-sm-2">
    {{ label }}
  </label>
  <div class="col-sm-10"
       ng-transclude>
  </div>
</div>

      

I also have several "extensions" for this directive for input fields of various forms. eg:.

input-form-text.js

module.directive('formInputText', function() {
  return {
    restrict : 'E',
    templateUrl : 'form-input-text.tmpl.html',
    scope: {
      label: '@',
      value: '=ngModel'
    }
  };
});

      

form-entry-text.tmpl.html

<form-control label="{{label}}">
  <input type="text"
         class="form-control"
         ng-model="value">
</form-control>

      

app.html

<form-input-text label="Name"
                 ng-model="person.name">
</form-input-text>

      

I ran into a problem here. In this example, the game has several areas:

appScope = { person : { name : "John" } };
isolateScope = {
  label: "Name",
  value: "John" // bound two-way with appScope.person.name
};
transcludeScope = {
  __proto__: isolateScope,
  label: "Name", // inherited from isolateScope
  value: "John" // inherited from isolateScope
};

      

If I change the text in the input textbox only changes transcludeScope

:

appScope = { person : { name : "John" } };
isolateScope = {
  label: "Name",
  value: "John" // bound two-way with appScope.person.name
};
transcludeScope = {
  __proto__: isolateScope,
  label: "Name", // inherited from isolateScope
  value: "Alice" // overrides value from isolateScope
};

      

This is because it is <input>

bound directly to a property transcludeScope

. transcludeScope.value

changes directly, but the parent area isolateScope

does not change. This way, any changes to the model in the input are never reverted to appScope

.

I would like to create a two way binding between appScope.person.name

and a nested property isolateScope

for example. isolateScope.model.value

...

Ideally, I would like to declare my directive like this:

input-form-text.js

module.directive('formInputText', function() {
  return {
    restrict : 'E',
    templateUrl : 'form-input-text.tmpl.html',
    scope: {
      model: {
        label: '@',
        value: '=ngModel'
      }
    }
  };
});

      

This will allow the transcoded part to bind to model.value

, making the changes visible to the isolation of the Scope, which in turn propagates the changes isolateScope

to appScope

.

This usage does not appear to be directly supported by Angular.

Can someone point me to an Angular feature that supports this use case, or if not, provide a workaround?

Edit:

My solution currently is to insert the template form-control

into the template form-input-text

.

form-entry-text.tmpl.html

<div class="form-group">
  <label class="control-label col-sm-2">
    {{ label }}
  </label>
  <div class="col-sm-10">
    <input type="text"
           class="form-control"
           ng-model="value">
  </div>
</div>

      

This excludes the injected content area ng-transclude

, but it also duplicates the markup, which is why I was hoping to refactor in one place.

+3


source to share


2 answers


The thought of clouds actually changes a little on the wrong track, and I don't think the toggle has much to do with that. To do this "correctly" you must integrate with ngModelController

. This allows the later integrated parsers and formatters (which may contain validation logic) to be used to run at appropriate times. This is a little tricky since you have 2 of them, a parent in the application and one in the directive template, and each of them has 2 "pipelines" that need to be integrated with:

  • model value -> view value
  • view value -> model value

The view value of the parent ngModelController is then used as the model value of the internal ngModelController. So the common pipelines look like

  • parent model value -> parent view value -> internal model value -> internal view value
  • internal view value -> internal model value -> parent view value -> parent model value

For this:

  • Make sure you specify require: 'ngModel'

    in the directive definition to have access to the parentngModelController

  • Changes from the parent ngModelController to the internal one are done using the $render

    parent's method ngModelController

    using it $viewValue

    . This ensures that any functionality in the parent is executed $formatters

    .

  • Custom changes from an internal directive are done by adding a function to an array $viewChangeListeners

    that is called $setViewValue

    in the parent ngModelController

    . To access this from the scope of a binding, you need named forms and input elements. A little annoyance is that the form is only registered at the scope of the directive after its directive binding function runs, so you need an observer to access it.

  • Just in case of any oddities, make sure the model formInputText

    is in the object. (I'm not sure if this is technically necessary)

  • You don't need to have the model in the scope

    inner directive object .



Putting it together,

app.directive('formInputText', function() {
  return {
    restrict : 'E',
    templateUrl : 'form-input-text.tmpl.html',
    scope: {
      label: '@'
    },
    require: 'ngModel',
    link: function(scope, element, attrs, ngModelController) {
      scope.model = {};

      // Propagate changes from parent model to local
      ngModelController.$render = function() {
        scope.model.value = ngModelController.$viewValue;
      };

      // Propagate local user-initiated changes to parent model
      scope.$watch('form', function(form) {
        if (!form) return;
        form.input.$viewChangeListeners.push(function() {
          ngModelController.$setViewValue(form.input.$modelValue);
        });       
      });
    }
  };
});

      

And its template looks like

<form-control label="{{label}}" ng-form name="form">
  <input type="text"
         class="form-control"
         name="input"
         ng-model="model.value">
</form-control>

      

This can be seen at http://plnkr.co/edit/vLGa6c55Ll4wV46a9HRi?p=preview

+2


source


I would use a custom control for your case as described here , making the custom directives <form-input-*>

true. This requires some additional work. To describe a simplified version of the solution:

input-form-text.js

app.directive('formInputText', function() {
    return {
        restrict : 'E',
        template : '<form-control label="{{label}}"><input type="text" class="form-control" /></form-control>',
        scope: {
            label: '@'
        },
        require: 'ngModel',
        link: function(scope, elem, attrs, ngModel) {
            var input = angular.element(elem[0].querySelectorAll("input")[0]);

            ngModel.$render = function() {
                input.val(ngModel.$viewValue || '');
            };

            input.on('blur keyup change', function() {
                scope.$apply(read);
            });

            function read() {
                ngModel.$setViewValue(input.val());
            }
        }
    };
});

      



In short, you require

ngModel

also implement its methods according to the documentation. ngModel

is just another directive applied to your control and other things will work for example. custom validators, ng-required

etc.

Working fiddle: http://jsfiddle.net/1n53q59z/

Keep in mind that you might need to make some changes depending on your use case.

+1


source







All Articles