Using ngModel on input within a directive, maintaining ng-* compatibility - javascript

I'm trying to buld a custom directive that is actually a wrapper around input field (to simplify formatting, encapsulate animations, etc.).
One goal is to use ngModel so my directive would also be compatible with ng-maxlength, ng-required and similar directives depending on ng-Model.
I've created this plunkr with my current state:
http://embed.plnkr.co/xV8IRqTmQmKEBhRhCfBQ/
My problem is that ng-required seems to be working, but invalidates only the complete form (so that form.$invalid becomes true), but not the element itself form.element.$invalid remains false.
Also, ng-maxlength / ng-minlength does not seem to have any effect at all.
What am I missing here? Any hints welcome :)

Hi everyone and thanks a lot for your answers!
I finally figured out what the missing piece for me was: the name attribute which is used by the form to reference the element MUST NOT be on the inner input field.
It has to reside on the outer element that carries the mg-model that also gets the other directives (that interact with the ng-model).
So, to illustrate this in more detail, before my template looked like:
<span class="custom-input-element">
<label for="{{elementId}}-input">{{elementLabel}}<span class="required-marker" ng-if="elementRequired">*</span></label>
<input id="{{elementId}}-input" type="text" name="{{elementName}}" ng-trim ng-model="value" ng-init="focused = false" ng-focus="focused = true" ng-blur="focused = false"/>
</span>
Which was used like
<custom-input id="foldername" name="foldername" label="Folder Name:"
ng-model="folder.name" ng-maxlength="15" ng-required="true"> </custom-input>
Notice the name={{elementName}} that basically overlayed the name="foldername" on my directive's tag.
After removing it from the directives template, the form references my directive and the ngModel on my directive for validation - the input and the inner ng-model keeps hidden. Thus, the interaction with other directives like ng-maxlength and mg-minlength and also custom directives/validators works as expected.
So now, not only the form gets invalidated but also each element is validated in the expected way.
I updated my plunker where everything is working as desired now: http://embed.plnkr.co/i3SzV8H7tnkUk2K9Pq6m/
Thanks for your time and your very valuable input!

