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.
source to share
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 methodngModelController
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 parentngModelController
. 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
source to share
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.
source to share