Multiple directives in same element operating on visibility - angularjs

I have been struggling with my approach for the following scenario.
I have a custom directive authorize where I pass the name of a group. If the current user has this group in his profile then the element will be visible, if not the element will be hidden.
Example:
<button class="btn btn-default" role="button"
ng-click="myVm.edit()"
authorize="{{myVm.groupName}}"><!--groupName = "accountants"-->
<span class="fa fa-edit" aria-hidden="true"></span> Edit
</button>
and my original directive in typescript authorize.ts using the link function (because I operate on the DOM)
namespace app.blocks.directives {
"use strict";
class AuthorizeDirective implements ng.IDirective {
public restrict: string = "A";
public replace: boolean = true;
constructor(private $compile: ng.ICompileService, private authService: services.IAuthService) {
}
public static factory(): ng.IDirectiveFactory {
const directive = ($compile: ng.ICompileService, authService: services.IAuthService) =>
new AuthorizeDirective($compile, authService);
directive.$inject = [
"$compile",
"app.services.AuthService"
];
return directive;
}
public link(scope: ng.IScope, instanceElement: ng.IAugmentedJQuery, instanceAttributes: ng.IAttributes): void {
let groupName: string = (<any>instanceAttributes).authorize;
let element = angular.element(instanceElement);
let hasGroup: boolean = this.authService.hasGroup(groupName);
element.attr("ng-show", String(hasGroup));
//remove the attribute, otherwise it creates an infinite loop.
element.removeAttr("authorize");
this.$compile(element)(scope);
}
}
}
angular
.module("app.blocks.directives")
.directive("authorize", AuthorizeDirective.factory());
}
This is working fine, the button is hidden if the authService returns false because the user does not belong to that group (i.e: "accountants").
The problem appears when my DOM element has ng-show or ng-hide directives also. Example:
<button class="btn btn-default" role="button"
ng-hide="myVm.isDeleted"
ng-click="myVm.edit()"
authorize="{{myVm.groupName}}">
<!--groupName = "accountants"-->
<span class="fa fa-edit" aria-hidden="true"></span> Edit
</button>
When myVm.isDeleted = true it seems that overrides the result of my directive and the DOM element is displayed (when it shouldn't because the user does not belong to the specified group as per my authorize directive).
I realize there is some priority (by default 0) in directives, when two directives have the same priority they are executed in alphabetical order according to the documentation. This post was very helpful to understand that.
So I have some options here:
Have my authorize directive evaluate the conditional in ng-hide or ng-show in order to compute (i.e: if the ng-hide says that the element should be shown but the user has not the specific group, then the element should be hidden). I could not find a way to access myVm.isDeleted within my directive link's function. If anyone know how I'd be happy with this approach.
Have my authorize directive executed BEFORE any other directive and rely on angular to later on determine visibility according to ng-show or ng-hide (i.e: if my authorize directive determines that the element should be hidden because the user does not belong to the given group, then it should transform the DOM element and make it ng-show="false" for example, so that angular hides the element later on. This approach does not seem to work, the DOM seems correct, I can see that the button has ng-show="false" but for some reason I still see the button on screen so it's as if Angular didn't know that it has to hide that element. The funny thing is that if I move to another tab, and I go back to the same tab (the view is reloaded and the directive re-executed) then it works fine. What's going on?.
I went with the option 2 and this is the code that seems to work properly manipulating the DOM, but Angular does not apply the ng-show directive afterwards therefor the result is not as expected.
public priority: number = 999; //High priority so it is executed BEFORE ng-show directive
public link(scope: ng.IScope, instanceElement: ng.IAugmentedJQuery, instanceAttributes: ng.IAttributes): void {
let groupName: string = (<any>instanceAttributes).authorize;
let element = angular.element(instanceElement);
let ngShow: string = (<any>instanceAttributes).ngShow;
let ngHide: string = (<any>instanceAttributes).ngHide;
let hasGroup: boolean = this.authService.hasGroup(groupName);
let ngHideValue = ngHide ? "!" + ngHide : "";
let ngShowValue = ngShow ? ngShow : "";
//if hasGroup, use whatever ng-show or ng-hide value the element had (ng-show = !ng-hide).
//if !hasGroup, it does not matter what value the element had, it will be hidden.
if (hasGroup) {
element.attr("ng-show", (ngShowValue + ngHideValue) || "true");
} else {
element.attr("ng-show", "false");
}
element.removeAttr("ng-hide");
//remove the attribute, otherwise it creates an infinite loop.
element.removeAttr("authorize");
this.$compile(element)(scope);
}

I'd argue that seeing as your authorize directive basically just controls whether the element that it's placed displays or not, you should just move its logic out into a service that you inject into your controller, and let ng-hide control whether the element displays like it's designed to.
This will be easier for developers who come later to understand - no one wants to go drilling down into individual directives to find various scattered bits of code that call the server, and your button then just looks like this:
<button class="btn btn-default" role="button"
ng-hide="myVm.isDeleted || !myVm.isAuthorized(myVm.groupName)"
ng-click="myVm.edit()">
<span class="fa fa-edit" aria-hidden="true"></span> Edit
</button>
Nice and simple to read.

Related

how to toggle medium editor option on click using angularjs

I am trying to toggle the medium editor option (disableEditing) on button click. On the click the value for the medium editor option is changed but the medium editor does not use 'updated' value.
AngularJS Controller
angular.module('myApp').controller('MyCtrl',
function MyCtrl($scope) {
$scope.isDisableEdit = false;
});
Html Template
<div ng-app='myApp' ng-controller="MyCtrl">
<span class='position-left' medium-editor ng-model='editModel' bind-options="{'disableEditing': isDisableEdit, 'placeholder': {'text': 'type here'}}"></span>
<button class='position-right' ng-click='isDisableEdit = !isDisableEdit'>
Click to Toggle Editing
</button>
<span class='position-right'>
toggle value - {{isDisableEdit}}
</span>
</div>
I have created a jsfiddle demo.
I think initialising medium editor on 'click' could solve the issue, but i am not sure how to do that either.
using thijsw angular medium editor and yabwe medium editor
For this specific use case, you could try just disabling/enabling the editor when the button is clicked:
var editor = new MediumEditor(iElement);
function onClick(event) {
if (editor.isActive) {
editor.destroy();
} else {
editor.setup();
}
}
In the above example, the onClick function is a handler for that toggle button you defined.
If you're just trying to enable/disable the user's ability to edit, I think those helpers should work for you.
MediumEditor does not currently support changing configuration options on an already existing instance. So, if you were actually trying to change a value for a MediumEditor option (ie disableEditing) you would need to .destroy() the previous instance, and create a new instance of the editor:
var editor = new MediumEditor(iElement),
editingAllowed = true;
function onClick(event) {
editor.destroy();
if (editingAllowed) {
editor = new MediumEditor(iElement, { disableEditing: true });
} else {
editor = new MediumEditor(iElement);
}
editingAllowed = !editingAllowed;
}
Once instantiated, you can use .setup() and .destroy() helper methods to tear-down and re-initialize the editor respectively. However, you cannot pass new options unless you create a new instance of the editor itself.
One last note, you were calling the init() method above. This method is not officially supported or documented and it may be going away in future releases, so I would definitely avoid calling that method if you can.
Or you could just use this dirty hack : duplicate the medium-editor element (one with disableEditing enabled, the other with disableEditing disabled), and show only one at a time with ng-show / ng-hide :)
<span ng-show='isDisableEdit' class='position-left' medium-editor ng-model='editModel' bind-options="{'disableEditing': true ,'disableReturn': isDisableEdit, 'placeholder': {'text': 'type here'}}"></span>
<span ng-hide='isDisableEdit' class='position-left' medium-editor ng-model='editModel' bind-options="{'disableEditing':false ,'disableReturn': isDisableEdit, 'placeholder': {'text': 'type here'}}"></span>
You can see jsfiddle.

Creating a "Like" button with Angular.js

Im trying to create a like button with Angular.js.
(It is just a heart icon. default color is white = NOT liked. It is red when liked. Like/unlike is toggled by a click)
I get some data from my web service that has also an array of some ID's. These ID's are the ones that clicked the like button before.
Then i populate the DOM with the ng-repeat directive according to the data retrieved from the web service.
I attach the button a ng-class that sets the proper class and a ng-click directive that is supposed to somehow change the class too.
* I cant connect between the ng-class and the ng-click result.
some code:
<div ng-repeat="photo in photos track by photo._id">
<button ng-class="{carouselFooterButtonLikeActive : initLike(photo)}" ng-click="like(photo, this)">
<i class="icon ion-heart"></i>
</button>
</div>
Controller:
// Handle like button click
$scope.like = function(photo, photoScope){
HOW CAN I AFFECT THE NG-CLASS FROM HERE?
}
$scope.initLike = function(photo){
if(photo.likes.indexOf($localstorage.getObject('userInfo').id) > -1) {
$scope.liked = true;
return true;
}
$scope.liked = false;
return false;
}
Edit: added a possible data retrieved from the web service
{
photos: [
{
src: "src1.jpg",
likes:[111,222,333]
},
{
src: "src2.jpg",
likes:[]
}
]
}
You can use as a flag some additional property that will be initially undefined on each photo element - say photo.liked. When user clicks it, $scope.like function sets this property to true. Then ng-class evaluates photo.liked to true and adds carouselFooterButtonLikeActive class to button element.
The code is as follows:
In the template:
<button ng-class="{'carouselFooterButtonLikeActive' : photo.liked}" ng-click="like(photo, this)">
In the controller:
$scope.like = function(photo, photoScope){
photo.liked = true;
}
UPD
Say you have photos array:
[
{'src':'bla-bla.jpg', liked: true, id: 8347},
{'src':'foo-bar.jpg', id: 45},
{'src':'baz-baz.jpg', id: 47}
]
then only the first one will be shown with button.carouselFooterButtonLikeActive class, thanks to ng-class evaluation expression.
UPD2
If photo.likes is an array, you can use:
//template
ng-class="{'carouselFooterButtonLikeActive' : (photo.likes && photo.likes.length >0)}"
//controller
$scope.like = function(photo, photoScope){
photo.likes.push(someUserID);
}

Change Single Button Color from ngRepeat generated button list

I'm using ngRepeat to generate four buttons. Whenever I click one of the buttons, I want to change its color and also execute a function (for now, I'm just using console.log for sake of simplicity). If I click on another button, I want to change its color while reverting the previous button back to its original color.
I have a couple of issues - the first is that I can't seem to get ng-click to accept two commands (the first being the console.log function and the second being the instruction to change the button color). The other issue is that if I take out the console.log function, I end up changing all of the buttons when I click on one.
Any ideas? Here's the plunkr: http://plnkr.co/edit/x1yLEGNOcBNfVw2BhbWA. You'll see the console.log works but the button changing doesn't work. Am I doing something wrong with this ng-click?
<span class="btn cal-btn btn-default" ng-class="{'activeButton':selectedButt === 'me'}" ng-click="log(element);selectedButt = 'me'" data-ng-repeat="element in array">{{element}}</span>
You can create a simple function in your controller which handles this logic:
$scope.selectButton = function(index) {
$scope.activeBtn = index;
}
Then, you can simply check in your template if the current button is active:
<span class="btn cal-btn btn-default" ng-class="{true:'activeButton'}[activeBtn == $index]" ng-click="selectButton($index);" ng-repeat="element in array">{{element}}</span>
I also changed your plunkr
You may convert your element list from string array to object array first.
$scope.array = [
{"name":"first", "checked":false},
{"name":"second", "checked":false},
{"name":"third", "checked":false},
{"name":"fourth", "checked":false}
];
And your log function need to change to:
$scope.log = function(element) {
console.log(element.name);
angular.forEach($scope.array, function(elem) {
elem.checked = false;
});
element.checked = !element.checked;
}
Then, in your HTML:
<button class="btn cal-btn"
ng-repeat="element in array"
ng-click="log(element)"
ng-class="{activeButton: element.checked, 'btn-default': !element.checked}"
>{{element.name}}</button>
Please see the updated plunker here.