I have created one that works, i'll try to show you the relevant part of the code.
The one really annoying point was to reattach the input and the validation to the form of the parent controller.
For this i had to cc a bunch of private code from angular :
/**
* start cc from angular.js to modify $setValidity of ngModel to retrieve the parent form...
*/
var VALID_CLASS = 'data-ng-valid',
INVALID_CLASS = 'data-ng-invalid',
PRISTINE_CLASS = 'data-ng-pristine',
DIRTY_CLASS = 'data-ng-dirty',
UNTOUCHED_CLASS = 'data-ng-untouched',
TOUCHED_CLASS = 'data-ng-touched',
PENDING_CLASS = 'data-ng-pending';
function addSetValidityMethod(context) {
var ctrl = context.ctrl,
$element = context.$element,
classCache = {},
set = context.set,
unset = context.unset,
parentForm = context.parentForm,
$animate = context.$animate;
classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS));
ctrl.$setValidity = setValidity;
function setValidity(validationErrorKey, state, controller) {
if (state === undefined) {
createAndSet('$pending', validationErrorKey, controller);
} else {
unsetAndCleanup('$pending', validationErrorKey, controller);
}
if (!isBoolean(state)) {
unset(ctrl.$error, validationErrorKey, controller);
unset(ctrl.$$success, validationErrorKey, controller);
} else {
if (state) {
unset(ctrl.$error, validationErrorKey, controller);
set(ctrl.$$success, validationErrorKey, controller);
} else {
set(ctrl.$error, validationErrorKey, controller);
unset(ctrl.$$success, validationErrorKey, controller);
}
}
if (ctrl.$pending) {
cachedToggleClass(PENDING_CLASS, true);
ctrl.$valid = ctrl.$invalid = undefined;
toggleValidationCss('', null);
} else {
cachedToggleClass(PENDING_CLASS, false);
ctrl.$valid = isObjectEmpty(ctrl.$error);
ctrl.$invalid = !ctrl.$valid;
toggleValidationCss('', ctrl.$valid);
}
// re-read the state as the set/unset methods could have
// combined state in ctrl.$error[validationError] (used for forms),
// where setting/unsetting only increments/decrements the value,
// and does not replace it.
var combinedState;
if (ctrl.$pending && ctrl.$pending[validationErrorKey]) {
combinedState = undefined;
} else if (ctrl.$error[validationErrorKey]) {
combinedState = false;
} else if (ctrl.$$success[validationErrorKey]) {
combinedState = true;
} else {
combinedState = null;
}
toggleValidationCss(validationErrorKey, combinedState);
parentForm.$setValidity(validationErrorKey, combinedState, ctrl);
}
function createAndSet(name, value, controller) {
if (!ctrl[name]) {
ctrl[name] = {};
}
set(ctrl[name], value, controller);
}
function unsetAndCleanup(name, value, controller) {
if (ctrl[name]) {
unset(ctrl[name], value, controller);
}
if (isObjectEmpty(ctrl[name])) {
ctrl[name] = undefined;
}
}
function cachedToggleClass(className, switchValue) {
if (switchValue && !classCache[className]) {
$animate.addClass($element, className);
classCache[className] = true;
} else if (!switchValue && classCache[className]) {
$animate.removeClass($element, className);
classCache[className] = false;
}
}
function toggleValidationCss(validationErrorKey, isValid) {
validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true);
cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false);
}
}
function arrayRemove(array, value) {
var index = array.indexOf(value);
if (index >= 0) {
array.splice(index, 1);
}
return index;
}
function isBoolean(value) {
return typeof value === 'boolean';
};
var SNAKE_CASE_REGEXP = /[A-Z]/g;
function snake_case(name, separator) {
separator = separator || '_';
return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
return (pos ? separator : '') + letter.toLowerCase();
});
}
function isObjectEmpty(obj) {
if (obj) {
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
return false;
}
}
}
return true;
};
/**
* end of cc
*/
Then in the link function :
function(scope, element, attrs, ctrl, transclude){
[...]
scope.form = element.parent().controller('form');
var transcludedContent = transclude(scope.$parent);
// find the input
var fieldContent = findFormField(transcludedContent);
var ngModelCtrl = angular.element(fieldContent).controller('ngModel');
if(!ngModelCtrl){
throw 'transcluded form field must have a ng-model';
}
addSetValidityMethod({
ctrl: ngModelCtrl,
$element: angular.element(fieldContent),
set: function(object, property, controller) {
var list = object[property];
if (!list) {
object[property] = [controller];
} else {
var index = list.indexOf(controller);
if (index === -1) {
list.push(controller);
}
}
},
unset: function(object, property, controller) {
var list = object[property];
if (!list) {
return;
}
arrayRemove(list, controller);
if (list.length === 0) {
delete object[property];
}
},
parentForm: scope.form,
$animate: $animate
});
scope.form.$addControl(ngModelCtrl);
element.html(template);
$compile(element.contents())(scope);
element.find('.ng-form-field-content').append(transcludedContent);
// remove the control from the form, otherwise an ng-if that hide an invalid input will block your form
scope.$on(
"$destroy",
function handleDestroyEvent() {
scope.form.$removeControl(ngModelCtrl);
});
The template is a variable containing the html of my wrapping around the input. (it generates the label, put a start if required, show a check or cross sign if field valid/invalid,...).
EDIT :
With my directive i can do :
<div my-directive>
<input/textarea/select ng-model="", required/ng-required, ng-pattern, <custom directive validation>...
</div>
And it will give something like
<div my-directive>
<label for=<input'sname>>Texte</label>
<input [the input will all his attrs]/>
[some extra content]
</div>
I can even put some intermediary nodes or have multiple input that point to the same ng-model (like with checkbox/Radio buttons), however it won't works with different ng-models. I didn't push it that far.

Related

Which characters are considered unsafe and should be blocked from a text input field?

Which characters are considered unsafe to allow for users to type in a text input field, to prevent hackers etc to perform SQL injections etc? Which characters should be blocked from the input?
For the record, I'm currently blocking the input with AngularJS using the following HTML:
<input type="text" ng-pattern-restrict='^[^<>#*"]+$'>
And here's the beautiful directive I found:
/* RESTRICT CERTAIN CHARACTERS IN INPUT FIELDS
<input type="text" ng-pattern-restrict="^[A-Za-z0-9]*$">
Alpha numeric chars only ^[A-Za-z0-9]*$
Date format YYYY-MM-DD ^\d{0,4}(-\d{0,2}(-\d{0,2})?)?$
*/
/*jslint browser: true, plusplus: true, indent: 2 */
// This will be removed by uglify, along with the DEBUG code
if (typeof DEBUG === 'undefined') {
DEBUG = true;
}
// Logic and fallbacks based on the following SO answers:
// - Getting caret position cross browser: http://stackoverflow.com/a/9370239/147507
// - Selection API on non input-text fields: http://stackoverflow.com/a/24247942/147507
// - Set cursor position on input text: http://stackoverflow.com/q/5755826/147507
angular.module('ngPatternRestrict', [])
.directive('ngPatternRestrict', ['$log', function ($log) {
'use strict';
function showDebugInfo() {
$log.debug("[ngPatternRestrict] " + Array.prototype.join.call(arguments, ' '));
}
return {
restrict: 'A',
require: "?ngModel",
compile: function uiPatternRestrictCompile() {
DEBUG && showDebugInfo("Loaded");
return function ngPatternRestrictLinking(scope, iElement, iAttrs, ngModelController) {
var regex, // validation regex object
oldValue, // keeping track of the previous value of the element
caretPosition, // keeping track of where the caret is at to avoid jumpiness
// housekeeping
initialized = false, // have we initialized our directive yet?
eventsBound = false, // have we bound our events yet?
// functions
getCaretPosition, // function to get the caret position, set in detectGetCaretPositionMethods
setCaretPosition; // function to set the caret position, set in detectSetCaretPositionMethods
//-------------------------------------------------------------------
// caret position
function getCaretPositionWithInputSelectionStart() {
return iElement[0].selectionStart; // we need to go under jqlite
}
function getCaretPositionWithDocumentSelection() {
// create a selection range from where we are to the beggining
// and measure how much we moved
var range = document.selection.createRange();
range.moveStart('character', -iElement.val().length);
return range.text.length;
}
function getCaretPositionWithWindowSelection() {
var s = window.getSelection(),
originalSelectionLength = String(s).length,
selectionLength,
didReachZero = false,
detectedCaretPosition,
restorePositionCounter;
do {
selectionLength = String(s).length;
s.modify('extend', 'backward', 'character');
// we're undoing a selection, and starting a new one towards the beggining of the string
if (String(s).length === 0) {
didReachZero = true;
}
} while (selectionLength !== String(s).length);
detectedCaretPosition = didReachZero ? selectionLength : selectionLength - originalSelectionLength;
s.collapseToStart();
restorePositionCounter = detectedCaretPosition;
while (restorePositionCounter-- > 0) {
s.modify('move', 'forward', 'character');
}
while (originalSelectionLength-- > 0) {
s.modify('extend', 'forward', 'character');
}
return detectedCaretPosition;
}
function setCaretPositionWithSetSelectionRange(position) {
iElement[0].setSelectionRange(position, position);
}
function setCaretPositionWithCreateTextRange(position) {
var textRange = iElement[0].createTextRange();
textRange.collapse(true);
textRange.moveEnd('character', position);
textRange.moveStart('character', position);
textRange.select();
}
function setCaretPositionWithWindowSelection(position) {
var s = window.getSelection(),
selectionLength;
do {
selectionLength = String(s).length;
s.modify('extend', 'backward', 'line');
} while (selectionLength !== String(s).length);
s.collapseToStart();
while (position--) {
s.modify('move', 'forward', 'character');
}
}
// HACK: Opera 12 won't give us a wrong validity status although the input is invalid
// we can select the whole text and check the selection size
// Congratulations to IE 11 for doing the same but not returning the selection.
function getValueLengthThroughSelection(input) {
// only do this on opera, since it'll mess up the caret position
// and break Firefox functionality
if (!/Opera/i.test(navigator.userAgent)) {
return 0;
}
input.focus();
document.execCommand("selectAll");
var focusNode = window.getSelection().focusNode;
return (focusNode || {}).selectionStart || 0;
}
//-------------------------------------------------------------------
// event handlers
function revertToPreviousValue() {
if (ngModelController) {
scope.$apply(function () {
ngModelController.$setViewValue(oldValue);
});
}
iElement.val(oldValue);
if (!angular.isUndefined(caretPosition)) {
setCaretPosition(caretPosition);
}
}
function updateCurrentValue(newValue) {
oldValue = newValue;
caretPosition = getCaretPosition();
}
function genericEventHandler(evt) {
DEBUG && showDebugInfo("Reacting to event:", evt.type);
//HACK Chrome returns an empty string as value if user inputs a non-numeric string into a number type input
// and this may happen with other non-text inputs soon enough. As such, if getting the string only gives us an
// empty string, we don't have the chance of validating it against a regex. All we can do is assume it's wrong,
// since the browser is rejecting it either way.
var newValue = iElement.val(),
inputValidity = iElement.prop("validity");
if (newValue === "" && iElement.attr("type") !== "text" && inputValidity && inputValidity.badInput) {
DEBUG && showDebugInfo("Value cannot be verified. Should be invalid. Reverting back to:", oldValue);
evt.preventDefault();
revertToPreviousValue();
} else if (newValue === "" && getValueLengthThroughSelection(iElement[0]) !== 0) {
DEBUG && showDebugInfo("Invalid input. Reverting back to:", oldValue);
evt.preventDefault();
revertToPreviousValue();
} else if (regex.test(newValue)) {
DEBUG && showDebugInfo("New value passed validation against", regex, newValue);
updateCurrentValue(newValue);
} else {
DEBUG && showDebugInfo("New value did NOT pass validation against", regex, newValue, "Reverting back to:", oldValue);
evt.preventDefault();
revertToPreviousValue();
}
}
//-------------------------------------------------------------------
// setup based on attributes
function tryParseRegex(regexString) {
try {
regex = new RegExp(regexString);
} catch (e) {
throw "Invalid RegEx string parsed for ngPatternRestrict: " + regexString;
}
}
//-------------------------------------------------------------------
// setup events
function bindListeners() {
if (eventsBound) {
return;
}
iElement.bind('input keyup click', genericEventHandler);
DEBUG && showDebugInfo("Bound events: input, keyup, click");
}
function unbindListeners() {
if (!eventsBound) {
return;
}
iElement.unbind('input', genericEventHandler);
//input: HTML5 spec, changes in content
iElement.unbind('keyup', genericEventHandler);
//keyup: DOM L3 spec, key released (possibly changing content)
iElement.unbind('click', genericEventHandler);
//click: DOM L3 spec, mouse clicked and released (possibly changing content)
DEBUG && showDebugInfo("Unbound events: input, keyup, click");
eventsBound = false;
}
//-------------------------------------------------------------------
// initialization
function readPattern() {
var entryRegex = !!iAttrs.ngPatternRestrict ? iAttrs.ngPatternRestrict : iAttrs.pattern;
DEBUG && showDebugInfo("RegEx to use:", entryRegex);
tryParseRegex(entryRegex);
}
function notThrows(testFn, shouldReturnTruthy) {
try {
return testFn() || !shouldReturnTruthy;
} catch (e) {
return false;
}
}
function detectGetCaretPositionMethods() {
var input = iElement[0];
// Chrome will throw on input.selectionStart of input type=number
// See http://stackoverflow.com/a/21959157/147507
if (notThrows(function () { return input.selectionStart; })) {
getCaretPosition = getCaretPositionWithInputSelectionStart;
} else {
// IE 9- will use document.selection
// TODO support IE 11+ with document.getSelection()
if (notThrows(function () { return document.selection; }, true)) {
getCaretPosition = getCaretPositionWithDocumentSelection;
} else {
getCaretPosition = getCaretPositionWithWindowSelection;
}
}
}
function detectSetCaretPositionMethods() {
var input = iElement[0];
if (typeof input.setSelectionRange === 'function') {
setCaretPosition = setCaretPositionWithSetSelectionRange;
} else if (typeof input.createTextRange === 'function') {
setCaretPosition = setCaretPositionWithCreateTextRange;
} else {
setCaretPosition = setCaretPositionWithWindowSelection;
}
}
function initialize() {
if (initialized) {
return;
}
DEBUG && showDebugInfo("Initializing");
readPattern();
oldValue = iElement.val();
if (!oldValue) {
oldValue = "";
}
DEBUG && showDebugInfo("Original value:", oldValue);
bindListeners();
detectGetCaretPositionMethods();
detectSetCaretPositionMethods();
initialized = true;
}
function uninitialize() {
DEBUG && showDebugInfo("Uninitializing");
unbindListeners();
}
iAttrs.$observe("ngPatternRestrict", readPattern);
iAttrs.$observe("pattern", readPattern);
scope.$on("$destroy", uninitialize);
initialize();
};
}
};
}]);
Please note that it's also necessary to load this directive in the beginning:
var app = angular.module("myApp", ['ngPatternRestrict']);
As others have pointed out, you should solve this on the back-end by using prepared statements. It's the only way as the client can be edited relatively easily.
In Java, for example, you can use this:
PreparedStatement stmt = conn.prepareStatement("INSERT INTO student VALUES(?)");
stmt.setString(1, userInput);
stmt.execute();
This way the input is placed into the query as plain text, so SQL injection will not be an issue and Little Bobby Tables can go to your school.

ng-change is watching for all items in ng-repeat instead of one item

I have this template:
<div class=characteristics-list>
<input ng-model="search" ng-change="onSearchChange(search)">
<div ng-repeat="policy in item.policies" ng-include="'policy.html'"></div>
</div>
and this logic inside controller:
$scope.onSearchChange = function (originalValue) {
var value = originalValue.toLowerCase();
if (!value) {
$scope.item.policies.forEach(function (policy) {
policy.isCollapsed = true;
policy.isHidden = false;
policy.items.forEach(function (item) {
item.isHidden = false;
});
});
return;
}
$scope.item.policies.forEach(function (policy) {
var isValueInPolicyName = policy.name.toLowerCase().includes(value);
var visibleItems = policy.items.filter(function (item) {
var isValueInDescription = item.description.toLowerCase().includes(value);
item.isHidden = !isValueInDescription;
return isValueInDescription;
});
policy.isHidden = !isValueInPolicyName && !visibleItems.length;
if (!isValueInPolicyName && !!visibleItems.length) {
policy.isCollapsed = false;
}
if (isValueInPolicyName && !visibleItems.length) {
policy.items.forEach(function (item) {
item.isHidden = false;
});
}
});
};
The result of changing search value is presented on the picture attached.
When input value is changed, the function is firing, but it's searching in each item inside ng-repeat. How do I handle just one current item without affecting all other items in ng-repeat?
It means, when I change the search value in one item, it should not handle other items.
I tried to create unique ng-models dynamically, but it doesn't help.
AngularJS version is 1.2.27

md-char-count not updating in md-input-container

Consider the following simple md-input-container with textarea inside:
<md-input-container class="md-block">
<textarea aria-label="tt" ng-model="obj.prop" md-maxlength="250" rows="5"></textarea>
</md-input-container>
When i update obj.prop in my controller, the text is changed (therefore i should call $scope.$evalAsync() too). However, the md-char-count is still not updated. In order it to be updated, user will click on text-area and change it. Only then it is being changed.
I think its a known bug but is there any improvements or is there a -at least- a workaround to this problem?
Here you find a codepen
ps(if needed):angular-material version 1.0.1 & angular 1.4.5
I resolved this issue by override - md-maxlength directive.
Try this codepen - http://codepen.io/himvins/pen/JEgyOK?editors=1010
Function for overriding md-maxlength:
function override_mdMaxlength($provide) {
$provide.decorator(
'mdMaxlengthDirective',
function($delegate) {
var mdMaxlength = $delegate[0];
var link = mdMaxlength.link;
mdMaxlength.compile = function() {
//Line 18 to 64: Code of md-maxlength directive. Change in line 62
return function(scope, element, attr, ctrls) {
var maxlength;
var ngModelCtrl = ctrls[0];
var containerCtrl = ctrls[1];
var charCountEl = angular.element('<div class="md-char-counter">');
attr.$set('ngTrim', 'false');
containerCtrl.element.append(charCountEl);
ngModelCtrl.$formatters.push(renderCharCount);
ngModelCtrl.$viewChangeListeners.push(renderCharCount);
element.on(
'input keydown',
function() {
renderCharCount(); //make sure it's called with no args
}
);
scope.$watch(attr.mdMaxlength, function(value) {
maxlength = value;
if (angular.isNumber(value) && value > 0) {
if (!charCountEl.parent().length) {
$animate.enter(
charCountEl,
containerCtrl.element,
angular.element(containerCtrl.element[0].lastElementChild)
);
}
renderCharCount();
} else {
$animate.leave(charCountEl);
}
});
ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) {
if (!angular.isNumber(maxlength) || maxlength < 0) {
return true;
}
return (modelValue || element.val() || viewValue || '').length <= maxlength;
};
function renderCharCount(value) {
//Original code commented
debugger;
if(ngModelCtrl.$modelValue !== "")
charCountEl.text( ( element.val() || value || '' ).length + '/' + maxlength );
else
charCountEl.text((ngModelCtrl.$modelValue || '').length + '/' + maxlength);
return value;
}
};
};
return $delegate;
}
);
}
Add this function into your module's config like below:
yourModule.config(override_mdMaxlength);
This will correct character-count behavior.
I meet this bug too, after look deeper into the mdMaxlength directive, I found that we could solve this problem easily like this(according to your code):
angular.module('app').controller(function($timeout){
$scope.obj={prop:""};
//after user input some value reset the form
$scope.reset=function(){
$scope.obj.prop=null;
$scope.formName.$setPristine();
$scope.formName.$setValidity();
$scope.formName.$setUntouched();
$timeout(function(){
$scope.obj.prop="";
})
}
})

