I have an interesting problem with the uiSref directive and I haven't been able to find a solution (well an elegant one anyway) anywhere on the web. Basically I have a requirement from a client to be able to click a row in a table of resources and go to the editing view for that resource. Normally the uiSref directive works beautifully, but the problem resides in the fact that I have a Bootstrap dropdown in the last <td> of the table with a bunch of quick actions in it. The HTML looks something like this:
<table class="table table-bordedered table-hover">
<thead>
<tr>
<td>Name</td>
<td>Actions</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="resource in resources" ui-sref="edit({id: resource.id})">
<td ng-bind="resource.name"></td>
<td class="actions-column">
<div class="btn btn-xs btn-default" data-toggle="dropdown">
<i class="fa fa-cog"></i>
</div>
<ul class="dropdown-menu pull-right">
<li>
SOMETHING CRAZY
</li>
</ul>
</td>
</tr>
</tbody>
</table>
The problem is that when I click on the button in the actions column, the uiSref overrides the default action of the dropdown and takes me to the edit page. Now you might be asking yourself "well that's easy, why can't you just stop the propagation of the event!?"... doesn't work. When I add this to the actions column:
<td class="actions-column" ng-click="$event.stopPropagation()">
It kills the functionality of the dropdown menu and nothing shows up. Right now I have a workaround in place where I define an ngClick on the <tr> element that then deciphers where the state should go depending on the element clicked like so:
<tr ng-repeat="resource in resources" ng-click="goToEdit(resource, $event)">
And The JS looks like this:
scope.goToEdit = function(resource, event) {
// if the event.target has parent '.actions-column' or is that column, do nothing else
// invoke $state.go('edit', {id: resource.id})
}
I hate it though and I have a lot of list views like this. All I'm looking for is an elegant and portable solution that hopefully works natively through UI Router like $event.stopPropagation() (Although I've poked through the UI Router source and can't seem to find a workable alternative). Basically I want to have my cake and eat it too. Anyway, it'll be interesting to see what the SO community can come up with or if what I'm asking for is not currently possible. Thanks!
I got it! While looking through the UI Router source some more, it appears that the click event will be ignored if the target attribute is populated on the element that the uiSref resides on. It may not be the most beautiful thing in the world, but it sure is easier than what I was doing before.
NOTE: This only works if you're using the whole jQuery library, not jQLite
So I wrote this directive:
app.directive('uiSrefIgnore', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
elem.on('click', function(e) {
// Find the ui sref parent
var uiSref = elem.parents('[ui-sref]').first();
// Set the target attribute so that the click event is ignored
uiSref.attr({
target: 'true'
});
// Function to remove the target attribute pushed to the bottom
// of the event loop. This allows for a digest cycle to be run
// and the uiSref element will be evaluated while the attribute
// is populated
setTimeout(function() {
uiSref.attr({
target: null
});
}, 0);
});
}
};
});
That way, whenever I want to ignore the javascript event for just the uiSref directive, I can just add this to the html:
<tr ui-sref="edit">
<!-- any other elements -->
<td ui-sref-ignore>The element I care about</td>
</tr>
BOOM! Let me know what you guys think about the implications of this.
Related
I'm updating the table via an ajax call, and want to display a spin.js spinner while the row.status == pending.
Basically I have a row fragment that successfully toggles from
<div class="spinner">
to
<div class="hide">
as the row computation progresses, (via ajax within a controller).
I'm happy with any mechanism that works!
What I'm struggling to do is, when the value is
<div class="spinner">
have a spinner showing, otherwise have it hidden.
<table class="table table-hover">
<thead>
<tr>
<th>url</th>
<th>status</th>
</tr>
</thead>
<tbody>
<tr data-ng-repeat="row in result.results">
<td>{{row.link.url}}</td>
<td> <div loadingWidget class="{{(row.status == 'pending' ? 'spinner' : 'hide' ) }}"></div>{{row.status}}</td>
</tr>
</tbody>
</table>
My latest script - basically I don't really know what I'm doing,
app.directive('loadingWidget', function ($rootScope) {
return {
restrict: 'E',
scope: {
field: '=',
attributes: '=',
editMode: '='
},
link: function (scope, element, attrs) {
scope.spinit = function() {
$rootScope.$broadcast('spinit');
}
}
};
});
$scope.$on('spinit', function(element){
//react to event
new Spinner().spin(element);
});
You should use the ngHide directive. I'm having difficulty determining what you want your show/hide condition to be though. The rule is that if the contents of ng-hide evaluate to true, the element will be set to hidden.
If you absolutely need to switch the active class, not just hide the element, user ng-class. That will set the class based on a variable in scope.
<table class="table table-hover">
<thead>
<tr>
<th>url</th>
<th>status</th>
</tr>
</thead>
<tbody>
<tr data-ng-repeat="row in result.results">
<td>{{row.link.url}}</td>
<td>
<div loadingWidget class="spinner" ng-hide="row.status"></div>
{{row.status}}
</td>
</tr>
</tbody>
</table>
Also, using $broadcast on root scope can be incredibly expensive, and is almost never necessary. In your case I don't see why you can't just call the Spinner().spin method directly.
ngHide docs: https://docs.angularjs.org/api/ng/directive/ngHide
ngClass docs: https://docs.angularjs.org/api/ng/directive/ngClass
Edit:
Okay, so I went through your code more thoroughly. You have several issues here.
First, you directives are kebab case in html, camelcase in javascript. That means your directive should look like: <div loading-widget class="spinner" ng-hide="row.status">.
Also, you have restrict E on your directive attributes. This means it can only be used as an element, and you are using it as an attribute. To fix this, either remove the restrict option or change it to restrict: 'A'.
You don't need the scope on your loadingWidget directive at all, and honestly any time you include Root Scope you are probably doing something wrong.
All of this results in a directive that looks something like this:
app.directive('loadingWidget', function() {
return {
restrict: 'A',
scope: {
field: '=',
attributes: '=',
editMode: '='
},
link: function (scope, element, attrs) {
console.log("elm: ", element[0]);
new Spinner().spin(element[0]);
}
};
});
Plunkr Link
Also, there are already wrapper modules for Spin.js in Angular, so if you don't mind more dependencies that is a good option (Link).
In terms of the logic for hiding the spinner, I would simply use an ngShow with the value of whatever will be in that cell. When you go to update the value, set it to '' or false or undefined. On page load or when it gets changed to one of the values I just mentioned, the spinner will be hidden. Once the value is set, it will no longer be falsy and the element will show. This works as long as you never expect to store false in the variable. In Angular, don't use events unless you really need to, just manipulate scope variables and wait for the digest cycle. Having some simple state variables and then a combination of ngShow and ngHide can make simple state changes in a view easy.
A core value of getting comfortable with writing good Angular code is knowing that you basically never interact with your application outside of scope or directives. Avoid element selectors, and avoid events unless you are sure you need them. Much of Angular was designed to avoid the JQuery like tools, because they can make your code really hard to read.
I'm teaching myself angular and working through the O'Reilly AngularJS book. There's an email app example and I want to add a button "Mark as Read" that will apply a class to the row when the button is clicked.
After doing some research I found solutions applying conditional classes based on using $index to select a row, but that is toggling the class from the other elements when I click the button. You can see what I'm talking about here in this Plunker.
Here's the HTML in my view:
<table>
<tr>
<td><strong>Sender</strong></td>
<td><strong>Subject</strong></td>
<td><strong>Date</strong></td>
</tr>
<tr ng-repeat='message in messages' ng-click='readMessage($index)'
ng-class='{markRead: $index==selectedRow}'>
<td>{{message.sender}}</td>
<td><a ng-href='#/view/{{message.id}}'>{{message.subject}}</a></td>
<td>{{message.date}}</td>
<td><button ng-click="remove(message)">Delete</button></td>
<td><button ng-click="markRead()">Mark as Read</button></td>
</tr>
</table>
and here is what I have added to the controller to apply the class on click:
$scope.readMessage = function(row) {
$scope.selectedRow = row;
};
My goal is to have the ability to apply the class to any and all elements in the array, as the user desires (not just one at a time).
Can someone please help me understand why this isn't working and how I can achieve my goal? All the questions/answers I can find on SO are about toggling classes or setting an active class, which is the opposite of what I'm trying to accomplish.
Thank you!
The function readMessage could flag the message as read, and ng-class set the css class if the flag is set:
Controller:
$scope.readMessage = function(message) {
message.read = true;
};
Template:
ng-class='{markRead: message.read}'
See plunker
Let's say I have a row that looks like
<tr>
<td>John</td>
<td>Smith</td>
<td>Approved?<input type="checkbox"/></td>
</tr>
Each row shows one employee, and allows you to check to approve/disapprove the employee (e.g. for registration for a course). I want the user to be able to click anywhere on the row to get greater detail about the employee, but if they click on the last column ("Approved?") it should not go to greater detail, since it should just change the checkbox.
Here are the solutions I know of, none is great:
Entire row: <tr class="clickable" ng-click="go()">. Makes all the cells and margins clickable, and only requires one ng-click entry, but then the checkbox causes "go()" to execute, which is bad.
Each cell: <td class="clickable" ng-click="go()">...<td class="clickable" ng-click="go()">. Pro: can restrict to just the cells I want. Con: lots of repetition (not DRY), and misses the margins.
Entire row with special "go" fn: <tr class="clickable" ng-click="go()">, but "go" knows how to differentiate between different cells. Pro: Has exactly the effect. Con: requires lots of view/html knowledge in a specialized controller action.
How can I make the first 2 columns and their margins clickable, but not the 3rd or its margins?
You can use two click handlers, one for a whole tr , second for a last td' checkbox. In the second use Event's stopPropagation method.
Controller:
var TableCtrl = function($scope){
$scope.click1 = function(){
console.log("Click 1 method")
}
$scope.click2 = function(e){
console.log("Click 2 method");
e.stopPropagation();
}
}
Markup:
<table ng-controller="TableCtrl">
<tr ng-click="click1()">
<td>John</td> <td>Smith</td>
<td>Approved?<input type="checkbox" ng-click="click2($event)"/></td>
</tr>
</table>
Working example: http://jsfiddle.net/zDMQB/1/
How about this?
<tr>
<td style="cursor: pointer;" ng-click="alert('hello1')">John</td>
<td style="cursor: pointer;" ng-click="alert('hello2')">Smith</td>
<td>Approved?<input type="checkbox"/></td>
</tr>
I'm new to AngularJS, and while playing with it, i have encountered with the problem.... I have a table like this :
...
<tr ng-repeat="line in lines">
<remove-line>
<input id="line_id" type="hidden" value="{{line.id}}">
<td id="sn">{{$index+1}}</td>
<td>{{line.ref}}</td>
<td>{{line.label}}</td>
<td class="tva">{{line.tva}}</td>
<td class="qty">{{line.qty}}</td>
<td class="unity">{{line.unity}}</td>
<td class="prix">{{line.prix}}</td>
<td>{{line.prix*line.qty}}</td>
<td><button class="btn" id= "remove"><i class="icon-remove"></i> </button></td>
</remove-line>
</tr>
...
I want to have some behavior when clicking on the remove button, using custom directive. AngularJS code looks like :
angular.module('myApp', []).directive('removeLine',function(){
var remove = function(){
...
alert ("Oops removed!");
}
return {
restrict: 'E',
link : function(scope,element, attrs){
$("#remove").on('click', remove);
}
};
});
But this doesn't work.... Nothing happens when i click on the remove button in the table..... But all this work fine when the button is out of the tag. Why is it so and how make it work?
I have created jsfiddle to illustrate my situation http://jsfiddle.net/alexrussinov/cs8RP/71/. What is strange that when I test this code on my local machine buttons in the table don't work but two separate under it, work fine. On jsfiddle, neither in the table nor below it, it doesn't work.
You can't have multiple elements on a page with the same ID, which is what you're doing with the button.
I think the simplest solution would be to stick the remove() call directly in the button tag with ng-click, passing in the line that you wish to remove. This of course assumes that your remove() method is part of $scope.
<button class="btn" ng-click="remove(line)"><i class="icon-remove"></i></button>
I am using underscore.js's templating capabilities from backbone.js, I have the following template that I define in my page like this:
<script type="text/template" id="businessunit_template">
<tr data-uid="{{Uid}}">
<td class="first"><span>{{Name}}</span></td>
<td class="{{StatusClass}} tac">{{OverallScore}}%</td>
<td>
<a class="impactanalysis individualBu" href="#"> </a>
</td>
</tr>
</script>
I am attaching the trs to the tbody element of following table:
<table class="listing">
<thead>
<tr>
<th class="first">Business Units</th>
<th>BCMS<br />Status</th>
<th>View</th>
</tr>
</thead>
<tbody id="reportBusinessUnits"></tbody>
</table>
My individual backbone view that renders the tr looks like this:
class ReportBusinessUnitView extends MIBaseView
initialize: (options) ->
#vent = options.vent
#template = _.template($('#businessunit_template').html())
events:
"click .individualBu": "showBusinessUnitDetail"
showBusinessUnitDetail: (e) =>
e.preventDefault()
self = #
#vent.trigger('management:showbusinessunitdeail', #model)
render: =>
$(#el).html(#template(#model.toJSON()))
#
The problem is, the rendered output has a div around the tr and I have no idea where it is coming from:
<div>
<tr data-uid="a5e3c218-1ca4-4806-b27e-24a25ed83ab6">
<td class="first"><span>Central Networks</span></td>
<td class="red tac">4%</td>
<td>
<a class="impactanalysis individualBu" href="#"> </a>
</td>
</tr>
</div>
I just cannot see what I am doing wrong. Has anybody any idea where this could be coming from?
That looks very much like the kind of faulty DOM fragment you get when you haven't declared the .el attribute in a View correctly. I'd put a breakpoint/debugger statement in ReportBusinessUnitView.render() and inspect the value of the this.el attribute from there. (View.el docs).
Also, check your code:
Have you declared an .el property? (in MIBaseView for example)
Does it hit the right DOM node?
If not, Backbone auto creates the DIV node for you, which can be confusing.
The inclusion of a default DIV tag to surround the template is, I believe, a safety measure. This gives a parent tag to which the view's events are attached. That way event propagation will work as expected. As well, if you discard a view and remove the inserted HTML all events will go with it allowing the garbage collector to do its job.
I recently had considerable grief because I set the .el to the static HTML parent node (in order to prevent the default DIV from being added). Since it remained even after my dynamic HTML was replaced the events were still around responding to actions and creating general chaos!