I'm trying to make a generic form validatation indicator. If the form is valid, it displays saying that the form is valid, else it displays an error saying that the form isn't valid.
I'm using bookingForm.$valid or SOMEFORM.$valid - but the ng-show isn't working. I think the connection between {{formValid}} and the actual form model isn't right.
Here's my directive:
sharedServices.directive('formValid', function() {
return {
restrict: 'A',
scope: {
formValid: '#'
},
template: '<div class="alert alert-warning" ng-show="!{{formValid}}.$valid"><b>ATH!</b> Útfylling ekki í lagi</div>' +
'<div class="alert alert-success" ng-show="{{formValid}}.$valid"><b>OK!</b> Útfylling í lagi</div>',
link: function(scope, element, attrs) {
scope.formValid = attrs.formValid;
}
};
});
Here's my usage:
<div form-valid="bookingForm"></div>
<form name="bookingForm">...</form>
<div form-valid="contactForm"></div>
<form name="contactForm">...</form>
I want to pass the form name (model) into my directive's template and display accordingly whether $valid is true/false on the forms model. Do I need to compile the template or how can I accomplish this?
Here's a GIF showing that it doesn't work, bookingForm.$valid is correct in the HTML, but always false in the directive's template.
Because ng-show creates a new scope, you need access the form in the $parent scope. It's a bit tricky in terms of the model assignment. You can use attrs.$observe to assign the object instead of the value of $valid.
Please try this code:
sharedServices.directive('formValid', function () {
return {
restrict: 'A',
scope: {
formValid: '#'
},
template: '<div class="alert alert-warning" ng-show="!formValid.$valid"><b>ATH!</b> Útfylling ekki í lagi</div>' +
'<div class="alert alert-success" ng-show="formValid.$valid"><b>OK!</b> Útfylling í lagi</div>',
link: function (scope, element, attrs) {
attrs.$observe('formValid', function () {
scope.formValid = scope.$parent[attrs.formValid];
});
}
};
});
DEMO
Related
I have a directive that displays an icon with a little cross. When the user clicks on this cross, a callback should be called.
Here's the code of the directive template:
<div class="item" title="{{name}}">
<button type="button" class="close">
<span ng-click="onDelete()">×</span>
</button>
<span class="glyphicon glyphicon-folder-open"></span>
</div>
The Javascript of the directive:
angular.module('hiStack').directive('hsItem', function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'item.tpl.html',
scope: {
onDelete: '&',
title: '#'
}
};
});
Finally, the code that uses the directive:
<hs-item on-delete="deleteGroupModal = true" title="TestTitle"></hs-item>
deleteGroupModal = true is never called when I click on the cross. If I pass a function instead like deleteGroup(), it works.
How can I pass an assignment like we usually do with ng-click for example?
Thank you.
As Igor Janković said, it's better to pass a function than to write it directly on the attribute.
That said, it's possible to eval the expression passed on the attribute like this:
angular.module('hiStack').directive('hsItem', function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'item.tpl.html',
scope: {
title: '#'
},
link: function(scope, element, attrs) {
scope.onDelete = function() {
// Eval the code on the parent scope because directive's scope is isolated in this case
if (attrs.onDelete) scope.$parent.$eval(attrs.onDelete);
}
}
};
});
How do I create an angular directive that adds a input in a form but also works with form validation.
For instance the following directive creates a text input:
var template = '\
<div class="control-group" ng-class="{ \'error\': {{formName}}[\'{{fieldName}}\'].$invalid}">\
<label for="{{formName}}-{{fieldName}} class="control-label">{{label}}</label>\
<div class="controls">\
<input id="{{formName}}-{{fieldName}}" name="{{fieldName}}" type="text" ng-model="model" />\
</div>\
</div>\
';
angular
.module('common')
.directive('formTextField', ['$compile', function ($compile) {
return {
replace: true,
restrict: 'E',
scope: {
model: '=iwModel',
formName: '#iwFormName',
fieldName: '#iwFieldName',
label: '#iwLabel'
},
transclude: false,
template: template
};
}])
;
However the input is not added to the form variable ($scope.formName). I've been able to work around that by using the dynamicName directive from the following stackoverflow question Angularjs: form validation and input directive but ng-class will still not work.
Update
It now seems to be working but it feels like a hack. I thought I needed the scope to grab the form and field names however I can read that directly from the attributes. Doing this shows the field in the controllers scope form variable. However the ng-class attribute does not apply. The only way around this is to add the html element a second time once the scope is available.
jsFiddle here
var template = '\
<div class="control-group">\
<label class="control-label"></label>\
<div class="controls">\
<input class="input-xlarge" type="text" />\
</div>\
</div>\
';
angular
.module('common')
.directive('formTextField', ['$compile', function ($compile) {
return {
replace: true,
restrict: 'E',
scope: false,
compile: function compile(tElement, tAttrs, transclude) {
var elem = $(template);
var formName = tAttrs.iwFormName;
var fieldName = tAttrs.iwFieldName;
var label = tAttrs.iwLabel;
var model = tAttrs.iwModel;
elem.attr('ng-class', '{ \'error\': ' + formName + '[\'' + fieldName + '\'].$invalid }');
elem.find('label').attr('for', formName + '-' + fieldName);
elem.find('label').html(label);
elem.find('input').attr('id', formName + '-' + fieldName);
elem.find('input').attr('name', fieldName);
elem.find('input').attr('ng-model', model);
// This one is required so that angular adds the input to the controllers form scope variable
tElement.replaceWith(elem);
return {
pre: function preLink(scope, iElement, iAttrs, controller) {
// This one is required for ng-class to apply correctly
elem.replaceWith($compile(elem)(scope));
}
};
}
};
}])
;
when I do something like this, I use the directive compile function to build my html prior to it being processed. For example:
myApp.directive('specialInput', ['$compile', function($compile){
return {
// create child scope for control
scope: true,
compile: function(elem, attrs) {
// use this area to build your desired dom object tree using angular.element (eg:)
var input = angular.element('<input/>');
// Once you have built and setup your html, replace the provided directive element
elem.replaceWith(input);
// Also note, that if you need the regular link function,
// you can return one from here like so (although optional)
return function(scope, elem, attrs) {
// do link function stuff here
};
}
};
}]);
In the following AngularJS code, when you type stuff into the input field, I was expecting the div below the input to update with what is typed in, but it doesn't. Any reason why?:
html
<div ng-app="myApp">
<input type="text" ng-model="city" placeholder="Enter a city" />
<div ng-sparkline ng-model="city" ></div>
</div>
javascript
var app = angular.module('myApp', []);
app.directive('ngSparkline', function () {
return {
restrict: 'A',
require: '^ngModel',
template: '<div class="sparkline"><h4>Weather for {{ngModel}}</h4></div>'
}
});
http://jsfiddle.net/AndroidDev/vT6tQ/12/
Add ngModel to the scope as mentioned below -
app.directive('ngSparkline', function () {
return {
restrict: 'A',
require: '^ngModel',
scope: {
ngModel: '='
},
template: '<div class="sparkline"><h4>Weather for {{ngModel}}</h4></div>'
}
});
Updated Fiddle
It should be
template: '<div class="sparkline"><h4>Weather for {{city}}</h4></div>'
since you are binding the model to city
JSFiddle
The basic issue with this code is you aren't sharing "ngModel" with the directive (which creates a new scope). That said, this could be easier to read by using the attributes and link function. Making these changes I ended up with:
HTML
<div ng-sparkline="city" ></div>
Javascript
app.directive('ngSparkline', function ($compile) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var newElement = '<div class="sparkline"><h4>Weather for {{' + attrs.ngSparkline + '}}</h4></div>';
element.append(angular.element($compile(newElement)(scope)));
}
}
});
Using this pattern you can include any dynamic html or angular code you want in your directive and it will be compiled with the $compile service. That means you don't need to use the scope property - variables are inherited "automatically"!
Hope that helps!
See the fiddle: http://jsfiddle.net/8RVYD/1/
template: '<div class="sparkline"><h4>Weather for {{city}}</h4></div>'
the issue is that require option means that ngSparkline directive expects ngModel directive controller as its link function 4th parameter. your directive can be modified like this:
app.directive('ngSparkline', function () {
return {
restrict: 'A',
require: '^ngModel',
template: '<div class="sparkline"><h4>Weather for {{someModel}}</h4></div>',
link: function(scope, element, attrs, controller) {
controller.$render = function() {
scope.someModel = controller.$viewValue;
}
}
}
});
but this creates someModel variable in scope. that I think isn't necessary for this use case.
fiddle
So, have created a few angular directives -- picture "user controls" around common data entry elements, like a label-textbox pair, etc
The problem we are having is that the zValidate directive does not seem to work from within the directive. Is there something we need to do to make nested directives work?
Edit
Here are the relevant code snippets.
So, first, we have a little directive that adds a label-input pair:
app.directive('afLabelInputPair', function ($compile) {
var directive = {
restrict: 'A',
transclude: true,
replace: true,
scope: { //#textValue =twoWayBinding &oneWayBinding
labelText: '#labelText',
afModel: '=',
afId: '#',
afPlaceholder: '#'
},
templateUrl: './app/templates/af-label-input-pair.html',
link: function (scope, element, attrs) {
scope.opts = attrs;
$compile(element.contents())(scope);
}
}
return directive;
});
Next, we have the template html (this is what's returned from templateUrl:
<div class="form-group">
<label class="control-label" for="{{afId}}">{{labelText}}</label>
<input id="{{afId}}" class="form-control" ng-model="afModel" placeholder="{{afPlaceholder}}" data-z-validate />
</div>
But, we don't get a display of breeze validation errors when we use this directive.
I have written the following Angular directive:
angular.module('solarquote.directives', []).directive('editfield', function() {
return {
restrict: 'A',
transclude: true,
template: '<span ng-hide="editorEnabled" ng-transclude></span>' + // viewable field
'<span ng-show="editorEnabled"><input class="input-medium" ng-model="editableField"></span>', // editable field
link: function(scope, elm, attrs, ctrl) {
scope.editorEnabled = false;
scope.editableField = elm.children[0].children[0].innerText;
}
};
})
And in the html, inside a ng-repeat:
<span editfield>{{ item.fields.name }}</span>
I would like to prepopulate the input field in the directive's template with the same content in the ng-transclude. Going through the DOM and grabbing the text yields: {{ item.fields.name }} instead of the rendered data: "Bob" (or whatever name).
What is the best way to access the transcluded data?
Thanks
It is not possible to assign to ng-model an expression that you specify in transclusion block. This is because a transclusion block can be an expression like {{ functionValue() }} or {{ field1+':'+field2 }}. Angular simply does not know how to reverse those expressions.
What you can do, is provide a reference to the model you want to update. See the following punkler http://plunker.co/edit/NeEzetsbPEwpXzCl7kI1?p=preview (needs jQuery)
directive('editfield', function() {
var template = ''+
'<span ng-show="editorEnabled"><input class="input-medium" ng-model="editfield"></span>'+
'<span ng-hide="editorEnabled" ng-transclude></span>';
return {
restrict: 'A',
template: template,
scope:{
editfield:'='
},
transclude:true,
link: function(scope, element, attrs) {
var input = element.find('input');
input.on('blur',function(){
scope.editorEnabled=false;
scope.$apply();
});
element.bind('click',function(){
scope.editorEnabled=!scope.editorEnabled;
scope.$apply();
input.focus();
})
}
};
})