Two way data binding with directive and isolate scope - angularjs

I'm trying to understand directives, and I'm having problems with two way data binding.
My directive will be used to submit a form when "enter" is pressed in a textarea.
I found a solution in another SO thread (see the code below in the scope definition of the directive), but I don't like it because it means that if I change the model name, I need to change the directive as well..
--> Here is the problem in codepen.io
Here is the html part:
<div ng-app="testApp" ng-controller="MyController">
<textarea ng-model="foo" enter-submit="submit()"></textarea><br/>
Binding: {{foo}}
</div>
Here is the javascript part:
var app = angular.module('testApp', []);
function MyController($scope) {
$scope.foo = "bar"
$scope.submit = function() {
console.log("Submitting form");
}
}
app.directive('enterSubmit', function () {
return {
restrict: 'A',
scope: {
submitFn: '&enterSubmit',
foo: '=ngModel' // <------------------- dont't like this solution
},
link: function (scope, elem, attrs) {
elem.bind('keydown', function(event) {
var code = event.keyCode || event.which;
if (code === 13) {
if (!event.shiftKey) {
event.preventDefault();
scope.submitFn();
}
}
});
}
}
});
Thanks for your help !

When multiple directives are used on an element, normally you don't want any of them to use an isolate scope, since that forces all of them to use the isolate scope. In particular, isolate scopes should not be used with ng-model – see Can I use ng-model with isolated scope?.
I suggest creating no new scope (the default, i.e., scope: false):
app.directive('enterSubmit', function () {
return {
restrict: 'A',
//scope: {
// submitFn: '&enterSubmit',
// foo: '=ngModel' // <------------------- dont't like this solution
//},
link: function (scope, elem, attrs) {
elem.bind('keydown', function(event) {
var code = event.keyCode || event.which;
if (code === 13) {
if (!event.shiftKey) {
event.preventDefault();
scope.$apply(attrs.enterSubmit);
}
}
});
}
}
});

Related

AngularJS Custom Validation Directives - How to avoid using isolated scope

I'm attempting to use multiple angular validators on a text field, but have run into the Multiple directives requesting isolated scope error. (Please read on before closing as a duplicate.)
All the solutions I've seen so far, recommend removing the scope: {...} from the offending directives, however for my scenario, I need to evaluate variables from the controller (and $watch them for changes).
I've tried using attrs.$observe, but I can't work out how to get the evaluated variables into the $validator function. (Additionally, I can't $observe the ngModel).
Please let me know if there's a another way to solve this issue.
Here's the smallest example I could put together. N.B. maxLength validator's scope is commented out, essentially disabling it:
angular
.module('app', [])
// validates the min length of a string...
.directive("minLen", function() {
return {
require: 'ngModel',
restrict: 'A',
scope: {
ngModel: '=',
minLen: '='
},
link: function(scope, element, attrs, ngModelCtrl) {
scope.$watch('ngModel', function(){
ngModelCtrl.$validate();
});
scope.$watch('minLen', function(){
ngModelCtrl.$validate();
});
ngModelCtrl.$validators.minLength = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return angular.isUndefined(scope.minLen) ||
angular.isUndefined(value) ||
value.length >= scope.minLen;
};
}
};
})
.directive("maxLen", function() {
return {
require: 'ngModel',
restrict: 'A',
// Commented out for now - causes error.
// scope: {
// ngModel: '=',
// maxLen: "="
// },
link: function(scope, element, attrs, ngModelCtrl) {
scope.$watch('ngModel', function(){
ngModelCtrl.$validate();
});
scope.$watch('maxLen', function(){
ngModelCtrl.$validate();
});
ngModelCtrl.$validators.maxLength = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return angular.isUndefined(scope.maxLen) ||
angular.isUndefined(value) ||
value.length >= scope.maxLen;
};
}
};
})
// this controller just initialises variables...
.controller('CustomController', function() {
var vm = this;
vm.toggleText = function(){
if (vm.text === 'aaa') {
vm.text = 'bbbbb';
} else {
vm.text = 'aaa';
}
}
vm.toggle = function(){
if (vm.minLen === 3) {
vm.minLen = 4;
vm.maxLen = 12;
} else {
vm.minLen = 3;
vm.maxLen = 10;
}
};
vm.toggleText();
vm.toggle();
return vm;
})
;
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
<div ng-app="app" ng-controller="CustomController as ctrl">
<ng-form name="ctrl.form">
<label>Enter {{ctrl.minLen}}-{{ctrl.maxLen}} characters: </label>
<input
name="text"
type="text"
ng-model="ctrl.text"
min-len="ctrl.minLen"
max-len="ctrl.maxLen"
/>
</ng-form>
<br/><br/>
<button ng-click="ctrl.toggle()">Modify validation lengths</button>
<button ng-click="ctrl.toggleText()">Modify ngModel (text)</button>
<h3>Validation (just list $error for now)</h3>
<pre>{{ ctrl.form.text.$error | json }}</pre>
</div>
remove isolated scope
You dont need to observe, watch ngModel - when it changes, angular will run validator for you.
Decide how you want to use your directive: my-val-dir="{{valName}}" vs my-val-dir="valName". In first case you use attrs.$observe('myValDir'), in second $watch(attrs.myValDir) & $eval.
When your value gonna be something simple, like number or short string - first way seems good, when value is something big, i.e. array - use second approach.
Remove isolate scope and use scope.$watch to evaluate the attribute:
app.directive("minLen", function() {
return {
require: 'ngModel',
restrict: 'A',
scope: false,
// ngModel: '=',
// minLen: '='
// },
link: function(scope, element, attrs, ngModelCtrl) {
var minLen = 0;
scope.$watch(attrs.minLen, function(value){
minLen = toInt(value) || 0;
ngModelCtrl.$validate();
});
ngModelCtrl.$validators.minLength = function(modelValue, viewValue) {
return ngModelCtrl.$isEmpty(viewValue) || viewValue.length >= minLen;
};
}
};
})
There is no need to watch the ngModel attribute as the ngModelController will automatically invoke the functions in the $validators collection when the model changes.
When the watch expression is a string, the string attribute will be evaluated as an Angular Expression.
For more information, see AngularJS scope API Reference - $watch.