AngularJS Show typeahead on button click

I am using the typeahead directive in AngularJS and it works fine. However, I would like to have a button outside of the input that when clicked would show the typeahead dropdown. Here is a snippet of what I am after...
<li class="input">
<input focus-me="click" ng-model="something"
typeahead="state for state in Suggestions | filter:$viewValue:stateComparator" typeahead-focus typeahead-focus-first="false" typeahead-on-select="updateTagInput(newTagName)">
Open
</li>
Ok, I am having an absolutely terrible time trying to create a JSFiddle or even a Plunkr for this, so I will just give you the code for this directive.
This directive originally comes from..
This epic Bootstrap library!
..and I stole it and played with it. If you would like to use it, you will need the "Bootstrap" (its really a subset of angular directives) library that I linked to. You can make your own subset of this library, but I am not entirely sure of all of the dependencies my directive has as I am using the entire library in my project. Basically, you need any directive that starts with "typeahead".
As you can see, I have named the directive wwTypeahead (that "ww" is for WebWanderer!). It is a very easy to use directive and it works just like the original.
<input
class="form-control"
type="text"
spellcheck="false"
ng-model="selection"
ng-trim="false"
placeholder="Search Here"
ww-typeahead="key as key.label for key in list"
typeahead-on-select="selectionMade($item, $model, $label)"
typeahead-min-length="0"
/>
The really important part to note is the attribute typeahead-min-length="0" which has really been the heart of many discussions online. I managed to make that work.
This directive is meant to take the place of the typeahead directive in the library I linked to. Your typeahead list will be shown on focus of your input box. No, the list does not show on the click of a button, but hopefully getting there will be baby-steps from here. If you need help implementing that, I will be happy to help.
/*
NOTE:
The following directive is a modification of the
Angular typeahead directive. The normal directives,
unfortunately, do not allow matching on 0 length values
and the user may want a returned list of all values during
the lack of input.
This directives was taken from ...
http://angular-ui.github.io/bootstrap/
..and modified.
*/
angular.module('ui.directives', []).directive('wwTypeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser',
function($compile, $parse, $q, $timeout, $document, $position, typeaheadParser)
{
var HOT_KEYS = [9, 13, 27, 38, 40];
return {
require:'ngModel',
link:function(originalScope, element, attrs, modelCtrl)
{
//SUPPORTED ATTRIBUTES (OPTIONS)
//minimal no of characters that needs to be entered before typeahead kicks-in
//var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1;
var testEval = originalScope.$eval(attrs.typeaheadMinLength);
var minSearch = !isNaN(parseFloat(testEval)) && isFinite(testEval) || 1;
//minimal wait time after last character typed before typehead kicks-in
var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
//should it restrict model values to the ones selected from the popup only?
var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
//binding to a variable that indicates if matches are being retrieved asynchronously
var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
//a callback executed when a match is selected
var onSelectCallback = $parse(attrs.typeaheadOnSelect);
var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
//INTERNAL VARIABLES
//model setter executed upon match selection
var $setModelValue = $parse(attrs.ngModel).assign;
//expressions used by typeahead
var parserResult = typeaheadParser.parse(attrs.cmcTypeahead);
//pop-up element used to display matches
var popUpEl = angular.element('<typeahead-popup></typeahead-popup>');
popUpEl.attr({
matches: 'matches',
active: 'activeIdx',
select: 'select(activeIdx)',
query: 'query',
position: 'position'
});
//custom item template
if(angular.isDefined(attrs.typeaheadTemplateUrl))
{
popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
}
//create a child scope for the typeahead directive so we are not polluting original scope
//with typeahead-specific data (matches, query etc.)
var scope = originalScope.$new();
originalScope.$on('$destroy', function()
{
scope.$destroy();
});
var resetMatches = function()
{
scope.matches = [];
scope.activeIdx = -1;
};
var getMatchesAsync = function(inputValue)
{
var matchParsePrefix = originalScope.$eval(attrs.typeaheadParsePrefix);
var locals = {
$viewValue: inputValue.indexOf(matchParsePrefix) === 0 ? inputValue.substring(matchParsePrefix.length, (inputValue.length + 1)) : inputValue
};
isLoadingSetter(originalScope, true);
$q.when(parserResult.source(scope, locals)).then(function(matches)
{
//it might happen that several async queries were in progress if a user were typing fast
//but we are interested only in responses that correspond to the current view value
//if(matches && inputValue === modelCtrl.$viewValue)
/*
Ehh.. that didn't seem to work when I "cleared" the input box
*/
if(matches)
{
if(matches.length > 0)
{
scope.activeIdx = 0;
scope.matches.length = 0;
//transform labels
for(var i = 0; i < matches.length; i++)
{
locals[parserResult.itemName] = matches[i];
scope.matches.push({
label: parserResult.viewMapper(scope, locals),
model: matches[i]
});
}
scope.query = inputValue;
//position pop-up with matches - we need to re-calculate its position each time we are opening a window
//with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
//due to other elements being rendered
scope.position = $position.position(element);
scope.position.top = scope.position.top + element.prop('offsetHeight');
}
else if(minSearch === 0)
{
resetMatches();//temp
}
else
{
resetMatches();
}
isLoadingSetter(originalScope, false);
}
}, function()
{
resetMatches();
isLoadingSetter(originalScope, false);
});
};
resetMatches();
/*
Can't figure out how to make this work...*/
if(attrs.hasOwnProperty('typeaheadBindMatchReloader'))
{
$parse(attrs.typeaheadBindMatchReloader).assign(scope, function()
{
getMatchesAsync(element[0].value);
});
}
//we need to propagate user's query so we can higlight matches
scope.query = undefined;
//Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
var timeoutPromise;
//plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
//$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
modelCtrl.$parsers.unshift(function(inputValue)
{
resetMatches();
if((inputValue && inputValue.length >= minSearch)
|| minSearch === 0)
{
if(waitTime > 0)
{
if(timeoutPromise)
{
$timeout.cancel(timeoutPromise);//cancel previous timeout
}
timeoutPromise = $timeout(function()
{
getMatchesAsync(inputValue);
}, waitTime);
}
else
{
getMatchesAsync(inputValue);
}
}
if(isEditable)
{
return inputValue;
}
else
{
modelCtrl.$setValidity('editable', false);
return undefined;
}
});
modelCtrl.$formatters.push(function(modelValue)
{
var candidateViewValue, emptyViewValue;
var locals = {};
if(inputFormatter)
{
locals['$model'] = modelValue;
return inputFormatter(originalScope, locals);
}
else
{
//it might happen that we don't have enough info to properly render input value
//we need to check for this situation and simply return model value if we can't apply custom formatting
locals[parserResult.itemName] = modelValue;
candidateViewValue = parserResult.viewMapper(originalScope, locals);
locals[parserResult.itemName] = undefined;
emptyViewValue = parserResult.viewMapper(originalScope, locals);
return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue;
}
});
scope.select = function(activeIdx)
{
//called from within the $digest() cycle
var locals = {};
var model, item;
locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
model = parserResult.modelMapper(originalScope, locals);
$setModelValue(originalScope, model);
modelCtrl.$setValidity('editable', true);
onSelectCallback(originalScope, {
$item: item,
$model: model,
$label: parserResult.viewMapper(originalScope, locals)
});
resetMatches();
//return focus to the input element if a mach was selected via a mouse click event
element[0].focus();
};
//bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
element.bind('keydown', function(evt)
{
//typeahead is open and an "interesting" key was pressed
if(scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1)
return;
evt.preventDefault();
if(evt.which === 40)
{
scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
scope.$digest();
}
else if(evt.which === 38)
{
scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1;
scope.$digest();
}
else if(evt.which === 13 || evt.which === 9)
{
scope.$apply(function()
{
scope.select(scope.activeIdx);
});
}
else if(evt.which === 27)
{
evt.stopPropagation();
resetMatches();
scope.$digest();
}
});
// Keep reference to click handler to unbind it.
var dismissClickHandler = function(evt)
{
if(element[0] !== evt.target)
{
resetMatches();
scope.$digest();
}
else
{
getMatchesAsync(element[0].value);
}
};
$document.bind('click', dismissClickHandler);
originalScope.$on('$destroy', function()
{
$document.unbind('click', dismissClickHandler);
});
element.after($compile(popUpEl)(scope));
}
};
}]);
Call To Action:
Somebody PLEASE make a working example of this typeahead directive! I would forever be in debt to you! (well, not really but it would make me very happy)
DISCLAIMER:
I understand that this answer is in no way orthodox. I did not provide the askee (askee?) with a direct answer to the question, yet I did provide the tools that I believe are needed to get to his/her answer. I understand that I should spend the time to make a working example, but I am a very busy man and simply wished to share my work with the community, as I have seen this question asked too many times while I sit back and hold the answer. Please let me know if you have any issues, questions, or complications. I am happy to help.
Thanks!
<input
class="form-control"
spellcheck="false"
focus-me="click" ng-model="something"
ng-trim="false"
placeholder="Search Here"
uib-typeahead="key as key.label for key in list | filter:{label:$viewValue}"
typeahead-on-select="openTypeAhead($item, $model, $label)"
typeahead-min-length="0"
/>
in controller angularjs
$scope.openTypeAhead = ($item, $model, $label) =>{ console.log('arg =>',$item, $model, $label);}

