I would like to create form with fields created in directive. Data binding of data working correctly but validation doesn't work.
this is html:
<body ng-controller="MainCtrl">
<h1>form</h1>
<form name="form">
<div ng-repeat="conf in config">
<div field data="data" conf="conf"></div>
</div>
</form>
<pre>{{data|json}}</pre>
</body>
controller and field directive:
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.data = {name: '', age: ''}
$scope.config = [
{field: 'name', required:true},
{field: 'age'}
];
});
app.directive('field', function ($compile) {
return {
scope: {
data: '=',
conf: '='
},
link: function linkFn(scope, element, attrs) {
// field container
var row = angular.element('<div></div>');
// label
row.append(scope.conf.field + ': ');
// field input
var field = angular.element('<input type="text" />');
field.attr('name', scope.conf.field);
field.attr('ng-model', 'data.' + scope.conf.field);
if (scope.conf.required) {
field.attr('required', 'required');
}
row.append(field);
// validation
if (scope.conf.required) {
var required = angular.element('<span>required</span>');
required.attr('ng-show',
'form.' + scope.conf.field + '.$error.required');
row.append(required);
}
$compile(row)(scope);
element.append(row);
}
}
});
problem is that validation for field name doesn't work and validation text required is never shown. May be form in ng-show is unknown in directive. But I don't know how to pass form into field directive. Can you help me how to fix it? Thanks.
here is live code: http://plnkr.co/edit/j0xc7iV1Sqid2VK6rMDF?p=preview
Todo:
before:
$compile(row)(scope);
element.append(row);
after:
element.append(row);
$compile(row)(scope);
p/s in 'planker' for facilities add css:
.ng-invalid {
border: 1px solid red;
}
You'll need to use ng-form directive and push the dynamic field directly into form object.
This thread can help you out:
https://github.com/angular/angular.js/issues/1404
Here is a plunker forked from yours to fix you're issue:
http://plnkr.co/edit/qoMOPRoSnyIdMiZnbnDF?p=preview
To summarize, I added a watch that will toggle the error message instead of using the ng-show directive. Things can get hairy when you attempt to dynamically add a directive within a directive link. For a simple use case as this, it is quicker to add your own watch.
You may also look at this directive which is preconfigured to handle many use cases for validation as well as allow you to create custom validations easily https://github.com/nelsonomuto/angular-ui-form-validation
var toggleRequiredErrorMessage = function (invalid) {
if(invalid === true) {
addRequiredErrorMessage();
} else {
removeRequiredErrorMessage();
}
};
scope.$watch( watchIfRequired, toggleRequiredErrorMessage );
Related
I'm trying to implement a directive for uncheckable radio buttons.
Context:
My customer asked this feature so user can reuse the same form with different criterias, ie being able to deselect previously used fields.
In the case of this field, he want it to stay a radio button, not a select.
My current Plunker is available here (not working).
I've succeeded with a method in ng-click when the radio button's model uses simple string values. (see the 'sexe' field in the Plunker example)
But this totally fails when model uses objects.
Ive tried $viewChangeListeners but it doesnot trigger on reclicking on same radio button, ie it considers no change... Same problem with $parsers and ng-change.
It seems like ng-click triggers AFTER model changes...
Ideally i'd like to reset the model if radio button already checked, problem $modelValue and :checked already changed when checking in element.on("click")
So i'm trying with a directive in toggleRadioCheckedState.js:
(function () {
'use strict';
angular
.module('formExample', [])
.controller('FormController', FormController);
function FormController () {
var vm = this;
var defaultCriteres = {
site: null
};
vm.sites = [
{ id: 1, name: 'Nantes', address: 'address1' },
{ id: 2, name: 'Lille', address: 'address2' },
{ id: 3, name: 'Lyon', address: 'address3' }
];
}
})();
(function () {
'use strict';
angular
.module('formExample')
.directive('toggleCheckState', toggleCheckState);
function toggleCheckState ($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attributes, ngModelController) {
var lastCheckedValue = null;
element.on('click', function (event) {
var test = angular.equals(
ngModelController.$viewValue,
lastCheckedValue
);
console.log(
'lastCheckedValue before', lastCheckedValue,
'\n$viewValue', ngModelController.$viewValue,
'\nevent.target.value', typeof event.target.value,
'\ntest', test
);
if (!test) lastCheckedValue = ngModelController.$viewValue;
else {
ngModelController.$setViewValue(null);
event.target.checked = false;
lastCheckedValue = null;
}
console.log('lastCheckedValue after', lastCheckedValue, "\n================================\n");
});
}
}
}
})();
<script src="//unpkg.com/angular/angular.js"></script>
<form ng-app="formExample" ng-controller="FormController as ctrl">
<label ng-repeat="site in ctrl.sites">
<input type="radio" name="site" ng-model="ctrl.criteres.site"
ng-value="site" toggle-check-state>
{{site.name}}
</label>
</form>
The problem is i'm first getting desired behavior on a single radio button, but as soon as i check other radio button then come back to previous radio button, the behavior fails...
I don't really understand why the internal var lastCheckedValue does not update properly...
Background
I looked and found nothing like this so I have been trying to create a reusable input wrapper element directive that can be dropped into a regular angular form, interact with the ngModel and Form controllers properly and be able to take the same validation directives that a regular input takes (e.g., required, ng-minlength, ...) and any other custom directives and apply them to a child input element it contains.
Basically the idea is that this directive is a more advanced form of the regular input (provides bootstrap 3 validation highlighting, validation icon inside the textbox itself, and more).
Problems:
It should be possible to dynamically decorate the child input with list of directives passed into the wrapper directive.
Recompiling the directive helps attach/detach directives to the
child input, but somehow the Watchers on the main scope keep
increasing (memory leak)? I believe I need to destroy the scope of
the directive and renew it to solve this problem but I haven't
figured out a way to do that without causing digest errors.
This is likely related to #1, but the form itself also shows signs
of improper cleanup. The $error field that corresponds to errors
would show multiple error objects related to "required" after you
inject a few different directives, showing that each time the
directive is recompiled, it leaves some residue on the parent form.
Full Demo: http://codepen.io/b3hrooz/pen/aORzrK
Wrapper Directive:
.directive("validatableInput", ['$compile', function($compile) {
return {
restrict: 'E',
require: ["^form","?ngModel"],
replace: false,
templateUrl: "validation-input-template.htm",
scope: {
label: '#',
id: '#',
type: '#',
bindModel: '=',
name: '#',
validationModel: '=',
directives: '&'
},
link: function(scope, elem, attrs, controllers) {
var inputElem = elem.find('input'),
newInputElem = null,
formController = controllers[0];
scope.type = scope.type || 'text';
scope.$watch('directives()', function(newDirectives, oldDirectives) {
newInputElem = inputElem.clone();
for (var directive in newDirectives) {
newInputElem.attr(directive, newDirectives[directive] ? newDirectives[directive] : directive);
}
// formController.$removeControl(scope.name);
elem.find('input').replaceWith(newInputElem);
$compile(elem.contents())(scope);
}, true);
}
};
}]);
Directive Template (removed irrelevant parts):
<script type='text/ng-template' id='validation-input-template.htm'>
<div class="row">
<div class="form-group has-feedback" data-ng-class="{'has-success':focus && validationModel.$valid ,'has-error':validationModel.$dirty && validationModel.$invalid , 'has-warning':validationModel.$pending}">
<label class="control-label" for='{{id}}-input'>{{label}}</label>
<input type="{{type}}" class="form-control" id="{{id}}-input" data-ng-model="bindModel" ng-focus="focus=true;" name="{{name}}">
</div>
</script>
Usage:
<form name='aForm'>
<validatable-input label='User Name' bind-model='main.userName' name='uname' validation-model='aForm.uname' id='some-id-2' directives='main.injectedDirectives'> </validatable-input>
<validatable-input label='ID Number' type='text' bind-model='main.idNumber' name='idNum' validation-model='aForm.idNum' id='some-id-3' directives='{"required":"", "digit-only":"","prime":""}'> </validatable-input>
</form>
Main Controller that handles changing of the list of directives that need to be applied to input:
.controller("MainController", ['$scope', function($scope) {
this.LIST = {
'required': '',
'ng-minlength': '3',
'ng-maxlength': '8',
'digit-only': '',
'even':'',
'ng-model-options':'{ debounce: 2000 }'
};
this.injectedDirectives = {};
this.injectDirective = function() {
for (var key in this.LIST) {
if (this.LIST.hasOwnProperty(key) && !this.injectedDirectives.hasOwnProperty(key)) {
this.injectedDirectives[key] = this.LIST[key];
return;
}
}
this.injectedDirectives= {};
};
}]);
I have a form that is wired into angular, using it for validation. I am able to display error messages using ng-show directives like so:
<span ng-show="t3.f.needsAttention(f.fieldName)" ng-cloak>
<span ng-show="f.fieldName.$error.required && !f.fieldName.$viewValue">
This field is required.
</span>
</span>
.. where f is the form, and t3 comes from a custom directive on the form which detects whether a submission was attempted, and contains functions for checking the validity of fields.
What I am trying to accomplish is to display validation message(s) inside a popover instead. Either bootstrap's native popover, or the popover from UI Bootstrap, I have both loaded. I may also consider AngularStrap if it is easier to do it using that lib.
What I'm struggling with right now is the nature of popovers in general -- they autodisplay based on user events like click, mouseenter, blur, etc. What I want to do is show & hide the popover(s) based on the same functions in the ng-show attributes above. So that when the expression returns false hide it, and when it returns true, show it.
I know bootstrap has the .popover('show') for this, but I'm not supposed to tell angular anything about the dom, so I'm not sure how I would get access to $(element).popover() if doing this in a custom form controller function. Am I missing something?
Update
The solution mentioned in the duplicate vote still only shows the popover on mouseenter. I want to force it to display, as if doing $('#popover_id').popover('show').
You can also build your own extended triggers. This will apply to both Tooltip and Popover.
First extend the Tooltip triggers as follows:
// define additional triggers on Tooltip and Popover
app.config(['$tooltipProvider', function($tooltipProvider){
$tooltipProvider.setTriggers({
'show': 'hide'
});
}]);
Then define the trigger on the HTML tag like this:
<div id="RegisterHelp" popover-trigger="show" popover-placement="left" popover="{{ 'Login or register here'}}">
And now you can call hide and show from JavaScript, this is a show in 3 seconds.
$("#RegisterHelp").trigger('show');
//Close the info again
$timeout(function () {
$("#RegisterHelp").trigger('hide');
}, 3000);
As it turns out, it's not very difficult to decorate either the ui-bootstrap tooltip or the popover with a custom directive. This is written in typescript, but the javascript parts of it should be obvious. This single piece of code works to decorate either a tooltip or a popover:
'use strict';
module App.Directives.TooltipToggle {
export interface DirectiveSettings {
directiveName: string;
directive: any[];
directiveConfig?: any[];
}
export function directiveSettings(tooltipOrPopover = 'tooltip'): DirectiveSettings {
var directiveName = tooltipOrPopover;
// events to handle show & hide of the tooltip or popover
var showEvent = 'show-' + directiveName;
var hideEvent = 'hide-' + directiveName;
// set up custom triggers
var directiveConfig = ['$tooltipProvider', ($tooltipProvider: ng.ui.bootstrap.ITooltipProvider): void => {
var trigger = {};
trigger[showEvent] = hideEvent;
$tooltipProvider.setTriggers(trigger);
}];
var directiveFactory = (): any[] => {
return ['$timeout', ($timeout: ng.ITimeoutService): ng.IDirective => {
var d: ng.IDirective = {
name: directiveName,
restrict: 'A',
link: (scope: ng.IScope, element: JQuery, attr: ng.IAttributes) => {
if (angular.isUndefined(attr[directiveName + 'Toggle'])) return;
// set the trigger to the custom show trigger
attr[directiveName + 'Trigger'] = showEvent;
// redraw the popover when responsive UI moves its source
var redrawPromise: ng.IPromise<void>;
$(window).on('resize', (): void => {
if (redrawPromise) $timeout.cancel(redrawPromise);
redrawPromise = $timeout((): void => {
if (!scope['tt_isOpen']) return;
element.triggerHandler(hideEvent);
element.triggerHandler(showEvent);
}, 100);
});
scope.$watch(attr[directiveName + 'Toggle'], (value: boolean): void => {
if (value && !scope['tt_isOpen']) {
// tooltip provider will call scope.$apply, so need to get out of this digest cycle first
$timeout((): void => {
element.triggerHandler(showEvent);
});
}
else if (!value && scope['tt_isOpen']) {
$timeout((): void => {
element.triggerHandler(hideEvent);
});
}
});
}
};
return d;
}];
};
var directive = directiveFactory();
var directiveSettings: DirectiveSettings = {
directiveName: directiveName,
directive: directive,
directiveConfig: directiveConfig,
};
return directiveSettings;
}
}
With this single piece of code, you can set up programmatic hide and show of either a tooltip or popover like so:
var tooltipToggle = App.Directives.TooltipToggle.directiveSettings();
var popoverToggle = App.Directives.TooltipToggle.directiveSettings('popover');
var myModule = angular.module('my-mod', ['ui.bootstrap.popover', 'ui.bootstrap.tpls'])
.directive(tooltipToggle.directiveName, tooltipToggle.directive)
.config(tooltipToggle.directiveConfig)
.directive(popoverToggle.directiveName, popoverToggle.directive)
.config(popoverToggle.directiveConfig);
Usage:
<span tooltip="This field is required."
tooltip-toggle="formName.fieldName.$error.required"
tooltip-animation="false" tooltip-placement="right"></span>
or
<span popover="This field is required."
popover-toggle="formName.fieldName.$error.required"
popover-animation="false" popover-placement="right"></span>
So we are reusing everything else that comes with the ui-bootstrap tooltip or popover, and only implementing the -toggle attribute. The decorative directive watches that attribute, and fires custom events to show or hide, which are then handled by the ui-bootstrap tooltip provider.
Update:
Since this answer seems to be helping others, here is the code written as javascript (the above typescript more or less compiles to this javascript):
'use strict';
function directiveSettings(tooltipOrPopover) {
if (typeof tooltipOrPopover === "undefined") {
tooltipOrPopover = 'tooltip';
}
var directiveName = tooltipOrPopover;
// events to handle show & hide of the tooltip or popover
var showEvent = 'show-' + directiveName;
var hideEvent = 'hide-' + directiveName;
// set up custom triggers
var directiveConfig = ['$tooltipProvider', function ($tooltipProvider) {
var trigger = {};
trigger[showEvent] = hideEvent;
$tooltipProvider.setTriggers(trigger);
}];
var directiveFactory = function() {
return ['$timeout', function($timeout) {
var d = {
name: directiveName,
restrict: 'A',
link: function(scope, element, attr) {
if (angular.isUndefined(attr[directiveName + 'Toggle']))
return;
// set the trigger to the custom show trigger
attr[directiveName + 'Trigger'] = showEvent;
// redraw the popover when responsive UI moves its source
var redrawPromise;
$(window).on('resize', function() {
if (redrawPromise) $timeout.cancel(redrawPromise);
redrawPromise = $timeout(function() {
if (!scope['tt_isOpen']) return;
element.triggerHandler(hideEvent);
element.triggerHandler(showEvent);
}, 100);
});
scope.$watch(attr[directiveName + 'Toggle'], function(value) {
if (value && !scope['tt_isOpen']) {
// tooltip provider will call scope.$apply, so need to get out of this digest cycle first
$timeout(function() {
element.triggerHandler(showEvent);
});
}
else if (!value && scope['tt_isOpen']) {
$timeout(function() {
element.triggerHandler(hideEvent);
});
}
});
}
};
return d;
}];
};
var directive = directiveFactory();
var directiveSettings = {
directiveName: directiveName,
directive: directive,
directiveConfig: directiveConfig,
};
return directiveSettings;
}
For ui.bootstrap 0.13.4 and newer:
A new parameter (popover-is-open) was introduced to control popovers in the official ui.bootstrap repo. This is how you use it in the latest version:
<a uib-popover="Hello world!" popover-is-open="isOpen" ng-click="isOpen = !isOpen">
Click me to show the popover!
</a>
For ui.bootstrap 0.13.3 and older:
I just published a small directive that adds more control over popovers on GitHub: https://github.com/Elijen/angular-popover-toggle
You can use a scope variable to show/hide the popover using popover-toggle="variable" directive like this:
<span popover="Hello world!" popover-toggle="isOpen">
Popover here
</span>
Here is a demo Plunkr: http://plnkr.co/edit/QeQqqEJAu1dCuDtSvomD?p=preview
My approach:
Track the state of the popover in the model
Change this state per element using the appropriate directives.
The idea being to leave the DOM manipulation to the directives.
I have put together a fiddle that I hope gives a better explain, but you'll find much more sophisticated solutions in UI Bootstrap which you mentioned.
jsfiddle
Markup:
<div ng-repeat="element in elements" class="element">
<!-- Only want to show a popup if the element has an error and is being hovered -->
<div class="popover" ng-show="element.hovered && element.error" ng-style>Popover</div>
<div class="popoverable" ng-mouseEnter="popoverShow(element)" ng-mouseLeave="popoverHide(element)">
{{ element.name }}
</div>
</div>
JS:
function DemoCtrl($scope)
{
$scope.elements = [
{name: 'Element1 (Error)', error: true, hovered: false},
{name: 'Element2 (no error)', error: false, hovered: false},
{name: 'Element3 (Error)', error: true, hovered: false},
{name: 'Element4 (no error)', error: false, hovered: false},
{name: 'Element5 (Error)', error: true, hovered: false},
];
$scope.popoverShow = function(element)
{
element.hovered = true;
}
$scope.popoverHide = function(element)
{
element.hovered = false
}
}
For others coming here, as of the 0.13.4 release, we have added the ability to programmatically open and close popovers via the *-is-open attribute on both tooltips and popovers in the Angular UI Bootstrap library. Thus, there is no longer any reason to have to roll your own code/solution.
From Michael Stramel's answer, but with a full angularJS solution:
// define additional triggers on Tooltip and Popover
app.config(['$tooltipProvider', function($tooltipProvider){
$tooltipProvider.setTriggers({
'show': 'hide'
});
}])
Now add this directive:
app.directive('ntTriggerIf', ['$timeout',
function ($timeout) {
/*
Intended use:
<div nt-trigger-if={ 'triggerName':{{someCodition === SomeValue}},'anotherTriggerName':{{someOtherCodition === someOtherValue}} } ></div>
*/
return {
restrict: 'A',
link: function (scope, element, attrs) {
attrs.$observe('ntTriggerIf', function (val) {
try {
var ob_options = JSON.parse(attrs.ntTriggerIf.split("'").join('"') || "");
}
catch (e) {
return
}
$timeout(function () {
for (var st_name in ob_options) {
var condition = ob_options[st_name];
if (condition) {
element.trigger(st_name);
}
}
})
})
}
}
}])
Then in your markup:
<span tooltip-trigger="show" tooltip="Login or register here" nt-trigger-if="{'show':{{ (errorConidtion) }}, 'hide':{{ !(errorConidtion) }} }"></span>
Using angularjs, if I bind the placeholder of an input to its model, the change event is fired when the document loads in IE. This does not appear to be correct and I'm not seeing this behavior in other browsers.
JS Fiddle
Html:
<div ng-app="angularjs-starter" data-ng-controller="MainCtrl">
<div data-ui-view="viewMain">
<input
placeholder="{{theValue}}"
data-ng-model="theValue"
data-ng-change="valueChanged(theValue)" />
</div>
Javascript:
var app = angular.module('angularjs-starter', []);
app.controller('MainCtrl', function($scope) {
$scope.valueChanged = function(theValue) {
alert("Value Change Called On Load in IE.");
};
});
It's possible to use the built-in ng-attr-placeholder directive as well.
ng-attr-placeholder="{{theValue}}"
I know this is old but just in case anyone else runs in to this I created a small directive that goes around putting a dynamic value in the placeholder and instead have it assign when it changes:
.directive('dynamicPlaceholder',
function() {
return {
restrict: 'A',
link: function ($scope, element, attrs) {
attrs.$observe('dynamicPlaceholder', function(value) {
element.attr('placeholder', value);
});
}
};
});
Then to use this all you need to do is use dynamic-placeholder instead of placeholder:
<input ng-model='someValue' dynamic-placeholder='{{someDynamicPlaceholder}}' />
Not sure what is causing the problem in IE though
I want to bind a directive to a variable within a controller, but cannot work out how to do it from the Angular.js docs (nor searching the web, looking at the egghead videos).
I have the following html:
<body ng-app="MyApp">
<div ng-controller="triCtrl">
<div jqslider pleaseBindTo="firstValue"></div>
<br>
<br>
<br>
<br>
<div jqslider pleaseBindTo="secondValue"></div>
<p>{{firstValue.v}}</p>
<p>{{secondValue.v}}</p>
</div>
</body>
And the following JS:
function triCtrl($scope) {
$scope.firstValue = {"min":0, "max":100, "v":50};
$scope.secondValue = {"min":0, "max":1000, "v":450};
}
var myAppModule = angular.module('MyApp', []);
myAppModule.directive('jqslider', function() {
return {
link:function(scope, element, attrs) {
element.slider({
range: false,
min: scope.min,
max: scope.max,
value: scope.v,
slide: function( event, ui ) {
scope.v = ui.value;
scope.$apply();
}
});
}
};
});
I have tried several ways using scope:{ } with &, #, = etc, but I can't get it to work. Any ideas? I understand the pleaseBindTo attribute has to be captured somewhere, but I don't know where or how.
Some Observations:
1. Your directive is an attribute
2. You are passing values to attributes themselves.
Maybe you should change your directive to an element instead. Rewrite the code as follows:
<jqslider pleaseBindTo="firstValue"></jqslider>
Remove the div's and use the directive directly as an element. Next, in the definition of your directive, write the following:
myAppModule.directive('jqslider', function() {
return {
scope: {
pleaseBindTo: "=pleaseBindTo"
}
link:function(scope, element, attrs) {
element.slider({
range: false,
min: scope.pleaseBindTo.min,
max: scope.pleaseBindTo.max,
value: scope.pleaseBindTo.v,
slide: function( event, ui ) {
scope.pleaseBindTo.v = ui.value;
scope.$apply();
}
});
}
};
});
If you wish to still keep the directive as an attribute, you could try to access the value of pleaseBindTo inside the link function with the statement attrs.pleaseBindTo - I am not sure of this, need to check it out.