I am converting some code to use angular directives. I am new at this..
The old code is:
$('.header .mobile-nav ').append($('.navigation').html());
$('.header .mobile-nav li').bind('click', function(e) {
var $this = $(this);
var $ulKid = $this.find('>ul');
var $ulKidA = $this.find('>a');
if ($ulKid.length === 0 && $ulKidA[0].nodeName.toLowerCase() === 'a') {
window.location.href = $ulKidA.attr('href');
}
else {
$ulKid.toggle(0, function() {
if ($(this).css('display') === 'block') {
$ulKidA.find('.icon-chevron-down').removeClass('icon-chevron-down').addClass('icon-chevron-up');
}
else {
$ulKidA.find('.icon-chevron-up').removeClass('icon-chevron-up').addClass('icon-chevron-down');
}
});
}
e.stopPropagation();
return false;
});
My attempt at creative the correct directives is as follows:
app.directive('mobilenav', function () {
return { template: $('.navigation').html() };
});
app.directive('mobileNavMenu', function () {
var directive = {
link: link,
restrict: 'A'
};
return directive;
function link(scope, element, attrs) {
var iElement = element.find('.header .mobile-nav li');
iElement.bind('click', function (e) {
var $this = $(this);
var $ulKid = $this.find('* > ul');
var $ulKidA = $this.find('* > a');
if ($ulKid.length === 0 && $ulKidA[0].nodeName.toLowerCase() === 'a') {
window.location.href = $ulKidA.attr('href');
} else {
$ulKid.toggle(0, function () {
if ($(this).css('display') === 'block') {
$ulKidA.find('.icon-chevron-down').removeClass('icon-chevron-down').addClass('icon-chevron-up');
} else {
$ulKidA.find('.icon-chevron-up').removeClass('icon-chevron-up').addClass('icon-chevron-down');
}
});
}
e.stopPropagation();
return false;
});
}
});
The "click" event is not being bound. I think it could be because it is not finding the element. Am I using the right approach? Can you help me fix my directives?
Thanks!
I think there are a few things you could be doing better with this approach. First, I don't think your directives should searching for or modifying other elements by CSS.
So first I would modify this directive to bind directly on the passed in element:
function link(scope, element, attrs) {
//var iElement = element.find('.header .mobile-nav li');
iElement.bind('click', function (e) {
...
This might mean you need to apply your directive to a different element than you were originally intending.
Next, for the section where you toggle the chevrons, I think you might want to do this differently. You could for example, set a variable in the scope, or in a service, and then create another directive that watches that variable and responds to the changes in it. Alternatively, you might be able to use ng-show/ng-hide to elements based on the change.
These questions might be helpful to you as well:
Sharing data between directives
Changing CSS from AngularJS
Related
I'm using smart-table and I need to preselect a specific row.
So after loading my list, I loop into it and set the isSelected attribute when I reach the item I want to select:
// Preselect a row
for (var i = 0, len = scope.displayCollection.length; i < len; i += 1) {
var person = scope.displayCollection[i];
if (person.firstName === 'Blandine') {
person.isSelected = true;
scope.selected = person;
break;
}
}
It's working fine, but when I want to select another line, the preselected line is not unselected! I have to click on it to manually unselect it and then be able to select another line correctly.
Here is a JSFiddle explaining the issue: http://jsfiddle.net/6pykn5hu/3/
I tried what's proposed there Smart-Table - Select first row displayed (angularjs) but did not manage to have someting working.
Thanks
So I looked through their directive as you can see it calls a function in a parent directive stTable. The row is bound to a click handler...Which calls the ctrl.select() function from stTable this function in turn stores the last selected row. This is your problem because this event does not fire it never sets the last clicked row and thus never looks to remove its class. I rewrote the directive for you so that it would work for what you are trying to achieve but it could pretty easily be improved.
app.directive('prSystem', function () {
return {
restrict: 'A',
require: '^stTable',
scope: {
row: '=prSystem'
},
link: function (scope, element, attr, ctrl) {
var mode = attr.stSelectMode || 'single';
if(scope.row.isSelected) {
scope.row.isSelected = undefined;
ctrl.select(scope.row, mode);
}
element.bind('click', function () {
scope.$apply(function () {
ctrl.select(scope.row, mode);
});
});
scope.$watch('row.isSelected', function (newValue) {
if (newValue === true) {
element.addClass('st-selected');
} else {
element.removeClass('st-selected');
}
});
}
}
})
Finally I found a solution:
No more $watch, but a ng-click on each rows:
<tr st-select-row="row" ng-repeat="row in displayCollection" ng-click="selectRow(row)">
And I manually unselect all row before selecting the clicked one:
scope.selectRow = function (row) {
for (var i = 0, len = scope.displayCollection.length; i < len; i += 1) {
scope.displayCollection[i].isSelected = false;
}
row.isSelected = true;
scope.selected = row;
}
Working JSFiddle: http://jsfiddle.net/6pykn5hu/6/
If someone has a better solution, I am open to other suggestions :)
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);}
I'm using AngularJS in our project and I find IE provides a clear icon on every input box by default. But when I click the 'X' icon the change event won't be fired.
Can someone kindly help me find a simple way to solve this issue?
It worked in plnkr, it's very weird...
$scope.$watch('model.search', function(search){
console.log(search);
}, true);
http://plnkr.co/edit/U8BMJtBnyK1oxviMV9aL?p=preview
I remove all the class and analytics in the input element, it still can not trigger the change...
Thanks in advance!
I would stop worrying about it by hiding this feature-
input::-ms-clear {
display: none;
}
That is just one more of countless non-standard browser features that Microsoft introduce that contain missing functionality.
Our time is too precious. Coding specifically for IE has been a pain for a decade...
I was able to solve this using the following directive, for those looking for an alternative to hiding the clear button. It may not be perfect yet, so I welcome any feedback :)
angular
.module('yourModuleName')
.directive('input', FixIEClearButton);
FixIEClearButton.$inject = ['$timeout', '$sniffer'];
function FixIEClearButton($timeout, $sniffer) {
var directive = {
restrict: 'E',
require: '?ngModel',
link: Link,
controller: function () { }
};
return directive;
function Link(scope, elem, attr, controller) {
var type = elem[0].type;
//ie11 doesn't seem to support the input event, at least according to angular
if (type !== 'text' || !controller || $sniffer.hasEvent('input')) {
return;
}
elem.on("mouseup", function (event) {
var oldValue = elem.val();
if (oldValue == "") {
return;
}
$timeout(function () {
var newValue = elem.val();
if (newValue !== oldValue) {
elem.val(oldValue);
elem.triggerHandler('keydown');
elem.val(newValue);
elem.triggerHandler('focus');
}
}, 0, false);
});
scope.$on('$destroy', destroy);
elem.on('$destroy', destroy);
function destroy() {
elem.off('mouseup');
}
}
}
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")
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.