I'm having trouble understanding Angular directives. I'd like to use a simple attribute to expand into more complicated html, but with some parts of this template being replaceable via parameters.
Given this code:
<form role="form" name="databaseForm">
<input type="text" name="connectionName"
ng-focus="databaseForm.connectionName.$focused = true;databaseForm.connectionName.$blurred = false;"
ng-blur="databaseForm.connectionName.$blurred = true;databaseForm.connectionName.$focused = false;"
>
</form>
I'd like to write it using a more terse directive (such as "blurred-focused"):
<form role="form" name="databaseForm">
<input type="text" name="connectionName"
blurred-focused="'databaseForm.connectionName'"
>
</form>
So that means I can very easily re-use it for other inputs:
<form role="form" name="databaseForm">
<input type="text" name="username"
blurred-focused="'databaseForm.username'"
>
</form>
The expected result from this is that the inputs with this directive will have the $blurred and $focused properties automatically applied to it, based on the user interaction with the input.
Thank you.
Update:
I ended up using MMHunter's version where the scope is non-isolated. I added some logic to automatically find the form and field object, so that I didn't need to specify it all.
My directive:
(function () {
"use strict";
angular
.module("app.shared.widgets")
.directive("blurredFocused", blurredFocused);
blurredFocused.$inject = ["_"];
/* #ngInject */
function blurredFocused(_) {
return {
restrict: "A",
priority: -1,
link: function(scope, element, attributes) {
element.on("blur", function () {
var formFieldName = element[0].form.name + "." + element[0].name;
var target = _.get(scope, formFieldName);
scope.$apply(function() {
target.$blurred = true;
target.$focused = false;
});
});
element.on("focus", function () {
var formFieldName = element[0].form.name + "." + element[0].name;
var target = _.get(scope, formFieldName);
scope.$apply(function() {
target.$blurred = false;
target.$focused = true;
});
});
}
};
}
})();
And an example of its usage:
<form role="form" name="databaseForm">
<input type="text" name="connectionName" blurred-focused>
</form>
You need is not difficult to achieve with angular directive. But things can be different based on whether isolated scope is used.
With isolated scope, the following code is straightForward. Binding the value to an isolated scope in the 'blurred-focused' directive and listen to the events.
//with isolated scope
app.directive("blurredFocused", [function () {
return {
restrict:"A",
priority:-1,
scope:{
blurredFocused:"="
},
link:function(scope,ele,attrs){
ele.on("blur",function(){
scope.$apply(function(){
scope.blurredFocused.$blurred = true;
scope.blurredFocused.$focused = false;
})
})
ele.on("focus",function(){
scope.$apply(function(){
scope.blurredFocused.$blurred = false;
scope.blurredFocused.$focused = true;
})
})
}
}
}]);
But without isolated scope, things could be a little bit tricky. we need to find the scope value manually by the attributes value.
//without isolated scope
app.directive("blurredFocused", [function () {
return {
restrict:"A",
priority:-1,
link:function(scope,ele,attrs){
ele.on("blur",function(){
var targetField = scope[attrs.blurredFocused];
scope.$apply(function(){
targetField.$blurred = true;
targetField.$focused = false;
})
})
ele.on("focus",function(){
var targetField = scope[attrs.blurredFocused];
scope.$apply(function(){
targetField.$blurred = false;
targetField.$focused = true;
})
})
}
}
}]);
Here is the plunker
I would recommend you use the one without isolated scope. Attribute directives are always used together so it may not be a good idea to have isolated scopes.
Related
I'm trying to add an errors to my floating placeholder labels when certain conditions are met in my controller
However, I'm not sure the best way to go about this and my current implementing doesn't seem to be detecting the attribute change in the directive (custom-error stays set to "test").
Here's what I've got right now:
HTML:
<input type="password" float-placeholder
custom-error="test" placeholder="Confirm password"
required name="passwordSecond" id="passwordSecond"
ng-model="vs.PasswordSecond" />
Directive:
angular.module('myApp').directive('floatPlaceholder', function ($window) {
return {
restrict: 'A',
scope: {
customError: '#'
},
link: function (scope, element, attrs) {
element.after("<label class='floating-placeholder'>" + attrs.placeholder + "</label>");
var label = angular.element(ele.parent()[0].getElementsByClassName('floating-placeholder'));
element.on('blur', function() {
if (ele.val().length > 0) {
if (scope.customError) {
label.text(attrs.placeholder + ' - ' + scope.customError);
}
}
}
}
};
});
Controller:
angular.module('myApp').controller('SignupController', function factory() {
_this.confirmPassword = () => {
if(_this.PasswordFirst !== _this.PasswordSecond){
angular.element(signupForm.passwordSecond).attr('custom-error', _this.Error);
}
}
});
I'm using Angular 1.6
Validator Directive which Matches Passwords
To have a form match password inputs, create a custom directive that hooks into the ngModelController API ($validators):
app.directive("matchWith", function() {
return {
require: "ngModel",
link: postLink
};
function postLink(scope,elem,attrs,ngModel) {
ngModel.$validators.match = function(modelValue, viewValue) {
if (ngModel.$isEmpty(modelValue)) {
// consider empty models to be valid
return true;
}
var matchValue = scope.$eval(attrs.matchWith);
if (matchValue == modelValue) {
// it is valid
return true;
}
// it is invalid
return false;
};
}
})
For more information, see AngularJS Developer Guide - Forms - Modifying Built-in Validators
The DEMO
angular.module("app",[])
.directive("matchWith", function() {
return {
require: "ngModel",
link: postLink
};
function postLink(scope,elem,attrs,ngModel) {
ngModel.$validators.match = function(modelValue, viewValue) {
if (ngModel.$isEmpty(modelValue)) {
// consider empty models to be valid
return true;
}
var matchValue = scope.$eval(attrs.matchWith);
if (matchValue == modelValue) {
// it is valid
return true;
}
// it is invalid
return false;
};
}
})
<script src="//unpkg.com/angular/angular.js"></script>
<body ng-app="app">
<form name="form1">
<input type="password" name="password1" required
placeholder="Enter password"
ng-model="vm.password1" />
<br>
<input type="password" name="password2" required
placeholder="Confirm password"
ng-model="vm.password2"
match-with="vm.password1"
ng-model-options="{updateOn: 'blur'}" />
<br>
<p ng-show="form1.password2.$error.match">
Passwords don't match
</p>
<input type="submit" value="submit" />
</form>
</body>
Had a look at your code. Have you defined the scope variables in the SignUpController
_this.PasswordFirst and _this.PasswordSecond
Also this line in your controller
angular.element(signupForm.passwordSecond).attr('custom-error', _this.Error);
good suggestion would be to implement this in the directive as attributes can be accessed correctly in the directive
(I'm basing this on you saying 'custom-error stays set to "test"')
custom-error is looking for a variable of "test", not a string value of "test". Have you tried setting a variable test in your controller and updating that?
I'm a newbie in angular so please bear with me. I have a character counter and word counter in my textarea. My problem is that everytime I press the space, it is also being counted by getWordCounter function. How can I fix this? Thank you in advance.
HTML:
<textarea id="notesContent" type="text" class="form-control" rows="10" ng-model="notesNode.text" ng-trim="false" maxlength="5000"></textarea>
<span class="wordCount">{{getWordCounter()}}</span>
<span style="float:right">{{getCharCounter()}} / 5000</span>
JS:
$scope.getCharCounter = function() {
return 5000 - notesNode.text.length;
}
$scope.getWordCounter = function() {
return $.trim(notesNode.text.split(' ').length);
}
It seems like you need to call 'trim' before calling split, like this:
$scope.getWordCounter = function() {
return notesNode.text.trim().split(' ').length;
}
If you want to support multiple spaces between words, use a regular expression instead:
$scope.getWordCounter = function() {
return notesNode.text.trim().split(/\s+/).length;
}
Filter implementation
You can also implement wordCounter as a filter, to make it reusable among different views:
myApp.filter('wordCounter', function () {
return function (value) {
if (value && (typeof value === 'string')) {
return value.trim().split(/\s+/).length;
} else {
return 0;
}
};
});
Then, in the view, use it like this:
<span class="wordCount">{{notesNode.text|wordCounter}</span>
See Example on JSFiddle
This is a more advanced answer for your problem, since it can be reusable as a directive:
var App = angular.module('app', []);
App.controller('Main', ['$scope', function($scope){
var notesNode = {
text: '',
counter: 0
};
this.notesNode = notesNode;
}]);
App.directive('counter', [function(){
return {
restrict: 'A',
scope: {
counter: '='
},
require: '?ngModel',
link: function(scope, el, attr, model) {
if (!model) { return; }
model.$viewChangeListeners.push(function(){
var count = model.$viewValue.split(/\b/g).filter(function(i){
return !/^\s+$/.test(i);
}).length;
scope.counter = count;
});
}
};
}]);
And the HTML
<body ng-app="app">
<div ng-controller="Main as main"></div>
<input type="text" ng-model="main.notesNode.text" class="county" counter="main.notesNode.counter">
<span ng-bind="main.notesNode.counter"></span>
</body>
See it in here http://plnkr.co/edit/9blLIiaMg0V3nbOG7SKo?p=preview
It creates a two way data binding to where the count should go, and update it automatically for you. No need for extra shovelling inside your scope and controllers code, plus you can reuse it in any other input.
I have a directive containing a text field, and I want to test to make sure that the text entered into the field makes it to the model.
The directive:
define(function(require) {
'use strict';
var module = require('reporting/js/directives/app.directives');
var template = require('text!reporting/templates/text.box.tpl');
module.directive('textField', function () {
return {
restrict: 'A',
replace: true,
template:template,
scope: {
textField : "=",
textBoxResponses : "="
},
link: function(scope) {
scope.debug = function () {
scope;
// debugger;
};
}
};
});
return module;
});
The markup:
<div ng-form name="textBox">
<!-- <button ng-click="debug()">debug the text box button</button> -->
<h1>Text Box!</h1>
{{textField.label}} <input type="text" name="textBox" ng-model="textBoxResponses[textField.fieldName]">{{name}}
</div>
The test code:
/* global inject, expect, angular */
define(function(require){
'use strict';
require('angular');
require('angularMock');
require('reporting/js/directives/app.directives');
require('reporting/js/directives/text.box.directive');
describe("builder experimenter", function() {
var directive, scope;
beforeEach(module('app.directives'));
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope;
scope.textBoxResponses = {};
scope.textBoxField = {
fieldName : "textBox1"
};
directive = angular.element('<div text-field="textBoxField" text-box-responses="textBoxResponses"></div>');
$compile(directive)(scope);
scope.$digest();
}));
it('should put the text box value on the model', inject(function() {
directive.find(":text").val("something");
expect(scope.textBoxResponses.textBox1).toBe("something");
}));
});
});
So, what I'm trying to do in the last it block is to simulate typing in the text field, and then check to make sure that the new value of the text field makes it to the model. The issue is that the model is never updated with the new value.
The issue is ng-model is never informed that anything is in the textfield. ng-model is listening for the input event. All you have to do to fix your code is:
var text = directive.find(":text");
text.val("something");
text.trigger('input');
expect(scope.textBoxResponses.textBox1).toBe("something");
When the ng-model gets the event input, then check your scope and everything will be what you expect.
I got this done by using the sniffer service.
Your test will look like this:
var sniffer;
beforeEach(inject(function($compile, $rootScope, $sniffer) {
scope = $rootScope;
sniffer = $sniffer;
scope.textBoxResponses = {};
scope.textBoxField = {
fieldName : "textBox1"
};
directive = angular.element('<div text-field="textBoxField" text-box-responses="textBoxResponses"></div>');
$compile(directive)(scope);
scope.$digest();
}));
it('should put the text box value on the model', inject(function() {
directive.find(":text").val("something");
directive.find(":text").trigger(sniffer.hasEvent('input') ? 'input' : 'change');
expect(directive.isolateScope().textBoxResponses.textBox1).toBe("something");
}));
I found this pattern here: angular-ui-bootstrap typeahead test
The trigger basically makes the view value go into the model.
Hope this helps
I am working on an angular form builder which generate a json.
Everything works fine except one thing.
You can find an example here : http://jsfiddle.net/dJRS5/8/
HTML :
<div ng-app='app'>
<div class='formBuilderWrapper' id='builderDiv' ng-controller="FormBuilderCtrl" >
<div class='configArea' data-ng-controller="elementDrag">
<h2>drag/drop</h2>
<form name="form" novalidate class='editBloc'>
<div data-ng-repeat="field in fields" class='inputEdit'>
<data-ng-switch on="field.type">
<div class='labelOrder' ng-class='{column : !$last}' drag="$index" dragStyle="columnDrag" drop="$index" dropStyle="columnDrop">{{field.type}}
</div>
<label for="{{field.name}}" data-ng-bind-html-unsafe="field.caption"></label>
<input data-ng-switch-when="Text" type="text" placeholder="{{field.placeholder}}" data-ng-model="field.value" />
<p data-ng-switch-when="Text/paragraph" data-ng-model="field.value" data-ng-bind-html-unsafe="field.paragraph"></p>
<span data-ng-switch-when="Yes/no question">
<p data-ng-bind-html-unsafe="field.yesNoQuestion"></p>
<input type='radio' name="yesNoQuestion" id="yesNoQuestion_yes" value="yesNoQuestion_yes" />
<label for="yesNoQuestion_yes">Oui</label>
<input type='radio' name="yesNoQuestion" id="yesNoQuestion_no" value="yesNoQuestion_no"/>
<label for="yesNoQuestion_no">Non</label>
</span>
<p data-ng-switch-when="Submit button" class='submit' data-ng-model="field.value">
<input value="{{field.name}}" type="submit">
</p>
</data-ng-switch>
</div>
</form>
</div>
<div id='previewArea' data-ng-controller="formWriterCtrl">
<h2>preview</h2>
<div data-ng-repeat="item in fields" content="item" class='templating-html'></div>
</div>
</div>
</div>
The JS :
var app = angular.module('app', []);
app.controller('FormBuilderCtrl', ['$scope', function ($scope){
$scope.fields = [{"type":"Text/paragraph","paragraph":"hello1"},{"type":"Yes/no question","yesNoQuestion":"following items must be hidden","yes":"yes","no":"no"},{"type":"Text/paragraph","paragraph":"hello2"},{"type":"Submit button","name":"last item"}] ;
}]);
app.controller('elementDrag', ["$scope", "$rootScope", function($scope, $rootScope, $compile) {
$rootScope.$on('dropEvent', function(evt, dragged, dropped) {
if($scope.fields[dropped].type == 'submitButton' || $scope.fields[dragged].type == 'submitButton'){
return;
}
var tempElement = $scope.fields[dragged];
$scope.fields[dragged] = $scope.fields[dropped];
$scope.fields[dropped] = tempElement;
$scope.$apply();
});
}]);
app.directive("drag", ["$rootScope", function($rootScope) {
function dragStart(evt, element, dragStyle) {
if(element.hasClass('column')){
element.addClass(dragStyle);
evt.dataTransfer.setData("id", evt.target.id);
evt.dataTransfer.effectAllowed = 'move';
}
};
function dragEnd(evt, element, dragStyle) {
element.removeClass(dragStyle);
};
return {
restrict: 'A',
link: function(scope, element, attrs) {
if(scope.$last === false){
attrs.$set('draggable', 'true');
scope.dragStyle = attrs["dragstyle"];
element.bind('dragstart', function(evt) {
$rootScope.draggedElement = scope[attrs["drag"]];
dragStart(evt, element, scope.dragStyle);
});
element.bind('dragend', function(evt) {
dragEnd(evt, element, scope.dragStyle);
});
}
}
}
}]);
app.directive("drop", ['$rootScope', function($rootScope) {
function dragEnter(evt, element, dropStyle) {
element.addClass(dropStyle);
evt.preventDefault();
};
function dragLeave(evt, element, dropStyle) {
element.removeClass(dropStyle);
};
function dragOver(evt) {
evt.preventDefault();
};
function drop(evt, element, dropStyle) {
evt.preventDefault();
element.removeClass(dropStyle);
};
return {
restrict: 'A',
link: function(scope, element, attrs) {
if(scope.$last === false){
scope.dropStyle = attrs["dropstyle"];
element.bind('dragenter', function(evt) {
dragEnter(evt, element, scope.dropStyle);
});
element.bind('dragleave', function(evt) {
dragLeave(evt, element, scope.dropStyle);
});
element.bind('dragover', dragOver);
element.bind('drop', function(evt) {
drop(evt, element, scope.dropStyle);
var dropData = scope[attrs["drop"]];
$rootScope.$broadcast('dropEvent', $rootScope.draggedElement, dropData);
});
}
}
}
}]);
app.controller('formWriterCtrl', ['$scope', function ($scope){
}]);
app.directive('templatingHtml', function ($compile) {
var previousElement;
var previousIndex;
var i=0;
var inputs = {};
var paragraphTemplate = '<p data-ng-bind-html-unsafe="content.paragraph"></p>';
var noYesQuestionTemplate = '<p data-ng-bind-html-unsafe="content.yesNoQuestion"></p><input id="a__index__yes" type="radio" name="a__index__"><label for="a__index__yes" />{{content.yes}}</label><input id="a__index__no" class="no" type="radio" name="a__index__" /><label for="a__index__no">{{content.no}}</label>';
var submitTemplate = '<p class="submit"><input value="{{content.name}}" type="submit" /></p>';
var getTemplate = function(contentType, contentReplace, contentRequired) {
var template = '';
switch(contentType) {
case 'Text/paragraph':
template = paragraphTemplate;
break;
case 'Yes/no question':
template = noYesQuestionTemplate;
break;
case 'Submit button':
template = submitTemplate;
break;
}
template = template.replace(/__index__/g, i);
return template;
}
var linker = function(scope, element, attrs) {
i++;
elementTemplate = getTemplate(scope.content.type);
element.html(elementTemplate);
if(previousElement == 'Yes/no question'){
element.children().addClass('hidden');
element.children().addClass('noYes'+previousIndex);
}
if(scope.content.type == 'Yes/no question'){
previousElement = scope.content.type;
previousIndex = i;
}
$compile(element.contents())(scope);
}
return {
restrict: "C",
link: linker,
scope:{
content:'='
}
};
});
On the example there are 2 areas :
- the first one does a ngRepeat on Json and allow to reorder items with drag and drop
- the second area also does a ngRepeat, it is a preview templated by a directive using compile function. Some elements are hidden if they are after what I called "Yes/no question"
Here is an example of Json generated by the form builder :
$scope.fields =
[{"type":"Text/paragraph","paragraph":"hello1"},{"type":"Yes/no question","yesNoQuestion":"following items must be hidden","yes":"yes","no":"no"},
{"type":"Text/paragraph","paragraph":"hello2"},{"type":"Submit button","name":"last item"}] ;
When the page load everything is ok, Hello1 is visible and Hello2 is hidden.
But when I drop Hello1 after "Yes/no question", dom elements are reorganised but Hello1 is not hidden.
I think it comes from $compile but I don't know how to resolve it.
Could you help me with this please?
Thank you
I only see you setting the 'hidden' class on the element based on that rule (after a yes/no) in the link function. That's only called once for the DOM element - when it's first created. Updating the data model doesn't re-create the element, it updates it in place. You would need a mechanism that does re-create it if you wanted to do it this way.
I see three ways you can do this:
In your linker function, listen for the same dropEvent that you listen for above. This is more efficient than you'd think (it's very fast) and you can re-evaluate whether to apply this hidden class or not.
Use something like ngIf or literally re-creating it in your collection to force the element to be recreated entirely. This is not as efficient, but sometimes is still desirable for various reasons.
If your use case is actually this simple (if this wasn't a redux of something more complicated you're trying to do) you could use CSS to do something like this. A simple rule like
.yes-no-question + .text-paragraph { display: none; }
using a sibling target could handle this directly without as much work. This is much more limited in what it can do, obviously, but it's the most efficient option if it covers what you need.
I am implementing a simple directive that represents a form field with all its extras like label, error field, regex all in a single line.
The directive is as follow:
<div ng-controller="parentController">
{{username}}
<!-- the directive -- >
<form-field label="Username:" regex="someRegex" constrainsViolationMessage="someValidationMessage" model="username" place-holder="some input value">
</form-field>
</div>
Now, I want to test the data binding between the directive scope and the parent scope.
The test is:
it("should bind input field to the scope variable provided by parent scope ! ", function () {
var formInput = ele.find('.form-input');
formInput.val("some input");
expect(ele.find('p').text()).toEqual('some input');
});
This problem is that I don't know why test don't pass, even the directive works correctly. Here is a fiddle of the directive.
And here is the whole test and test set up.
var formsModule = angular.module('forms', []);
formsModule.controller('parentController', function ($scope) {
});
formsModule.directive('formField', function () {
var label;
var constrainsViolationMessage;
var placeHolder;
var model;
return {
restrict:'E',
transclude:true,
replace:false,
scope:{
model:'='
},
link:function (scope, element, attr) {
console.log("link function is executed .... ");
scope.$watch('formInput', function (newValue, oldValue) {
console.log("watch function is executed .... !")
scope.model = newValue;
});
scope.label = attr.label;
},
template:'<div class="control-group ">' +
'<div class="form-label control-label">{{label}}</div> ' +
'<div class="controls controls-row"> ' +
'<input type="text" size="15" class="form-input input-medium" ng-model="formInput" placeholder="{{placeHolder}}">' +
'<label class="error" ng-show={{hasViolationConstrain}}>{{constrainsViolationMessage}}</label>' +
'</div>'
}
});
beforeEach(module('forms'));
var ele;
var linkingFunction;
var elementBody;
var scope;
var text = "";
var placeHolder = "filed place holder";
var label = "someLabel";
var regex = "^[a-z]{5}$";
beforeEach(inject(function ($compile, $rootScope) {
scope = $rootScope;
elementBody = angular.element('<div ng-controller="parentController">' +
'<p>{{username}}</p>' +
'<form-field label="Username:" regex="someRegex" constrainsViolationMessage="someValidationMessage" model="username" place-holder="some input value"> </form-field>');
ele = $compile(elementBody)(scope);
scope.$digest();
}
));
afterEach(function () {
scope.$destroy();
});
iit("should bind input field to the scope variable provided by parent scope ! ", function () {
var formInput = ele.find('.form-input');
formInput.val("some input");
expect(ele.find('p').text()).toEqual('some input');
});
As you can see, I want to assert that form input is reflected in the scope variable set in the 'model' attribute provided by the parent scope.
Am I missing something here ?
Thanks for helping me ... !
You're missing the scope.$apply() call after you set the input value, so the change is never getting digested. In the regular application lifecycle this would happen automatically, but you have to manually trigger this in your tests
Take a look at https://github.com/angular/angular.js/blob/master/test/ng/directive/formSpec.js for a ton of examples.
Use $scope.$digest() after adding condition to execute watch. It will fire watch.