Change HTML input field to label by defining custom angular directive - angularjs

I have lots of input, textarea and select on some pages (angular templates).
I want to redefine "input" directive such that It will take a value like ViewMode = true from either localStorage and convert all inputs as label. If I change the ViewMode then on page refresh input should behave properly.
But I do not want to edit any input tag on any angular template.
Means I want to override input, textarea and select as my own angular directive.
I am not able to start. Where from should I start? (I have experience of custom directive with new name, but not with any exciting HTML tag name)
Note: I do not want to use readonly (with proper style) since it requires editing all input tag. Not only that I have custom directives with isolated scope, so I need to pass the ViewMode value to all custom directives. More over if user press CTRL+A content readonly field is not being selected.
I am looking for a solution kind of as follows
ViewButtonClickEvent () {
set localStorage.viewMode = true;
callExistingEditMethod();
}
EditButtonClickEvent () {
set localStorage.viewMode = false;
callExistingEditMethod();
}
editPagesModule.directive('input', {
if(localStorage.viewMode != true)
//Keep all existing functionality with ng-model
}
else {
//replace input with span or label.
}
})

You could create directives called input, select and textarea, which would automatically be compiled without having to change your existing markup.
Working examples: JSFiddle & Plunker
It would look something like this:
angular.module('myApp', [])
.directive('input', inputDirective)
.directive('select', inputDirective)
.directive('textarea', inputDirective)
.factory('$editMode', editModeFactory)
;
inputDirective.$inject = ['$editMode'];
function inputDirective($editMode) {
return {
link: postLink
};
function postLink(scope, iElement, iAttr) {
scope.$watch($editMode.get, function(edit) {
if (iElement[0].nodeName === 'SELECT') {
if (edit === 'true') iElement.removeAttr('disabled');
else iElement.attr('disabled', true);
}
else {
if (edit === 'true') iElement.removeAttr('readonly');
else iElement.attr('readonly', true);
}
});
}
}
editModeFactory.$inject = ['$window'];
function editModeFactory($window) {
return {
get: function() {
return $window.localStorage.getItem('editMode');
},
set: function(value) {
$window.localStorage.setItem('editMode', value);
}
};
}
I did use the readonly attribute (disabled for select), because the only other option I can think of would be to replace the entire input element with something like a div. You would also have to cache the original element, so you can restore it later...and doing that sort of thing would break your bindings so you'd have to recompile every input, every time. That just seems like a horrible idea.

Related

Enable/disable validation for angular form with nested subforms using `ng-form`

I need to enable/disable all validation rules in Angular form or subform under ng-form="myForm" based on a scope variable $scope.isValidationRequired. So, if isValidationRequired is false, none of the validations set for the designated group of fields will run, and the result will always be myForm.$valid==true, otherwise, the validation rules will run as usual.
I did a lot of research, and realized that this feature is not available out of the box with Angular. However, I found some add-ons or with some customization, it is possible.
For example, I can use the add-on angular-conditional-validation (github and demo) with custom directive enable-validation="isValidationRequired". This will be perfect, except that I cannot apply this feature for a group of fields under ng-form. I have to add this directive for each and every field where applicable.
The other solution is to use custom validation using Angular $validators pipeline. This requires some extra effort and I don't have time since the sprint is almost over and I have to give some results in a few days.
If you have any other suggestions please post an answer.
Use Case:
To clarify the need for this, I will mention the use-case. The end user can fill the form with invalid data and he can click Save button and in this case, the validation rules shouldn't be triggered. Only when the user clicks Validate and Save then the validation rules should be fired.
Solution:
See the final plunker code here.
UPDATE: as per comments below, the solution will cause the browser to hang if inner subforms are used under ng-form. More effort is needed to debug and resolver this issuer. If only one level is used, then it works fine.
UPDATE: The plunker here was updated with a more general solution. Now the code will work with a form that has sub-forms under ng-form. The function setAllInputsDirty() checks if the object is a $$parentForm to stop recursion. Also, the changeValidity() will check if the object is a form using $addControl then it will call itself to validate its child objects. So far, this function works fine, but it needs a bit of additional optimization.
One idea is to reset the errors in the digest loop if the validation flag is disabled. You can iterate through the form errors on change and set them to valid, one by one.
$scope.$watch(function() {
$scope.changeValidity();
}, true);
$scope.changeValidity = function() {
if ($scope.isValidationRequired === "false") {
for (var error in $scope.form.$error) {
while ($scope.form.$error[error]) {
$scope.form.$error[error][0].$setValidity(error, true);
}
}
}
}
Here is a plunkr: https://plnkr.co/edit/fH4vGVPa1MwljPFknYHZ
This is the updated answer that will prevent infinite loop and infinite recursion. Also, the code depends on a known root form which can be tweaked a bit to make it more general.
References: Pixelastic blog and Larry's answer
Plunker: https://plnkr.co/edit/ycPmYDSg6da10KdoNCiM?p=preview
UPDATE: code improvements to make it work for multiple errors for each field in each subform, and loop to ensure the errors are cleared on the subform level
var app = angular.module('plunker', []);
app.controller('MainCtrl', ["$scope", function($scope) {
$scope.isValidationRequired = true;
var rootForm = "form";
function setAllInputsDirty(scope) {
angular.forEach(scope, function(value, key) {
// We skip non-form and non-inputs
if (!value || value.$dirty === undefined) {
return;
}
// Recursively applying same method on all forms included in the form except the parent form
if (value.$addControl && key !== "$$parentForm") {
return setAllInputsDirty(value);
}
if (value.$validate){
value.$validate();
}
// Setting inputs to $dirty, but re-applying its content in itself
if (value.$setViewValue) {
//debugger;
return value.$setViewValue(value.$viewValue);
}
});
}
$scope.$watch(function() {
$scope.changeValidity();
}, true);
$scope.changeValidity = function(theForm) {
debugger;
//This will check if validation is truned off, it will
// clear all validation errors
if (!theForm) {
theForm = $scope[rootForm];
}
if ($scope.isValidationRequired === "false") {
for (var error in theForm.$error) {
errTypeArr = theForm.$error[error];
angular.forEach (errTypeArr, function(value, idx) {
var theObjName = value.$name;
var theObj = value;
if (theObj.$addControl) {
//This is a subform, so call the function recursively for each of the children
var isValid=false;
while (!isValid) {
$scope.changeValidity(theObj);
isValid = theObj.$valid;
}
} else {
while (theObj.$error[error]) {
theObj.$setValidity(error, true);
}
}
})
}
} else {
setAllInputsDirty($scope);
}
}
}]);