Using scope variables with non-form elements

If I have 2 divs (removed ng-click function for simplicity)
<div ng-class="{selected: header.type == 'percent'}" data-type="percent"></div>
<div ng-class="{selected: header.type == 'invisible'}" data-type="invisible"></div>
This will apply the class of .selected to one of the divs, depending on the value of $scope.header.type
However, I also have it so when I click on the div that does not have the .selected class, i remove the selected class from the div that had it, and apply it to the div that was just clicked.
Now, on the controller, how do I get the data-type of the div that has the .selected class?
Basically I'm trying to set $scope.header.type to hold the value of data-type of the div that has the .selected class
Just in case it's needed, here's the ng-click fn (which is not angular-like, but I couldn't find an alternative)
$scope.changeOfferbox = function($event) {
var selected = angular.element(document.querySelector('.selected'))
selected.removeClass('selected')
var clicked = angular.element($event.target).addClass('selected')
}
A simple solution might be to pass the data-type to your click function ; this way, you actually don't even need to manually add/remove classes, the ng-class directives will automatically update when header.type changes :
$scope.changeOfferbox = function($event, localType) {
if (localType !== $scope.header.type) {
$scope.header.type = localType;
}
};
<div ng-class="{selected: header.type == 'invisible'}" ng-click="changeOfferbox('invisible');" data-type="invisible"></div>

How to expand/collapse all rows in Angular

I have successfully created a function to toggle the individual rows of my ng-table to open and close using:
TestCase.prototype.toggle = function() {
this.showMe = !this.showMe;
}
and
<tr ng-repeat="row in $data">
<td align="left">
<p ng-click="row.toggle();">{{row.description}}</p>
<div ng-show="row.showMe">
See the plunkr for more code, note the expand/collapse buttons are in the "menu".
However, I can't figure out a way to now toggle ALL of the rows on and off. I want to be able to somehow run a for loop over the rows and then call toggle if needed, however my attempts at doing so have failed. See them below:
TestCase.prototype.expandAllAttemptOne = function() {
for (var row in this) {
if (!row.showMe)
row.showMe = !row.showMe;
}
}
function expandAllAttemptOneTwo(data) {
for (var i in data) {
if (!data[i].showMe)
data[i].showMe = !data[i].showMe;
}
}
Any ideas on how to properly toggle all rows on/off?
Using the ng-show directive in combination with the ng-click and ng-init directives, we can do something like this:
<div ng-controller="TableController">
<button ng-click="setVisible(true)">Show All</button>
<button ng-click="setVisible(false)">Hide All</button>
<ul>
<li ng-repeat="person in persons"
ng-click="person.visible = !person.visible"
ng-show="person.visible">
{{person.name}}
</li>
</ul>
</div>
Our controller might then look like this:
myApp.controller('TableController', function ($scope) {
$scope.persons = [
{ name: "John", visible : true},
{ name: "Jill", visible : true},
{ name: "Sue", visible : true},
{ name: "Jackson", visible : true}
];
$scope.setVisible = function (visible) {
angular.forEach($scope.persons, function (person) {
person.visible = visible;
});
}
});
We are doing a couple things here. First, our controller contains an array of person objects. Each one of these objects has a property named visible. We'll use this to toggle items on and off. Second, we define a function in our controller named setVisible. This takes a boolean value as an argument, and will iterate over the entire persons array and set each person object's visible property to that value.
Now, in our html, we are using three angular directives; ng-click, ng-repeat, and ng-show. It seems like you already kinda know how these work, so I'll just explain what I'm doing with them instead. In our html we use ng-click to set up our click event handler for our "Show All" and "Hide All" buttons. Clicking either of these will cause setVisible to be called with a value of either true or false. This will take care of toggling all of our list items either all on, or all off.
Next, in our ng-repeat directive, we provide an expression for angular to evaluate when a list item is clicked. In this case, we tell angular to toggle person.visible to the opposite value that it is currently. This effectively will hide a list item. And finally, we have our ng-show directive, which is simply used in conjunction with our visible property to determine whether or not to render a particular list item.
Here is a plnkr with a working example: http://plnkr.co/edit/MlxyvfDo0jZVTkK0gman?p=preview
This code is a general example of something you might do, you should be able to expand upon it to fit your particular need. Hope this help!

Resources