At the company I work at, we took over a project made with symfony2 and angular.js.
It is a platform containing courses. These courses are actually "books", that are made interactive with videos and places to discuss course content.
A course consists of a number of chapters, each with a set of pages.
The person who developed this in angular, used div with contenteditable attributes to enter text. (see screenshot here : http://imgur.com/kqpelaG ) The divs also have a sk-placeholder attribute.
I know the basics to angular and I presumed the sk-placeholder attribute was a directive, replacing the content of the div with some text. Appearently, the contenteditable is the directive using the attribute sk-placeholder's content, to fill the element. (in this case a div).
When in an editable element, if you press enter, the content is saved. If however, you don't fill in anything, the placeholder text is not removed and it is presumed the content of the specific section of the page you're working on. (I hope I am clear enough, if there are any questions please do ask). It should clear the content of all divs with attribute "contenteditable" by default when saving. And that's the part I can't seem to figure out.
SEK.app.directive('contenteditable', function($location, sekApi, $q){
return {
require: 'ngModel',
restrict: 'A',
link: function(scope, element, attrs, ctrl) {
var richText = attrs.richText || false,
focused = false;
function renderMath () {
if(richText) {
var math = element[0];
MathJax.Hub.Queue(["Typeset",MathJax.Hub,math]);
}
}
function renderElement() {
if(!ctrl.$viewValue && attrs.skPlaceholder) {
element.addClass("sk-placeholding");
element.html(attrs.skPlaceholder);
} else {
element.removeClass("sk-placeholding");
element.html(ctrl.$viewValue);
renderMath();
}
}
ctrl.$render = function() {
renderElement();
};
element[0].onpaste = function(e) {
var pastedText = undefined;
if (window.clipboardData && window.clipboardData.getData) { // IE
pastedText = window.clipboardData.getData('Text');
} else if (e.clipboardData && e.clipboardData.getData) {
pastedText = e.clipboardData.getData('text/plain');
}
SEK.utilities.insertTextAtCursor(pastedText);
// Prevent the default handler from running.
return false;
};
element.bind('focus', function () {
element.html(ctrl.$viewValue || "");
element.removeClass("sk-placeholding");
focused = true;
});
element.bind('blur', function(event) {
var newViewValue = false;
if(element.html().length > 0){
var htmlContent = element.html();
htmlContent = htmlContent.replace(/<div><br><\/div>/g, "<br>");
htmlContent = htmlContent.replace(/<div><br \/><\/div>/g, "<br>");
htmlContent = htmlContent.replace(/<div>/g, "<br>");
htmlContent = htmlContent.replace(/<\/div>/g, "");
newViewValue = htmlContent;
}
if(element.html().length == 0 && attrs.skPlaceholder) {
newViewValue = "";
};
if(typeof newViewValue === "string") {
scope.$apply(function() {
ctrl.$setViewValue(newViewValue);
});
}
renderElement();
focused = false;
});
element.bind('keydown', function(event) {
var esc = event.which == 27,
enter = event.which == 13,
el = event.target;
if(!richText && esc) {
element.html(ctrl.$viewValue);
el.blur();
event.preventDefault();
}
if (esc || (!richText && enter)) {
scope.ngModel = element.html();
el.blur();
event.preventDefault();
}
});
}
}
});
Any questions are more than welcome. Please do note that I'm a novice when it comes to Angular.js
On save check:
attrs.skPlaceholder === element.html() || attrs.skPlaceholder === element.text()
if this expression returns true, the element seems to be empty.
Or you may check:
element.hasClass("sk-placeholding")
Related
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.
I have a textarea which allows users to put in 16 lines. I've build an directive for that purpose, and everything works fine, if the user hits enter.
But I also want to prevent more than 16 lines, even if the user does not hit enter, but puts in a very long text, which is displayed into multiple lines (forced line break).
The background of this question is the following: I have a postcard, and users should be able to enter text to this postcard. The postcard has a fixed width/height. The textarea should represent the fixed width/height of the postcard, so users can see how many space they have left to fill out the postcard (not more than 16 lines).
Is this possible with JS?
My code so far:
HTML
<textarea placeholder="Enter text" rows="16" ng-trim="false" id="message-textarea" maxlines="16" maxlines-prevent-enter="true"></textarea>
JS Directive
app.directive('maxlines', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attrs, ngModel) {
var maxLines = 1;
attrs.$observe('maxlines', function(val) {
maxLines = parseInt(val);
});
ngModel.$validators.maxlines = function(modelValue, viewValue) {
var numLines = (modelValue || '').split("\n").length;
var diffLines = maxLines - numLines;
scope.$emit('cliked-from-directive-maxlines', {diffLines});
return numLines <= maxLines;
};
attrs.$observe('maxlinesPreventEnter', function(preventEnter) {
// if attribute value starts with 'f', treat as false. Everything else is true
preventEnter = (preventEnter || '').toLocaleLowerCase().indexOf('f') !== 0;
if (preventEnter) {
addKeypress();
} else {
removeKeypress();
}
});
function addKeypress() {
elem.on('keypress', function(event) {
// test if adding a newline would cause the validator to fail
if (event.keyCode == 13 && !ngModel.$validators.maxlines(ngModel.$modelValue + '\n', ngModel.$viewValue + '\n')) {
event.preventDefault();
}
});
}
function removeKeypress() {
elem.off('.maxlines');
}
scope.$on('$destroy', removeKeypress);
}
};
});
AFAIk there is no way to read or restrict the number of lines of a textarea. Your best chance IMHO is using 16 single-line inputs and focus the next row whenever the user hits the chars-per-line limit.
Try this
function addKeypress() {
var lines = 16;
elem.on('keypress', function(e) {
var newLines = elem.val().split("\n").length;
if(e.keyCode == 13 && newLines >= lines) {
return false;
}
});
});
}
Maybe you can do something like this:
JavaScript
$(".limit").on("input", function(evt) {
var $limit = $(this);
var limit = this;
if($limit.innerHeight() !== limit.scrollHeight) {
$limit.val($limit.data("before"));
evt.preventDefault();
return false;
} else {
$limit.data("before", $limit.val());
}
}).each(function(index, el) {
$(el).data("before", $(el).val());
});
HTML
<textarea class="limit" rows="10"></textarea>
CSS
.limit {
max-width: 300px;
min-width: 300px;
resize: none;
overflow: hidden;
}
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="";
})
}
})
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);}
So first day on the job with angularjs and i'm not quite getting it. I'm trying to mimic an html5 placeholder using an angular directive. It totally works until I add an ng-model to the field and then it only works after a user interacts with the field and also breaks any value the field had.
code up here
http://jsbin.com/esujax/32/edit
the directive
App.directive('placehold', function(){
return {
restrict: 'A',
link: function(scope, element, attrs) {
var insert = function() {
element.val(attrs.placehold);
};
element.bind('blur', function(){
if(element.val() === '')
insert();
});
element.bind('focus', function(){
if(element.val() === attrs.placehold)
element.val('');
});
if(element.val() === '')
insert();
}
}
});
the html
<textarea ng-model="comment" placehold="with a model it doesn't work"></textarea>
seems super simple but i'm lost
Just few modifications in your sample:
app.directive('placehold', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, ctrl) {
var value;
var placehold = function () {
element.val(attr.placehold)
};
var unplacehold = function () {
element.val('');
};
scope.$watch(attr.ngModel, function (val) {
value = val || '';
});
element.bind('focus', function () {
if(value == '') unplacehold();
});
element.bind('blur', function () {
if (element.val() == '') placehold();
});
ctrl.$formatters.unshift(function (val) {
if (!val) {
placehold();
value = '';
return attr.placehold;
}
return val;
});
}
};
});
You can test it here: http://plnkr.co/edit/8m54JO?p=preview
Not sure, that it is the best solution, anyway it works. Even if you type the same text, that you have in your placehold attribute, cause it checks the model's value on focus.
You can also checkout an Angular.JS module that implements the "placeholder" attribute for older web browsers:
https://github.com/urish/angular-placeholder-shim
I have created a placeholder directive that can take angularjs expressions and also hides the placeholder text on input. You can read about the placeholder at http://blog.f1circle.com/2013/09/supporting-placeholders-in-non-html5.html
Here is the gist.
(function(angular, app) {
"use strict";
app.directive('placeholder',["$document", "$timeout", function($document, $timeout){
var link = function(scope,element,attrs,ctrl){
// if you dont use modernizr library use the solution given at
// http://stackoverflow.com/questions/5536236/javascript-check-for-native-placeholder-support-in-ie8
// to check if placeholder is supported natively
if(Modernizr.input.placeholder){
return;
}
/*
The following keys all cause the caret to jump to the end of the input value
27, Escape
33, Page up
34, Page down
35, End
36, Home
Arrow keys allow you to move the caret manually, which should be prevented when the placeholder is visible
37, Left
38, Up
39, Right
40, Down
The following keys allow you to modify the placeholder text by removing characters, which should be prevented when the placeholder is visible
8, Backspace
46 Delete
*/
var pTxt, modelValue, placeholding = false, badKeys = [27,33,34,35,36,37,38,39,40,8,46];
var unplacehold = function(){
if(!placeholding){
return;
}
placeholding = false;
element.removeClass('placeholder');
element.val('');
};
var placehold = function(){
if(placeholding || modelValue){
return;
}
placeholding = true;
element.addClass('placeholder');
element.val(pTxt);
};
var moveCaret = function(elem, index) {
var range;
if (elem.createTextRange) {
range = elem.createTextRange();
range.move("character", index);
range.select();
} else if (elem.selectionStart) {
elem.focus();
elem.setSelectionRange(index, index);
}
};
attrs.$observe('placeholder',function(value){
pTxt = value;
placeholding = false;
placehold();
});
ctrl.$parsers.unshift(function (value){
modelValue = value;
if(!value){
placehold();
}
if(placeholding){
return '';
}
return value;
});
ctrl.$formatters.unshift(function (value){
if(!value){
placehold();
modelValue = '';
return pTxt;
}
return value;
});
element.on('click focus contextmenu',function(event){
if($document[0].activeElement !== this){
return;
}
if(!modelValue){
moveCaret(this,0);
}
});
element.on('blur',function(){
placehold();
});
element.on('keydown',function(e){
if(!placeholding){
return;
}
if(_.contains(badKeys,e.keyCode)){
if(e.preventDefault){
e.preventDefault();
}
return false;
}
unplacehold();
});
element.on('keyup',function(e){
if(modelValue){
return;
}
placehold();
moveCaret(this,0);
});
element.on('paste',function(e){
$timeout(function(){
modelValue = element.val();
},0);
});
};
return{
restrict: 'A',
require: 'ngModel',
link : link,
priority:3,
};
}]);
})(angular, app);
This works on all cases except when copy and paste the same placeholder text.