AngularJS Get text selection and append html to start and end of selection

Have little dilemma here. I'm building text editor in angular js. The problem that I have is, when user selects part of text within a paragraph or heading I need to change styling of that part of text to bold / italic etc.
So basically I need to wrap selected text in <strong></strong> or <em></em>.
Plunker
I have a directive
editorApp.directive('watchSelection', function() {
return function(scope, elem) {
elem.on('mouseup', function() {
scope.startPosition = elem[0].selectionStart;
scope.endPosition = elem[0].selectionEnd;
// scope.selected = elem[0].value.substring(start, end);
scope.$apply();
});
};
});
That gets text selection its startposition and endposition. On button click I need to wrap that selection in specific tags, which I'm hoping to accomplish with this function:
$scope.boldText = function(startPosition, endPosition) {
$scope.start = startPosition;
$scope.end = endPosition;
var htmlStart = angular.element('<strong>');
var htmlEnd = angular.element('</strong>');
$scope.start.append(htmlStart);
$scope.end.append(htmlEnd);
};
I relatively new to angular and I might have taken a bigger bite than I can handle :)
Issue is I can't get selection to wrap inside them tags.
You don't need to watch anything.
$scope.boldText = function() {
document.execCommand('bold');
};
This will bold the selected text.

Setting Attributes within Angular Directives: Radio Button > set > checked = true

