Creating a "Like" button with Angular.js - angularjs

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);
}

Related

Angular 12 FormGroup dynamically Array checkboxes custom validator does not work

I am creating an Angular 12 app, with Material.
I have a form with an checkbox array loaded dynamically from database.
I need to validate that at least one checkbox is selected
I defined like this in my OnInit():
ngOnInit(): void {
this.form = this.fb.group({
Id: new FormControl(null),
Name: new FormControl('',Validators.required),
Recipents: new FormControl('',[Validators.required, matchingEmailValidator()]),
IsActive: new FormControl(true),
ProcessorName: new FormControl('',Validators.required),
Channel: new FormArray([]),
}, { validators: [customValidateArrayGroup()] }
);
}
I need a custom validation for channel form array. If I added it in the definition of the channel, it does not fire when I check it. So, I decided to do it at the form level..
I added:
{ validators: [customValidateArrayGroup()] }
Every time an object changes, it fires this validator.
This is my custom validator:
export function customValidateArrayGroup(): ValidatorFn {
return function validate(formGroup: FormGroup) {
let checked = 0
Object.keys(formGroup.controls).forEach(key => {
const control = formGroup.controls[key]
if (control.value) {
checked++
}
})
if (checked < 1) {
return {
requireCheckboxToBeChecked: true,
}
}
return null
}
}
Here is my Html where I defined the Checkbox Array
<mat-label><strong>Channel</strong></mat-label>
<li *ngFor="let chanel of notification.NotificationChannelLogLevels">
<mat-checkbox id= {{chanel.NotificationLogLevel.Id}} formArrayName="Channel"
[checked]="chanel.IsActive"
(change)="changeEventFunc($event)">
{{chanel.NotificationLogLevel.Name}}
</mat-checkbox>
</li>
The problem I have is that the custom validator does not fire when a checkbox is clicked. Maybe is becouse they are loaded dinamically and are not recognized by formGroup.controls
How can I validate this?
You have an odd mix of using formarray and your js array in the template. Currently your formarray is completely empty, so it would be expected that it does not run when checkboxes are checked. You can choose to iterate your JS array and push / remove to formarray, or then you push the values to the formarray when you receive the data and then just iterate that one in the template. The below solution does the latter:
Shortened code....
Build form:
this.form = this.fb.group({
Channel: this.fb.array([], [customValidateArrayGroup()]),
});
I attached the custom validator to the formarray itself. When you have the dynamic data ready, then iterate it and push form controls to your formarray. I like to use a getter as well. Push whatever properties you need, here I choose IsActive and Name only:
get channels() {
return this.form.get('Channel') as FormArray;
}
// when you have data accessible:
this.notificationChannelLogLevels.forEach(value => {
this.channels.push(this.fb.group({
isActive: value.IsActive,
name: value.Name
}))
})
Now iterate this formarray in the template:
<div formArrayName="Channel">
<li *ngFor="let chanel of channels.controls; let i = index">
<ng-container [formGroupName]="i">
<mat-checkbox formControlName="isActive">
{{ chanel.get('name').value}}
</mat-checkbox>
</ng-container>
</li>
<small *ngIf="channels.hasError('hasError') && channels.touched">
Choose at least one
</small>
</div>
The custom validator checks that at least one checkbox field has isActive as true:
export function customValidateArrayGroup() {
return function validate(formArr: AbstractControl): ValidationErrors | null {
const filtered = (formArr as FormArray).value.filter(chk => chk.isActive);
return filtered.length ? null : { hasError: true }
};
}
A STACKBLITZ for your reference.
I think you have your FormArray setup incorrectly in your template.
You are applying the formArrayName attribute the each checkbox when it needs to be applied to a parent container,
<div formArrayName="myFormArray">
<div *ngFor="*ngFor="let chanel of notification.NotificationChannelLogLevels; let i = index">
//Use the index here to dynamically tie each mat-checkbox to a FormControl
<mat-checkbox [FormControl]="myCheckboxes[i]"></mat-checkbox>
</div>
</div>
And then in your .ts file you'll have to define myCheckboxes as a FormArray with instances of form control inside it. Otherwise myCheckboxes[i] will be either null or an index out of bounds. You can use the form array you added to your form group, but the indexes you reference in the template have to be defined.
Here is a good blog post going over how to handle adding/removing instances from the form array,
https://netbasal.com/angular-reactive-forms-the-ultimate-guide-to-formarray-3adbe6b0b61a
And another,
https://blog.angular-university.io/angular-form-array/
As a side note, if your logging levels are static, it may just be easier or more intuitive to define the list of checkbox controls as a FormGroup and apply your validator to the form group.