Custom directive with dynamic template and binding parent scope to ng-model

I have a view that contains a template specified in a custom directive. The template that is used in the custom directive depends on 'dynamicTemplate':
View:
<div ng-controller="MyController">
<custom-dir dynamicTemplate="dynamicTemplateType"></custom-dir>
<button ng-click="ok()">Ok</button>
</div>
View's Controller:
myModule
.controller('MyController', ['$scope', function ($scope) {
$scope.dynamicTemplateType= 'input';
$scope.myValue = "";
$scope.ok = function()
{
// Problem! Here I check for 'myValue' but it is never updated!
}
Custom Directive:
myModule.directive("customDir", function ($compile) {
var inputTemplate = '<input ng-model="$parent.myValue"></input>';
var getTemplate = function (templateType) {
// Some logic to return one of several possible templates
if( templateType == 'input' )
{
return inputTemplate;
}
}
var linker = function (scope, element, attrs) {
scope.$watch('dynamicTemplate', function (val) {
element.html(getTemplate(scope.dynamicTemplate)).show();
});
$compile(element.contents())(scope);
}
return {
restrict: 'AEC',
link: linker,
scope: {
dynamicTemplate: '='
}
}
});
In this above example, I want 'myValue' that is in MyController to be bound to the template that is in my custom directive, but this does not happen.
I noticed that if I removed the dynamic templating (i.e. the contents in my directive's linker) and returned a hardcoded template instead, then the binding worked fine:
// If directive is changed to the following then binding works as desired
// but it is no longer a dynamic template:
return {
restrict: 'AEC',
link: linker,
scope: {
dynamicTemplate: '='
},
template: '<input ng-model="$parent.myValue"></input>'
}
I don't understand why this doesn't work for the dynamic template?
I am using AngularJS 1.3.0.
Maybe you should pass that value into your directives scope, instead of only dynamicTemplate, i think it should work.
You have a good answer about directives scope here: How to access parent scope from within a custom directive *with own scope* in AngularJS?
Hope I was of any help.
js directive :
angular.module('directive')
.directive('myDirective', function ($compile) {
var tpl1 ='<div class="template1">{{data.title}}</div>';
var tpl2 ='<div class="template2">Hi</div>';
var getTemplate = function (data) {
if (data.title == 'hello') {
template = tpl1;
}
else {
template = tpl2;
}
return template;
};
var linker = function (scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$render = function () {
// wait for data from the ng-model, particulary if you are loading the data from an http request
if (scope.data != null) {
element.html(getTemplate(scope.data));
$compile(element.contents())(scope);
}
};
};
return {
restrict: "E",
require: 'ngModel',
link: linker,
scope: {
data: '=ngModel'
}
};
});
html :
<my-directive
ng-model="myData">
</my-directive>
js controller:
$scope.myData = {title:'hello'};

Angular Isolate Scope breaks down?

I have the following markup:
<div class="controller" ng-controller="mainController">
<input type="text" ng-model="value">
<div class="matches"
positions="{{client.positions | filter:value}}"
select="selectPosition(pos)">
<div class="match"
ng-repeat="match in matches"
ng-click="select({pos: match})"
ng-bind="match.name">
Then, inside my matches directive I have
app.directive('matches', function()
{
return {
scope: {
select: '&'
},
link: function(scope, element, attrs)
{
scope.matches = [];
attrs.$observe('positions', function(value)
{
scope.matches = angular.fromJson(value);
scope.$apply();
})
}
}
}
When I do this, I can console log scope.matches, and it does change with the value from my input. However, the last div .match doesn't render anything! If I remove scope: {...} and replace it with scope: true, then it does render the result, but I want to use the & evaluation to execute a function within my main controller.
What do i do?
Use scope.$watch instead, you can watch the attribute select whenever changes are made from that attribute.
app.directive('matches', function()
{
return {
scope: true,
link: function(scope, element, attrs)
{
scope.matches = [];
scope.$watch(attrs.select, function(value) {
scope.matches = angular.fromJson(value);
});
}
}
}
UPDATE: Likewise, if you define select itself as a scope attribute, you must use the = notation(use the & notation only, if you intend to use it as a callback in a template defined in the directive), and use scope.$watch(), not attr.$observe(). Since attr.$observe() is only used for interpolation changes {{}}, while $watch is used for the changes of the scope property itself.
app.directive('matches', function()
{
return {
scope: {select: '='},
link: function(scope, element, attrs)
{
scope.matches = [];
scope.$watch('select', function(value) {
scope.matches = angular.fromJson(value);
});
}
}
}
The AngularJS Documentation states:
$observe(key, fn);
Observes an interpolated attribute.
The observer function will be invoked once during the next $digest
following compilation. The observer is then invoked whenever the
interpolated value changes.
Not scope properties defined as such in your problem which is defining scope in the directive definition.
If you don't need the isolated scope, you could use $parse instead of the & evaluatation like this:
var selectFn = $parse(attrs.select);
scope.select = function (obj) {
selectFn(scope, obj);
};
Example Plunker: http://plnkr.co/edit/QZy6TQChAw5fEXYtw8wt?p=preview
But if you prefer the isolated scope, you have to transclude children elements and correctly assign the scope of your directive like this:
app.directive('matches', function($parse) {
return {
restrict: 'C',
scope: {
select: '&',
},
transclude: true,
link: function(scope, element, attrs, ctrl, transcludeFn) {
transcludeFn(scope, function (clone) {
element.append(clone);
});
scope.matches = [];
attrs.$observe('positions', function(value) {
scope.matches = angular.fromJson(value);
});
}
}
});
Example Plunker: http://plnkr.co/edit/9SPhTG08uUd440nBxGju?p=preview

Angular - Directive hides input

I have a custom directive:
.directive('myDirective', function() {
return {
scope: {ngModel:'='},
link: function(scope, element) {
element.bind("keyup", function(event) {
scope.ngModel=0;
scope.$apply();
});
}
}
});
This works as planned, setting the variables to 0 on keyup, but it doesn't reflect the changes on the input themselves. Also when initialized, the values of the model are not in the input. Here is an example:
http://jsfiddle.net/prXm3/
What am I missing?
You need to put a watcher to populate the data since the directive creates an isolated scope.
angular.module('test', []).directive('myDirective', function () {
return {
scope: {
ngModel: '='
},
link: function (scope, element, attrs) {
scope.$watch('ngModel', function (val) {
element.val(scope.ngModel);
});
element.bind("keyup", function (event) {
scope.ngModel = 0;
scope.$apply();
element.val(0); //set the value in the dom as well.
});
}
}
});
Or, you can change the template to
<input type="text" ng-model="$parent.testModel.inputA" my-directive>
the data will be populated thought it will break your logic to do the event binding.
So it is easier to use the watcher instead.
Working Demo

Angular.js binding model to directive

I'm trying to put together an Angular directive that will be a replacement for adding
ng-disabled="!canSave(schoolSetup)"
On a form button element where canSave is a function being defined in a controller something like the following where the parameter is the name of the form.
$scope.canSave = function(form) {
return form.$dirty && form.$valid;
};
Ideally I'd love the directive on the submit button to look like this.
can-save="schoolSetup"
Where the string is the name of the form.
So... how would you do this? This is as far as I could get...
angular.module('MyApp')
.directive('canSave', function () {
return function (scope, element, attrs) {
var form = scope.$eval(attrs.canSave);
function canSave()
{
return form.$dirty && form.$valid;;
}
attrs.$set('disabled', !canSave());
}
});
But this obviously doesn't bind properly to the form model and only works on initialisation. Is there anyway to bind the ng-disabled directive from within this directive or is that the wrong approach too?
angular.module('MyApp')
.directive('canSave', function () {
return function (scope, element, attrs) {
var form = scope.$eval(attrs.canSave);
scope.$watch(function() {
return form.$dirty && form.$valid;
}, function(value) {
value = !!value;
attrs.$set('disabled', !value);
});
}
});
Plunker: http://plnkr.co/edit/0SyK8M
You can pass the function call to the directive like this
function Ctrl($scope) {
$scope.canSave = function () {
return form.$dirty && form.$valid;
};
}
app.directive('canSave', function () {
return {
scope: {
canSave: '&'
},
link: function (scope, element, attrs) {
attrs.$set('disabled', !scope.canSave());
}
}
});
This is the template
<div ng-app="myApp" ng-controller="Ctrl">
<div can-save="canSave()">test</div>
</div>
You can see the function is called from the directive. Demo

Resources