Getting HTML element class from AngularJS Directive

I have block of AngularJS code that looks like the following:
<ion-list can-swipe="true">
<ion-item ng-repeat="item in items" simplify-item>
<div class="row" style="background-color:orange;">
Hello
</div>
</ion-item>
</ion-list>
The directives come from the ionic framework. Still, I am trying to add my own directive to this. I want to set a CSS property via an attribute called "simplify-item". My directive to make this happen looks like this:
myApp.directive('simplifyItem', function() {
return {
restrict:'A',
link: function(element) {
console.log('linking element');
if (element === null) {
console.log('element is null');
} else {
var result = element.hasClass('item-complex');
if (result === null) {
console.log('result is null.');
} else {
console.log('Sweet!');
}
}
}
};
});
When the line var result = element.hasClass('item-complex'); gets executed, I get an exception. The exception says:
undefined is not a function
I don't understand why I can't do this. What am I doing wrong?
try like this
myApp.directive('simplifyItem', function() {
return {
restrict:'A',
link: function(scope , iElement, iAttrs) {
var element = iElement;
console.log('linking element');
if (element === null) {
console.log('element is null');
} else {
var result = element.hasClass('item-complex');
if (result === null) {
console.log('result is null.');
} else {
console.log('Sweet!');
}
}
}
};
});
explanation from angular docs
Directives that want to modify the DOM typically use the link option. link takes a function with the following signature, function link(scope, element, attrs) { ... } where:
scope is an Angular scope object.
element is the jqLite-wrapped element that this directive matches.
attrs is a hash object with key-value pairs of normalized attribute names and their corresponding
attribute values.

Categories