EDIT PLUNKER EXAMPLE: http://plnkr.co/edit/WuiCAmMwbQnC0n197LSJ?p=preview
In the examples "sa" shoulbe checked and remain as checked. It is checked for a short time and then it looses its check status. I dont now why?
I am using a classical old fashion radio-button-based-navigation-tab-menu with Angular-UI-Router, it works well. Each click on a tab gets its URL.
If a user puts the URL manually into the adress bar of a browser and presses enter, the proper URL's content will be shown, it is also OK.
But my tab menu doesn't react on the manually changes at the adress bar. The correponding radio button should be checked. Therefore I've written a directive:
.directive ('checkIfMe', function (){
return {
link: function (scope, e, a) {
////////////////
if (currentUrl == currentNaviElement) {
console.log("Yes it is");
a.$set("checked", true);
}
}
}
I can detect the correct radio button, I see "yes it is" and I want to set its checked attribute to true. I've tried:
e.g. The ID of the current radio button is "navRadio_sa"
a.$set("checked", true);
a.checked = true;
$('#'+a.id+'').prop("checked",true);
All of them didn't work. But it I try it in the firebug console
$('#navRadio_sa').prop("checked",true);
it works. Where is my mistake?
Last but not least, that is a:
Try this
.directive('checkIfMe', function ($timeout) {
return {
link: function (scope, e, a) {
scope.my.info.e.push(e);
scope.my.info.a.push(a);
console
$timeout(function(){
if(a.id == "navRadio_sa") {
e.prop("checked", true);
var me = (a.id).replace("navRadio_","");
}
}, 10);
}
}
});
The e variable is a jQuery object of the element itself. You can work with it the same way that you'd normally work with jQuery objects.
I wrapped it in a timeout of 10 ms to give the ng-repeat time to complete before manipulating the dom. IT looks like ti was not working before because of a race condition. By setting the timeout, it should alleviate the issue.

angularjs with ace code editor - forcing the editor to execute 'onblur' when model changes

I am using angularjs in conjunction with ui-ace, a library that has a directive for the popular ace library.
ui-ace
ace text editor
I have made some modifications to the directive because I need it to work with string[], instead of normal strings. Everything works fine except a strange situation when switching my core model. Here is how it is set up;
There is a grid with objects from my database.
When an item in the grid is clicked, the $scope.Model is populated with the information from the database
This includes a property called Scripting, which is a string[], and it is bound to the ace text editor.
the editor's text is set to $scope.Model.Scripting.join('\n')
this behavior is repeated in different ways in the editor's onChange and onBlur events.
Now, what is going wrong is that I have to actually click on the text editor to trigger the onBlur event before I click on an item in the grid. This has to be repeated each time, or the editor won't update. I cannot figure out why this is happening.
Here is the relevant code. I am going to link the whole directive, as well. The plunkr has everything needed to reproduce the issue, including exact instructions on how to do so.
Full Demonstration and Full Directive Live (Plunkr)
Relevant Directive Changes
return {
restrict: 'EA',
require: '?ngModel',
priority: 1,
link: function (scope, elm, attrs, ngModel) {
/**
* Corresponds to the ngModel, and will enable
* support for binding to an array of strings.
*/
var lines = scope.$eval(attrs.ngModel);
/*************************************************
* normal ui-ace code
************************************************/
/**
* Listener factory. Until now only change listeners can be created.
* #type object
*/
var listenerFactory = {
/**
* Creates a blur listener which propagates the editor session
* to the callback from the user option onBlur. It might be
* exchanged during runtime, if this happens the old listener
* will be unbound.
*
* #param callback callback function defined in the user options
* #see onBlurListener
*/
onBlur: function (callback) {
return function (e) {
if (angular.isArray(lines)) {
scope.$apply(function () {
ngModel.$setViewValue(acee.getSession().doc.$lines);
});
}
executeUserCallback(callback, acee);
};
}
};
// Value Blind
if (angular.isDefined(ngModel)) {
ngModel.$formatters.push(function (value) {
if (angular.isUndefined(value) || value === null) {
return '';
}
else if (angular.isArray(value)) {
return '';
}
// removed error if the editor is bound to array
else if (angular.isObject(value)) {
throw new Error('ui-ace cannot use an object as a model');
}
return value;
});
ngModel.$render = function () {
if (angular.isArray(lines)) {
session.setValue(scope.$eval(attrs.ngModel).join('\n'));
}
else {
// normal ui-ace $render behavior
}
};
}
// set the value when the directive first runs.
if (angular.isArray(lines)) {
ngModel.$setViewValue(acee.getSession().doc.$lines);
}
Looks like you set up ngModel.$formatters incorrectly for your array case.
Try changing:
else if (angular.isArray(value)) {
return '';
}
To:
else if (angular.isArray(value)) {
return value.join('');
}
Personally I think it would have been easier to pass joined arrays in to model and not modify the directive. Then you wouldn't have issues with future upgrades
DEMO

Force validation of entire form in AngularJS upon editing any portion of the form?

I have a form in which the validity depends upon the relationship between multiple textboxes. For example, if there are three textboxes, then the form is valid only if each textbox's integer value is greater than the previous textbox's integer value.
I'd like to set up this form so that if the user edits any of the textboxes, the entire form revalidates.
I've tried setting up ng-change=revalidate() on all the textboxes, with the following:
$scope.revalidate = function() {
var formData = $parse('signals');
var dataCopy = angular.copy(formData($scope));
formData.assign($scope, dataCopy);
};
I hoped that copying and reassigning the form's data would trigger revalidation, but it doesn't seem to work. How would I achieve this?
I solved this by creating a directive. In that directive, I set up a $watch on the concatenated values of all the textboxes. Then when that $watch sees a change in any of the textboxes, it revalidates the element. Since this directive is applied to all my textboxes, the entire form revalidates when any one of the textboxes is edited.
If someone has a more elegant solution than this, let me know.
link: function(scope, elm, attrs, ctrl) {
// when any of the intervals for this signal change, revalidate this interval
scope.$watch(
// loop through all the intervals for this signal, concatenate their values into one string
function() {
var intervals = [],
child = scope.$parent.$$childHead;
while (child !== null) {
console.log(child);
intervals.push(child.interval.end);
child = child.$$nextSibling;
}
return intervals.join();
},
function() {
validate(ctrl.$viewValue);
}
);
function validate(intervalDateTimeFromView) {
var valid = false;
// if this interval ends before or at the same time as the previous interval
if (scope.$$prevSibling && Number(intervalDateTimeFromView) <= Number(scope.$$prevSibling.interval.end))
{
ctrl.$setValidity('overlappingInterval', false);
return undefined;
} else {
ctrl.$setValidity('overlappingInterval', true);
return intervalDateTimeFromView;
}
}
ctrl.$parsers.unshift(validate);
ctrl.$formatters.unshift(validate);
}
It's not perfect, but it's what I'm working on at the moment:
$element.bind('blur', function() {
formCtrl[inputName].$dirty = true;
$scope.$emit('validate-refresh');
});
$scope.$on('validate-refresh', function() {
var control = formCtrl[inputName];
if (control.$dirty) {
control.$setViewValue(control.$viewValue);
}
}

Resources