AngularJS directive remove class in parent element - angularjs

I am using the following code to add / remove class "checked" to the radio input parent. It works perfectly when I use JQuery selector inside the directive but fails when I try to use the directive element, can someone please check my code and tell me why it is not working with element and how I can possibly add/ remove class checked to the radio input parent while using element instead of the jquery selectors? Thanks
.directive('disInpDir', function() {
return {
restrict: 'A',
scope: {
inpflag: '='
},
link: function(scope, element, attrs) {
element.bind('click', function(){
//This code will not work
if(element.parent().hasClass("checked")){
scope.$apply(function(){
element.parent().removeClass("checked");
element.parent().addClass("checked");
});
}else{
scope.$apply(function(){
element.parent().addClass("checked");
});
}
//This code works perfectly
$('input:not(:checked)').parent().removeClass("checked");
$('input:checked').parent().addClass("checked");
});
}
};
});
HTML:
<div class="inpwrap" for="image1">
<input type="radio" id="image1" name="radio1" value="" inpflag="imageLoaded" dis-inp-dir/>
</div>
<div class="inpwrap" for="image2">
<input type="radio" id="image2" name="radio1" value="" inpflag="imageLoaded" dis-inp-dir/>
</div>

Your code actually works for me in Plnkr (more or less):
http://plnkr.co/edit/vJJRYQQxH7u2bKSc27UA?p=preview
When you run this, the 'checked' class gets correctly added to the parent DIVs using only the first code you included. (I commented out the jQuery mechanism - I didn't add jQuery to this page, as a test.)
However, I think what you're trying to accomplish isn't working out because you're only capturing click events. The radio button that loses its checked attribute doesn't get a click event, only the next one does. In jQuery your selector is really broad - you're hitting every radio button, so it does what you want. But since you only trap click on the radio button that receives the click, it doesn't do what you want using the other pattern. checked gets added, but never removed.
A more Angular-ish pattern would be something like this:
http://plnkr.co/edit/HN7tLxkRA0jUL5GPjk5V?p=preview
link: function($scope) {
$scope.checked = false;
$scope.$watch('currentValue', function() {
$scope.checked = ($scope.currentValue === $scope.imgNumber);
});
$scope.setValue = function() {
$scope.currentValue = $scope.imgNumber;
};
}
What you see here lets Angular do all the dirty work, which is kind of the point. You can actually go a lot further than this - you could probably cut half the code out and do it all with expressions. The point is that in Angular, you really want to focus on the DATA (the model). You wire all of your behaviors and events up (controller) to things that manipulate that data, and then wire up all your DOM styles, classes, templates (view), etc. up to conditionals against that same data. And that is the point of MVC!

Related

Emoticons support for textarea or contenteditable div

Trying to implement a textarea component with emoticons support while writing.
I want to be able to backup the original text (ascii chars only) while presenting the filtered/generated html outcome (with an angular emoticons filter) on a div.
My initial solution is to
<textarea ng-model="text" ng-change="..." ng-focus="..."></textarea>
<div ng-bind-html="text | myEmoticonsFilter"></div>
but I'm having trouble getting to the part of using a hidden textarea. Also, with this I wouldn't be able to mouse-select text and delete or copy/paste safely.
I also thought of using a <div contenteditable="true"> but ng-focus and ng-change wouldn't be handled.
Does anyone have any sugestion on how to continue this?
Edit 1: here is a jsfiddle with an attempt on what I'm doing. Up until now, able to replace the first occurrence, but the behavior remains erratic since that. I'm using a contenteditable directive for 2-way data binding and to filter the emoticon pattern.
Edit 2: regarding my statement saying that ng-focus and ng-change wouldn't be handled, that is not true - ng-focus works natively on <div contenteditable="true"> and ng-change will work as long as a directive is declared using the ngModel and setting the appropriate $modelValue and $viewValue (an example is provided in the jsfiddle in Edit 1).
The only way to do this in a consistently cross-browser manner is to use a WYSIWYG field that converts emoji to images.
There's a jQuery plugin jquery-emojiarea that does what you need, so you'd just need to create a directive that wraps this plugin and you're off to the races. Since it inputs into a hidden textarea with emoji syntax :smile: angular should have no difficulty binding.
Here's a working directive I threw together. http://jsfiddle.net/dboskovic/g8x8xs2t/
var app = angular.module('app', []);
app.controller('BaseController', function ($scope) {
$scope.text = 'This is pretty awesome. :smile: :laughing:';
});
app.directive('emojiInput', function ($timeout) {
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, $el, $attr, ngModel) {
$.emojiarea.path = 'https://s3-us-west-1.amazonaws.com/dboskovic/jquery-emojiarea-master/packs/basic';
$.emojiarea.icons = {
':smile:': 'smile.png',
':angry:': 'angry.png',
':flushed:': 'flushed.png',
':neckbeard:': 'neckbeard.png',
':laughing:': 'laughing.png'
};
var options = $scope.$eval($attr.emojiInput);
var $wysiwyg = $($el[0]).emojiarea(options);
$wysiwyg.on('change', function () {
ngModel.$setViewValue($wysiwyg.val());
$scope.$apply();
});
ngModel.$formatters.push(function (data) {
// emojiarea doesn't have a proper destroy :( so we have to remove and rebuild
$wysiwyg.siblings('.emoji-wysiwyg-editor, .emoji-button').remove();
$timeout(function () {
$wysiwyg.emojiarea(options);
}, 0);
return data;
});
}
};
});
And usage:
<textarea ng-model="text" emoji-input="{buttonLabel:'Insert Emoji',wysiwyg:true}"></textarea>
If you want the editable field to convert text like :( as you type you'll need to fork that jquery plugin and modify it slightly to parse input text on change as well as on init. (like, a couple lines of code)

Setting attrs dynamically for ui-bootstrap tooltip / popover

I'm trying to programmatically toggle tooltips (like mentioned here: https://stackoverflow.com/a/23377441) and got it fully functional except for one issue. In order for it to work I must have tooltip-trigger and tooltip attributes hardcoded as follows:
<input type="text" tooltip-trigger="show" tooltip="" field1>
In my working directive, I'm able to change the tooltip attributes and trigger a tooltip, but if I try to leave those two attributes out and attempt to set them dynamically, ui-bootstrap doesn't pick them up and no tooltip gets displayed.
html
<input type="text" field2>
js
myApp.directive('field2', function($timeout) {
return {
scope: true,
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch('errors', function() {
var id = "field2";
if (scope.errors[id]) {
$timeout(function(){
// these attrs dont take effect...
attrs.$set('tooltip-trigger', 'show');
attrs.$set('tooltip-placement', 'top');
attrs.$set('tooltip', scope.errors[id]);
element.triggerHandler('show');
});
element.bind("click", function(e){
element.triggerHandler('hide');
});
}
});
},
};
});
I'd prefer not to hardcode these attributes in the html, so how do I go about setting these attributes dynamically and get ui-bootstrap to pick them up?
Here is a plunker that has a working (field1) and non working (field2) directive: http://plnkr.co/edit/mP0JD8KHt4ZR3n0vF46e
You can do this, but you have to change a couple of things in your approach.
Plunker Demo
Directive
app.directive("errorTooltip", function($compile, $interpolate, $timeout) {
return {
scope: true,
link: function($scope, $element, $attrs) {
var errorObj = $attrs.errorTooltip;
var inputName = $attrs.name;
var startSym = $interpolate.startSymbol();
var endSym = $interpolate.endSymbol();
var content = startSym+errorObj+'.'+inputName+endSym;
$element.attr('tooltip-trigger', 'show');
$element.attr('tooltip-placement', 'top');
$element.attr('tooltip', content);
$element.removeAttr('error-tooltip');
$compile($element)($scope);
$scope.$watch(errorObj, function() {
$timeout(function(){
$element.triggerHandler('show');
});
}, true);
$element.on('click', function(e){
$element.triggerHandler('hide');
});
}
};
});
The super long detailed explanation:
Okay, so from the top: #Travis is correct in that you can't just inject the attributes after the fact. The tooltip attributes that you place on the element are directives themselves, so the tooltip needs to be compiled when it's appended. That's not a problem, you can use the $compile service to do this, but you need to do it just once for the element.
Also, you need to bind the tooltip text (the value given to the tooltip attribute) to an expression. I do that by passing in a concatenated value of $interpolate.startSymbol() + the scope value that you want to display (in the demo it is the fieldx property of the errors object) + the $interpolate.endSymbol(). This basically evaluates to something like: {{error.field1}}. I use the $interpolate service start and end symbols because it just makes the directive more componentized, so you can use it on other projects where you might have multiple frameworks and be using something other than double curly-braces for your Angular expressions. It's not necessary though and you could instead do: '{{'+errorObj+'.'+inputName+'}}'. In this case, you don't have to add the $interpolate service as a dependency.
As you can see, to make the directive truly reuseable, rather than hard-coding the error field, I set the value given to the directive attribute to the name of the object that will be watched and use the input name value as the object property.
The chief thing you need to remember is that before you compile, you have to remove the error-tooltip attribute from the element because if you don't you'll wind up in an infinite loop and crash hard! Basically, the compile service is going to take the element that the directive is attached to and compile it with all of the attributes your directive added, if you leave the error-tooltip attribute, it's going to try and recompile that directive too.
Lastly, you can take advantage of the fact that the tooltip will not display if its text value is empty or undefined (see line 192). That means you only have to watch the errors object not the individual property on the error associated with the tooltip. Make sure that you set the equality operator on the $watch to true, so that it will trigger if any of the object's properties are changed:
$scope.$watch('errors', function() {
$timeout(function(){
$element.triggerHandler('show');
});
}, true); //<--equality operator
In the demo, you can see the effect of changing the errors object. If you click the Set Errors button the tooltip will display for both the first and second inputs. Click the Change Error Values and the tooltip displays for the first and third inputs.
TL;DR:
Add the directive to your markup by setting the value to be the name of the object that will contain all of the errors. Make sure to give the field a name attribute that corresponds to the property key name in the object that will contain the errors for that input, such as:
<input class="form-control" ng-model="demo.field1" name="field1" error-tooltip="errors" />