Multiple directives in same element operating on visibility

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.

angularjs : disable a button and show popup instead

I have this button :
html:
<button nav-direction="back" class="button yy" ui-sref="app.result" ui-sref-active="currentNav" ng-click="navResult()">
Board
</button>
I would like it to display a popup if a certain condition is, else I would like it to go to another page.
I need to keep the benefit of the class in ui-sref-active to show that this is the current page.
controller.js
$scope.navResult = function (){
console.log(sessionService.get('computed'));
if (sessionService.get('computed')) {
$scope.go('app.result');
} else {
//popup to user to tap on a board
//$scope.go('app.compute');
var popupConfig = {
title: 'Beware! ;)',
template: 'Tap on a board below'
};
var popup = $ionicPopup.show(popupConfig);
ClosePopupService.register(popup);
}
}
$scope.go = function ( state ) {
// console.log("go has been launched with : "+ state)
$state.go( state );
};
Simple. You just use an ng-click method instead of a ui-sref, and go to the state from there.
<button nav-direction="back" ng-class="{'your-class':classCondition}" class="button yy" ng-click="navResult()">
Board
</button>
Then in your controller....
$scope.navResult = function(){
if(something){
$scope.classCondition = false;
//code to display popup here
} else {
$state.go('app.result')
}
}
You can pass any valid state into $state.go, so if you ever want to check for a condition and perform some logic BEFORE you redirect to another page, use it inside a $scope method instead of just using the straight ui-sref.

AngularJS - Manage button or input availability via model

So, i'm quite stuck at this:
I have quite many buttons on my page, each button's availability is different in different context, for example:
When button "Add" is pressed, its state will be "disabled", Cancel and Save buttons will be available.
I came up with a solution to manage the stage of every buttons in one object call state, like this:
// This is the Controller
$scope.some_id = false;
// With some UI interaction, $scope.some_id will be some integer value
$scope.getContextBtnAdd = function(){ return $scope.some_id; };
$scope.getContextBtnCancel = function(){ return !$scope.getContextBtnAdd(); };
$scope.state = {
contextBtns : {
btnAdd : $scope.getContextBtnAdd(),
btnCancel: $scope.getContextBtnCancel(),
},
footerBtns : { Some footer buttons }
};
// This is the View
<button name="add" ng-disabled="!state.contextBtns.btnAdd">Add</button>
"So when there is no product load, $scope.some_id will be false, so Add button will be available, but when user clicks on some products, the $scope.some_id will be some integer value like 4,7 or 100... and the button will be disabled. " <=== That's what i want. But actually it doesn't work, the some_id changes but maybe the getContext function is never called. Right now i have to pass the getContext function to ng-disabled like this:
<button name="add" ng-disabled="!getContextBtnAdd()">Add</button>
But i want to manage them all via one object. So what's is my code problem and is there any better way to manage button, input....s availabilities?
Thank you!
When that line is executed:
$scope.state = {
contextBtns : {
btnAdd : $scope.getContextBtnAdd(),
btnCancel: $scope.getContextBtnCancel(),
},
footerBtns : { Some footer buttons }
};
the state object is created, and its contextBtns.btnAdd attribute (for example) takes the value returned by $scope.getContextBtnAdd() at that time. The attribute value never changes after.
What you want instead is something that always has the value of $scope.getContextBtnAdd() every time it's evaluated. So you simply need the function itself:
$scope.state = {
contextBtns : {
btnAdd : $scope.getContextBtnAdd,
btnCancel: $scope.getContextBtnCancel,
},
footerBtns : { Some footer buttons }
};
and in the view:
<button name="add" ng-disabled="!state.contextBtns.btnAdd()">Add</button>

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