Using angularjs, I'm showing a 2-level list like this
- first main item
- first subitem of the first main item
- second subitem of the first main item
- AN EMPTY ITEM AS PLACEHOLDER TO ENTER THE NEXT SUBITEM
- second main item
- first subitem of the second main item
- second subitem of the second main item
- AN EMPTY ITEM AS PLACEHOLDER TO ENTER THE NEXT SUBITEM
In order to save place, I'd like to show the PLACEHOLDER only if anything in the corresponding div has focus, so that there's only one such placeholder. I know that there's ngFocus, but I'd prefer something simpler than creating tons of event handlers. Maybe something like this :
<div ng-focus-model="mainItem.hasFocus" ng-repeat="mainItem in list">
... main item line
... all subitems
</div>
A unidirectional binding would be sufficient as I don't need to set the focus.
The problem here is the following; we want to avoid adding event listener to each and every child, but add it only to the parent. The parent will be responsible for taking the appropriate action. The general solution to this, is to use even propagation (delegation). We attach only one listener to the parent, when an event occurs on the child (focus on input element in this example), it will bubble up to the parent and the parent will execute the listener.
Here's the directive:
app.directive('ngFocusModel', function () {
return function (scope, element) {
var focusListener = function () {
scope.hasFocus = true;
scope.$digest();
};
var blurListener = function () {
scope.hasFocus = false;
scope.$digest();
};
element[0].addEventListener('focus', focusListener, true);
element[0].addEventListener('blur', blurListener, true);
};
});
The directive listens for events and accordingly sets the value on scope, so we can make conditional changes.
There are several things to notice here.
focus and blur events don't "bubble", we need to use "event capturing" to catch them. That's why element.on('focus/blur') is not used (it doesn't allow for capture, afaik) but an addEventListener method. This method allows us to specify if the listener will be executed on "event bubbling" or "event capturing" by setting the third argument to false or true accordingly.
We could have used focusin and focusout events which "bubble", unfortunatelly these aren't supported in Firefox (focusin and focusout).
Here's a plunker with the implementation.
Update:
It occurred to me that this can be done with pure CSS using the :focus pseudo-class, the only downside is that the placeholder needs to be in proper position (sibling) relative to the input elements. See codepen.
Unfortunately the only rock solid way to do what you want is to respond to the focus\blur events on the inputs...that's the only way to get notified.
You could put a hidden input as the first element in each div and put the NgFocus attribute on it but that only works if a user tabs into it.
DEMO
I created a small directive that can be used for what you need:
app.directive('childFocus', function($window){
var registered = [];
// observing focus events in single place
$window.addEventListener('focus', function(event){
registered.forEach(function(element){
if(element.contains(event.target)){
// if element with focus is a descendant of the
// element with our directive then action is triggered
element._scope.$apply(element._scope.action);
}
});
}, true)
return {
scope : {
action : '&childFocus' // you can pass whatever expression here
},
link : function(scope, element){
// keep track ref to scope object
element[0]._scope = scope;
// (probably better would be to register
// scope with attached element)
registered.push(element[0]);
scope.$on('destroy', function(){
registered.splice(registered.indexOf(element[0]),1);
});
}
}
});
You could use the focus event of the '.parent *' selector to capture any focus events, then loop through each of the parent DIVs and use the :focus JQuery selector to check for child elements with focus, then add a class to the parent DIV and use that class to show/hide the placholder (see this jsfiddle):
$(function(){
$('.parent *').focus(function(){
$('.selected').removeClass('selected');
$('.parent').each(function(index, el){
(function($el){
setTimeout(function(){
console.log($el.attr('id'));
if($el.find(':focus').length){
$el.addClass('selected');
}
});
})($(el));
});
});
});
.parent{
padding:1rem;
margin:1rem;
border:solid 1px green;
}
.selected{
border:solid 1px red;
}
.parent .placeholder{
display:none;
}
.parent.selected .placeholder{
display:block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class='parent' id='div1'>
<input type="text" />
<div class='placeholder'>Placeholder</div>
</div>
<div class='parent' id='div2'>
<input type="text" />
<div class='placeholder'>Placeholder</div>
</div>
<div class='parent' id='div3'>
<input type="text" />
<div class='placeholder'>Placeholder</div>
</div>
<div class='parent' id='div4'>
<input type="text" />
<div class='placeholder'>Placeholder</div>
</div>
Related
I have built a custom directive to enable arrow key navigation in a dropdown.
This is my HTML code
<div ng-click="dropdownShow = !dropdownShow" id="dropdownToggle" arrow-navigation>
{{currentlySelectedItem}}
</div>
<div ng-show="dropdownShow">
<div ng-repeat="item in list" id="row_{{$index}}" ng-click="getItemInfo($index)">
<span>{{item}}</span>
</div>
</div>
And my JS code
app.directive('arrowNavigation', ['$document', function($document){
return{
restrict: 'A',
link: function(scope, element, attrs){
$document.bind('keydown',function(e){
// check if dropdown open
if(scope.dropdownShow){
// if down arrow key pressed
if(e.keyCode === 40){
console.log("down arrow key pressed");
// When the dropdown is first opened, the focus will be on the dropdownToggle.
// In this case, I'm moving the focus to the first item on the list.
if(document.activeElement.id === "dropdownToggle"){
document.getElementById('row_0').focus();
}
else{
let currentFocused = document.activeElement.id;
// currentFocused = row_ + $index
let index = currentFocused.substring(4);
// index = $index of currently focused item
console.log(index);
index++;
// check if the currently focused item is the last item on the list
// In this case, move the focus back to the first item on the list
if(index >= scope.list.length){
document.getElementById('row_0').focus();
}
else{
document.getElementById('row_' + index).focus();
}
}
e.preventDefault();
}
// there's similar code for up arrow key press. I have decided to skip it for the sake of simplicity.
}
})
}
}
}])
The first time I use the dropdown, everything works perfectly.
But when I select any item from the dropdown, the resulting ng-click function has a $route.reload inside it. This causes my ng-view to get reloaded. That's when the problem starts. After the first reload, when I try to use the dropdown, it gets executed twice for every single arrow click. So if the first list item is focused, and I press the down arrow key, instead of moving the focus to the second item, it moves the focus to the third item. Upon every subsequent $route.reload(), the number of executions increases by one.
I'm guessing that this is happening cause everytime the route gets reloaded, the directive is being re-rendered, causing multiple copies of the same directive, all of which then get executed on the arrow click.
Is there any way to prevent this re-rendering?
Remove the event listener when scope is destroyed
// inside link
scope.$on("$destroy", function() {
$document.unbind('keydown')
});
Note that angular.element bind and unbind are deprecated so am assuming you may be using a fairly old version. Most recent versions use on() and off()
Is there an easy way to enable hitting enter to execute some javascript for a form with paper-input's. I can catch the keystroke on enter for every element but this seems kind of tedious.
With the current Polymer version 1.0 I was able to resolve that using iron-a11y-keys.
Here is an example bound to the whole form which triggers submission on any child input element:
<iron-a11y-keys id="a11y" target="[[_form]]" keys="enter"
on-keys-pressed="submitForm"></iron-a11y-keys>
<form is="iron-form" id="form"
method="post"
action="{{url}}">
...
Polymer({
is: 'example-form',
properties: {
_form: {
type: Object,
value: function() {
return this.$.form;
}
}
},
submitForm: function() {
document.getElementById('form').submit();
},
Currently (Polymer 0.3.4) there seems to be no event fired when one presses the enter key in a paper-input. But you can extend the paper-input element and add this functionality (see Extending other elements in the Polymer doc):
<polymer-element name="my-paper-input" extends="paper-input">
<template>
<shadow></shadow>
</template>
...
</polymer-element>
Then you can fire a custom event when the return key is pressed:
ready: function() {
self = this;
this.$.input.addEventListener('keypress', function(e) {
if (e.keyCode == 13) {
self.async(function() {
self.fire('enter', self.value);
});
}
});
}
For convenience the input value is passed to the event handler. Now you can use your new element like so:
<my-paper-input ... on-enter="{{inputEntered}}"></my-paper-input>
Edit 1:
Since the event bubbles up in the element hierarchy, one can catch it on the surrounding form element:
<my-form on-enter="{{anyInputEntered}}" ...>
Then one gets the events of all input elements in one place (the event propagation can be stopped by calling stopPropagation(); on the event object).
Edit 2:
It's best to give custom events unique names, so that they don't clash with the names of core events that may be added in the future (e.g. my-unique-prefix-input-entered).
I have two ng-grids in the same view/partial and need to identify on which ng-grid was the sort event fired. How do I do that? I am using the ngGridEventSorted event.
You can access the scope on which the event was fired with event.targetScope
$scope.$on('ngGridEventSorted', function(event, args) {
var targetScope = event.targetScope;
// inspect targetScope's properties to differentiate between the two grids
});
Another way would be to create two wrapping div's around the grids, each with their own controller that handles the event.
<div ng-controller="controllerOne">
<ng-grid ...>
</div>
<div ng-controller="controllerTwo">
<ng-grid ...>
</div>
That what you need:
$scope.$on('ngGridEventSorted', function(event,data) {
if ($scope.gridOptions1.gridId==event.targetScope.gridId){
...
}
if ($scope.gridOptions2.gridId==event.targetScope.gridId){
...
}
});
And no need to define gridId.
Why is my click event fired twice in jquery?
HTML
<ul class=submenu>
<li><label for=toggle><input id=toggle type=checkbox checked>Show</label></li>
</ul>
Javascript
$("ul.submenu li:contains('Show')").on("click", function(e) {
console.log("toggle");
if ($(this).find("[type=checkbox]").is(":checked")) console.log("Show");
else console.log("Hide");
});
This is what I get in console:
toggle menu.js:39
Show menu.js:40
toggle menu.js:39
Hide menu.js:41
> $("ul.submenu li:contains('Show')")
[<li> ]
<label for="toggle">
<input id="toggle" type="checkbox" checked>
"Show"
</label>
</li>
If I remember correctly, I've seen this behavior on at least some browsers, where clicking the label both triggers a click on the label and on the input.
So if you ignore the events where e.target.tagName is "LABEL", you'll just get the one event. At least, that's what I get in my tests:
Example with both events | Source
Example filtering out the e.target.tagName = "LABEL" ones | Source
I recommend you use the change event on the input[type="checkbox"] which will only be triggered once. So as a solution to the above problem you might do the following:
$("#toggle").on("change", function(e) {
if ($(this).is(":checked"))
console.log("toggle: Show");
else
console.log("toggle: Hide");
});
https://jsfiddle.net/ssrboq3w/
The vanilla JS version using querySelector which isn't compatible with older versions of IE:
document.querySelector('#toggle').addEventListener('change',function(){
if(this.checked)
console.log('toggle: Show');
else
console.log('toggle: Hide');
});
https://jsfiddle.net/rp6vsyh6/
This behavior occurs when the input tag is structured within the label tag:
<label for="toggle"><input id="toggle" type="checkbox" checked>Show</label>
If the input checkbox is placed outside label, with the use of the id and for attributes, the multiple firing of the click event will not occur:
<label for="toggle">Show</label>
<input id="toggle" type="checkbox" checked>
I found that when I had the click (or change) event defined in a location in the code that was called multiple times, this issue occurred. Move definition to click event to document ready and you should be all set.
Not sure why this wasn't mentioned. But if:
You don't want to move the input outside of the label (possibly because you don't want to alter the HTML).
Checking by e.target.tagName or even e.target doesn't work for
you because you have other elements inside the label
(in my case it had spans holding an SVG with a path so e.target.tagName sometimes showed SVG and other times it showed PATH).
You want the click handler to stay on the li (possibly because you have
other items in the li besides the checkbox).
Then this should do the trick nicely.
$('label').on('click', function(e) {
e.stopPropagation();
});
$('#toggle').on('click', function(e) {
e.stopPropagation();
$(this).closest('li').trigger('click');
});
Then you can write your own li click handler without worrying about events being triggered twice. Personally, I prefer to use a data-selected attribute that changes from false to true and vice versa each time the li is clicked instead of relying on the input's value:
$('ul.submenu li').on('click', function() {
let _li = $(this),
ticked = _li.attr('data-selected');
ticked = (ticked === 'false') ? true : false;
_li.attr('data-selected', ticked);
_li.find('#toggle').prop('checked', ticked);
});
EDIT: for those of you who don't want to go through the code, I'm basically passing the form a "node" object with node.selectedAnswer = "4,1,4" or some string like that. The form has radio buttons and one of the buttons has a value "4,1,4". the radio button also has ng-checked="node.selectedAnswer" expression. But that doesn't work. I know for sure that node.selectedAnswer has the appropriate value.
I have a series of radio button questions that I'm asking the user. I want them to be able to go previous and next. I'm using a stack to store the data retrieved from ajax call, as well as selectedAnswer when they select an option and click next. I've commented the code itself to explain the situation where I can. Everything seems to be working, except ng-checked is just not picking up node.selectedAnswer, even though I can output {{node.selectedAnswer}} properly to the page.
<div class="container-fluid" ng-app="AccountRequest" ng-controller="GameNode" ng-init="outside={}">
<div class="row-fluid">
<div class="span2"></div>
<div class="span10">
<form>
<!-- node.selectedAnswer displays the selectedAnswer correctly when clicking previous and going back.
However, ng-checked is somehow not selecting the appropriate radio button. -->
<span>{{node.Question.Text}} selected answer: {{node.selectedAnswer}}</span>
<div class="radio" ng-repeat="answer in node.Answers">
<input type="radio" id="answerGroup" name="answerGroup" ng-checked="node.selectedAnswer" ng-model="outside.selectedAnswer"
value="{{answer.BranchId}},{{node.LeafId}},{{answer.Id}}"/> {{answer.Text}}
</div>
<div>
<input type="button" ng-click="previous()" value="Previous"/>
<input type="button" ng-click="next(outside.selectedAnswer)" value="Next"/>
</div>
</form>
</div>
</div>
</div>
//below is the script
app.controller('GameNode', function ($scope, $http) {
var nodes = [];
function load(branchId, leafId, answerId) {
$http.get("/AccountRequest/GetNode?branchId=" + branchId +
"&leafId=" + leafId +
"&answerId=" + answerId)
.success(function (data) {
//get data and push it in the stack
nodes.push(data);
$scope.node = data;
});
}
function populateValues(selectedAnswer) {
var answer = null;
if (selectedAnswer === undefined || selectedAnswer == null)
selectedAnswer = "0,0,0";
//when next is clicked, retrieve the selectedAnswer from form and store it in current node as a property.
if (nodes.length > 0) {
var curNode = nodes.pop();
curNode.selectedAnswer = selectedAnswer;
nodes.push(curNode);
}
answer = selectedAnswer.split(',');
if (answer != null) {
load(answer[0], answer[1], answer[2]);
}
}
$scope.next = populateValues;
$scope.previous = function () {
//when previous is clicked, pop the current node out and throw it away.
//then pop the previous node out, read it, and push it back in as current node.
if (nodes.length > 1) {
nodes.pop();
var prevNode = nodes.pop();
nodes.push(prevNode);
$scope.node = prevNode;
}
};
populateValues();
});
Older Answer - This works, (was marked correct) but using $parent can get a bit messy in nested repeats.
In this instance, you don't need to use ng-checked at all. Since this is a radio group, the checked attribute will be bound to the model. If the model is bound to the value of an individual radio button, then your ability to change which button is "checked" becomes very simple.
Here is a plunk that demonstrates the concept.
So in your case a few changes need to be made.
1. Get rid of 'id' attribute - the ID must be unique for each element.
2. Each item created in an ng-repeat creates its own child scope. So to access the original model, "$parent" must be invoked.
<input type="radio" name="answerGroup" ng-model="$parent.someAnswerAttribute"
value="{{answer.BranchId}},{{node.LeafId}},{{answer.Id}}"/>
In your controller define the model as you already did, then modify it to be tied to a value of a button, which in your case will be a bit lengthy, since you have multiple attributes within your value.
$scope.someAnswerAttribute = // exactly what the value of a radio button would be.
Again, the plunker above reflects this concept. Hope this helps!
..
..
Edit - Better Answer:
Since the ng-repeat creates its own child scope, and two-way binding is necessary, the ng-model should be referencing an object instead of a primitive. In other words, if the model was $scope.myModel="Biff", the child scope can not access that without invoking $parent (in the answer below). However, if the model is referencing a property of an object, the child will receive prototype inheritance of that object. (I think I said that right).
So using the older answer example, we can change:
From this in the parent controller:
$scope.someAnswerAttribute = "Biff";
To this in the parent controller:
$scope.someAnswerAttribute = {value: "Biff"}
And in the radio group:
<input type="radio" name="answerGroup" ng-model="someAnswerAttribue.value"
value="{{answer.BranchId}},{{node.LeafId}},{{answer.Id}}"/>
This plunk is forked from the older answer and demonstrates model as an object property.