ng-click in directive doesn't work in IE but works in Chrome

I'm not sure if this is a known bug (or even a bug...) or not but I can't find it on the angular issues list, maybe I'm doing something wrong:
.directive('clickTest', function () {
return {
transclude: true,
template: '<div ng-click="click()" style="width:200px; height:200px;">Click me<div ng-transclude></div></div> Clicked {{clicked}} times',
controller: function($scope, $element){
$scope.clicked = 0;
$scope.click = function() {
$scope.clicked++;
}
}
}
So I've essentially added the directive to an element on my page. The directive has an ng-click handler in the template and transcludes the existing elements. The ng-click runs the click() function which should trigger the click on the current scope.
In Chrome when I click the button the counter goes up by 1.
If I do the same in IE11 nothing happens. I've been trying to get this working for a while as most of my target audience will be using IE
I think that this should work - as far as I can tell, angular should compile the template after transclusion to identify any further directives it needs to work with
Am I doing something wrong or should this click work?
Knew it would be something I was doing wrong - as commenters suggested it's because I tried to add the directive to a <button> element.
I wrapped the element in a <div> instead and applied the directive to that and it works!

Can I delay setting of ng-model until input has been blurred?

I have an angularjs application that does a bunch of calculations based on inputs from textboxes. I have it working pretty well, but if the user selects the numbers in the input box and deletes them, the result that's based on that number is immediately changed to undefined. In this case, angularjs is TOO fast. From a UX standpoint, I would prefer that the user is free to edit the textbox and only after they have blurred that box will the calculations update.
Is there way to make this happen right on an input field like this:
<input type="text" ng-model="model" custom-magical-directive>
I know I can create my own directive with an isolated scope and only update the parent model when I'm ready, but I'd prefer to keep it clean because if I end up with something like this:
<div custom-isolated-directive ng-model="model">
<input type="text" ng-model="isolatedModel">
</div>
styling is going to be a challenge.
If you don't want the immediate two way binding provided by ng-model, you can leave it out and use a directive to update the model when you want to:
<input type="text" update-on-blur="data.name" />
directive:
app.directive('updateOnBlur', function(){
return {
restrict: 'A',
scope: {
updateOnBlur: '='
},
link: function(scope, element, attr) {
scope.$watch('updateOnBlur', function(newVal, oldVal) {
element.val(newVal);
});
element.on('blur', function() {
scope.updateOnBlur = element.val();
scope.$apply();
});
}
}
})
Here is a demo: http://plnkr.co/dF9JbfPkgRQxWmGp57ap
A $watch function will make sure that programmatic changes are visible in the input. Then, an event handler updates the model on blur.

Is it possible mask input using a different placeholder?

Since my form has no labels, I would like to be able to use a different placeholder with Angular and Angular-UI-Utils-Mask:
<div ng-controller="myController">
<input type="text"
ng-model="date"
ui-mask="99/99/9999"
placeholder="Birth Date"/>
</div>
Using jquery-inputmask it works like a charm, but I had too many problems to make it work with Angular so I'm now trying to go Angular way, but Angular shows my input as:
Bi/th/Date
Here's a fiddle to show it: http://jsfiddle.net/XS4R6/
I also saw some people talking about ´ui-mask-placeholder´, but it does nothing.
Is there a way to accomplish this?
EDIT
To clarify, I think it's just fine to use just placeholders since you also use titles (hint) so people always know what are they supposed to type in those inputs:
The input showing __.___.___ is the one I'm using Angular UI Mask.
JQuery Inputmask works very fine, since it shows the 'name' placeholder and as soon as I mouse over or click the input it shows the mask.
I'm thinking you can use another tag to simulate placeholder, maybe the code here is not very good, but I just provide another thought.
app.directive("myPlaceholder", ['$compile', function($compile){
return {
restrict: 'A',
link: function(scope, elem, attr) {
attr.$observe('myPlaceholder', initialize);
var mask = '__/__/____';
function initialize(value) {
// label is not clickable in IE, that's the reason why we use span tag
var fakePlaceholder = angular.element('<span class="placeholder">' + value + '</span>');
// click placeholder to focus the input
fakePlaceholder.on('click', function(){
elem.focus();
});
elem.before(fakePlaceholder);
$compile(fakePlaceholder)(scope);
elem.on('focus', function() {
fakePlaceholder.hide();
}).on('blur', function() {
if (elem.val() === mask) {
fakePlaceholder.show();
}
});
}
}
};
}]);
demo on http://jsfiddle.net/XS4R6/19/ (jQuery required)

Resources