Re-focus after click outside md-chips - angularjs

After upgrading the Angular Material from version 1.1.1 to 1.1.4, the md-chips are not working as before.
Type a string in the entry and then click outside, the focus returns to the input.
I do not want this to happen.
With Angular Material 1.1.1:
https://youtu.be/LD2CxbuMxJg
With Angular Material 1.1.4:
https://youtu.be/dG1kKvU1Y0s
Can anybody help me, please?

In mdChipsCtrl there is a boolean variable responsible for returning focus to the entry called shouldFocusLastChip.
I overwritten the function by changing the value of this variable with the following directive:
angular.module('myApp').directive('mdChips', function () {
return {
restrict: 'E',
require: 'mdChips', // Extends the original mdChips directive
link: function (scope, element, attributes, mdChipsCtrl) {
mdChipsCtrl.appendChip = function (newChip) {
// Set to FALSE
this.shouldFocusLastChip = false;
if (this.useTransformChip && this.transformChip) {
var transformedChip = this.transformChip({'$chip': newChip});
// Check to make sure the chip is defined before assigning it, otherwise, we'll just assume
// they want the string version.
if (angular.isDefined(transformedChip)) {
newChip = transformedChip;
}
}
// If items contains an identical object to newChip, do not append
if (angular.isObject(newChip)){
var identical = this.items.some(function(item){
return angular.equals(newChip, item);
});
if (identical) return;
}
// Check for a null (but not undefined), or existing chip and cancel appending
if (newChip === null || this.items.indexOf(newChip) + 1) return;
// Append the new chip onto our list
var length = this.items.push(newChip);
var index = length - 1;
// Update model validation
this.ngModelCtrl.$setDirty();
this.validateModel();
// If they provide the md-on-add attribute, notify them of the chip addition
if (this.useOnAdd && this.onAdd) {
this.onAdd({ '$chip': newChip, '$index': index });
}
};
}
};

Related

How to trigger $destroy event of already removed element scope?

Consider the following directive:
class HasPermissionDirective {
constructor(PermissionService) {
this.restrict = 'A';
this.priority = 1005;
this.PermissionService = PermissionService;
}
compile(element, attrs) {
let permission = _.trim(attrs.hasPermission),
hide = _.get(attrs, 'hideOnly'),
$element = angular.element(element);
if (permission && !this.PermissionService.hasPermission(permission)) {
if (!_.isUndefined(hide)) {
hide = _.trim(hide);
if (hide === 'visibility') {
$element.css('visibility', 'hidden');
} else {
$element.hide();
}
} else {
$element.remove();
}
}
}
link($scope, $element, attrs, ctrl) {
$scope.$destroy();
}
}
HasPermissionDirective.$inject = ['PermissionService'];
The problem is now, the $scope.$destroy() is executed always, for every element-scope the directive is attached to (of course).
When I'm now add a "isRemoved" member variable and set it to true in case the element was removed and do the following in the link function:
if (this.isRemoved) {
$scope.$destroy();
}
Of course the $scope.$destroy() is triggered for every element-scope as soon as at least one element is removed, cause the directive is handled as a singleton not as an instance.
I cannot add any information to the element node since it seems to be removed after compile and is only a comment node "ngInclude: undefined" (no, I don't remove the node, I add an data-attribute and want to get it inside of the link function: $element.data('remove', true) and then want to $destroy and remove()). EDIT: This seems to be the transclude-behavior of the ngInclude-directive.
If I remove the $scope.$destroy() from the link-function and just remove the node, the ngInclude-directive is still running ...
Waht I want? I just want to remove the element from DOM while compile time since the current user has not the permission to see this element/directive/view and I also want to avoid further processing of directives (in my case the ng-include which should not unnecessary request templates (since our server will respond 401 anyway) and so on).
UPDATE: I guess i need a way to set the terminal option inside of the compile function to stop the processing of the upcoming directives. My problem is that ngInclude is running even though the element was removed before.
Solution found! I also have to to use transclusion (it was clear after I was checking the impl of ngIf) and its only possible inside of (pre/post)link function(s), so here the impl. for someone who runs into similar problems:
class HasPermissionDirective {
constructor(PermissionService) {
this.restrict = 'A';
this.priority = 1011; // high prio so the element is removed before all other directives can be processed
this.transclude = 'element';
this.$$tlb = true; // BAD! But w/o, transclusion for different directives won't work :(
this.PermissionService = PermissionService;
}
preLink($scope, $element, attrs, ctrl, $transclude) {
let $newScope = $scope.$new(),
hide = _.get(attrs, 'hideOnly');
$transclude($newScope, ($clone) => {
if (!this.PermissionService.hasPermission(_.trim(attrs.hasPermission))) {
if (!_.isUndefined(hide)) {
hide = _.trim(hide);
if (hide === 'visibility') {
$clone.css('visibility', 'hidden');
} else {
$clone.hide();
}
} else {
$newScope.$destroy();
$newScope = null;
$clone.remove();
$clone = null;
}
} else {
// in case the user has the permission we have to attach the element to the DOM (cause of transclusion)
$element.after($clone);
}
});
}
}
HasPermissionDirective.$inject = ['PermissionService'];
I did also outsource the implementation to a controller so I can reuse the logic, but I wan't to provide a complete example for clarification :)

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

Caret position in textarea with AngularJS

I am asking myself if I am doing it right. The problem I have is that I want to preserve caret position after AngularJS update textarea value.
HTML looks like this:
<div ng-controlle="editorController">
<button ng-click="addSomeTextAtTheEnd()">Add some text at the end</button>
<textarea id="editor" ng-model="editor"></textarea>
</div>
My controller looks like this:
app.controller("editorController", function($scope, $timeout, $window) {
$scope.editor = "";
$scope.addSomeTextAtTheEnd = function() {
$timeout(function() {
$scope.editor = $scope.editor + " Period!";
}, 5000);
}
$scope.$watch("editor", function editorListener() {
var editor = $window.document.getElementById("editor");
var start = editor.selectionStart;
var end = editor.selectionEnd;
$scope.$evalAsync(function() {
editor.selectionStart = start;
editor.selectionEnd = end;
});
});
});
Let say I start typing some text in textarea. Then I hit the button which will soon add " Period!" at the end of $scope.editor value. During the 5 seconds timeout I make focus on textarea again and write some more text. After 5 seconds my textarea value is updated.
I am watching for $scope.editor value. The editorListener will be executed on every $digest cycle. In this cycle also happens two-way data binding. I need to correct caret position right after data binding. Is $scope.$evalAsync(...) the right place where should I do this or not?
Here is a directive I use to manipulate the caret position; however, like I stated in a comment, there is an issue with IE.
Below is something that may help you plan this out. One thing I noticed in your question is that you mention a condition where the user may re-focus the input box to type additional text, which I would believe resets the timeout; would this condition be true?
Instead of using a button to add text, would you rather just add it without? Like running the addSomeTextAtTheEnd function whenever a user un-focuses from the input box?
If you have to use the button, and a user re-focuses on the input box and types more into, you should cancel your button timeout.
Like:
myVar = setTimeout(function(){ alert("Hello"); }, 3000);
// Then clear the timeout in your $watch if there is any change to the input.
clearTimeout(myVar);
If you do it this way, perhaps you will not even need to know the cursor position as the addSomeTextAtTheEnd function timeout will just reset on any input change before the 5 second timeout. If the 5 second timeout occurs then the addSomeTextAtTheEnd will run and "append text to the end" like it's supposed to do. Please give more information and I will update this as needed.
app.directive('filterElement', ['$filter', function($filter){
return {
restrict:'A', // Declares an Attributes Directive.
require: '?ngModel', // ? checks for parent scope if one exists.
link: function( scope, elem, attrs, ngModel ){
if( !ngModel ){ return }
var conditional = attrs.rsFilterElement.conditional ? attrs.rsFilterElement.conditional : null;
scope.$watch(attrs.ngModel, function(value){
if( value == undefined || !attrs.rsFilterElement ){ return }
// Initialize the following values
// These values are used to set the cursor of the input.
var initialCursorPosition = elem[0].selectionStart
var initialValueLength = elem[0].value.length
var difference = false
// Sets ngModelView and ngViewValue
ngModel.$setViewValue($filter( attrs.rsFilterElement )( value, conditional ));
attrs.$$element[0].value = $filter( attrs.rsFilterElement )( value, conditional );
if(elem[0].value.length > initialValueLength){ difference = true }
if(elem[0].value.length < initialValueLength){ initialCursorPosition = initialCursorPosition - 1 }
elem[0].selectionStart = difference ? elem[0].selectionStart : initialCursorPosition
elem[0].selectionEnd = difference ? elem[0].selectionEnd : initialCursorPosition
});
} // end link
} // end return
}]);

angularjs input field directive isn't clearing errors when scope changes value

I have a directive designed to impose date range restrictions on a date input field (earliest and latest). Here is the directive below, I am using the momentjs library to do my date comparison:
.directive('startDate', function () {
return {
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
console.log(arguments);
var compareStartDates = function (value) {
var startDateCompare = moment((attrs.startDate ? scope.$eval(attrs.startDate) : '1901-01-01'), 'YYYY-MM-DD');
if (startDateCompare && startDateCompare.isValid() && value && value.match(/\d{4}\-?\d{2}\-?\d{2}/g)) {
var valueMoment = moment(value, 'YYYY-MM-DD');
if (valueMoment && valueMoment.isValid() && valueMoment < startDateCompare) {
ctrl.$setValidity('startdate', false);
ctrl.$error['startdate'] = true;
return undefined;
}
}
ctrl.$setValidity('startdate', true);
return value;
};
ctrl.$parsers.unshift(compareStartDates);
}
};
})
JSFiddle: http://jsfiddle.net/2ug4X/
Look at the fiddle above and do the following:
1) enter "A" in the text box, the pattern error triggers.
2) click the "CLICK ME" text, which updates teh value of the model on the scope, notice the error clears
3) enter "1800-01-01" in the text box, the date restriction error triggers
4) enter "2000-01-01" in the text box which is a valid date, should clear the startdate error but it doesn't. Any idea why this is happening?
I'd expect updating the ng-model bound variable like so
scope.sample.open_date = '2000-01-01';
would clear the error on the input element like the pattern error clears.
Found this after searching more on Stack: AngularJS custom validation not firing when changing the model programatically
it seems my error was not also pushing the compare function to the ctrl.$formatters like so:
ctrl.$formatters.unshift(compareStartDates);
this did the trick!

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