How to refresh Font Awesome icon on Angular? - javascript

Is there any way to change font awesome icon dynamically? I want user to be able to select one of font awesome icons dynamically. It works only when you add class first time. The place where I try to do it is - MatDialog. There is form where user have to select icon, background color and category name. To select icon user should open another dialog.
I'm using Angular 9.1.4 and Font Awesome 5.13.0.
That's what I tried:
1. Using ngClass
category-dialog.component.html
<div [ngStyle]="selectedColor">
<i [ngClass]="selectedIcon"></i>
</div>
category-dialog.component.ts
openIconDialog(): void {
const dialogRef = this.dialog.open(DialogIconSelectComponent, { width: '15rem' });
dialogRef.afterClosed().subscribe(result => {
this.selectedIcon = result;
});
}
This works only first time. But when you try to change icon selectedIcon changes, but UI doesn't refresh element class.
2. Using #ViewChild
#ViewChild('iconElement') iconElement: ElementRef;
constructor(private dialog: MatDialog,
private renderer: Renderer2) { }
openIconDialog(): void {
const dialogRef = this.dialog.open(DialogIconSelectComponent, { width: '15rem' });
dialogRef.afterClosed().subscribe((result: string) => {
this.iconElement.nativeElement.className = result;
});
}
This also works only first time.
3. Using #ViewChild and Renderer2
category-dialog.component.html
<div #colorElement [ngStyle]="selectedColor">
<i #iconElement></i>
</div>
category-dialog.component.ts
#ViewChild('colorElement') parentElement: ElementRef;
#ViewChild('iconElement') childElement: ElementRef;
constructor(private dialog: MatDialog,
private renderer: Renderer2) { }
openIconDialog(): void {
const dialogRef = this.dialog.open(DialogIconSelectComponent, { width: '15rem' });
dialogRef.afterClosed().subscribe(result => {
this.replaceIcon(result);
});
}
replaceIcon(iconClass: string): void {
const i = this.renderer.createElement('i');
this.renderer.setProperty(i, 'class', iconClass);
this.renderer.removeChild(this.parentElement.nativeElement, this.childElement);
this.renderer.appendChild(this.parentElement.nativeElement, i);
}
That doesn't work at all.
Is there any way how to change font awesome dynamically?

Resolution
Wasted lot of my free time to investigate how to resolve this issue. Tried everything with Renderer2 and all dirty Javascript methods. But one day I came up with idea to use innerHtml.
Rendering new string of inner HTML changes Font Awesome icons interactively.
category-dialog.component.html
<div [ngStyle]="selectedColor" [innerHtml]="selectedIconHtml"></div>
category-dialog.component.ts
openIconDialog(): void {
const dialogRef = this.dialog.open(DialogIconSelectComponent, { width: '15rem' });
dialogRef.afterClosed().subscribe((result: string) => {
// EVERY TIME NEW ELEMENT WITH NEW FA CLASS
this.selectedIconHtml = `<i class="${result}"></i>`;
});
}
This solution - on every icon selection's changing <div> element content (inner html).

I solved this issue like this:
<div innerHTML="<i class='{{icon}}'></i>">
</div>
In this case, icon will be rerendered after value changes. innerHTML makes this happen easily. No need any code in TS file.

Related

Angular - How to reproduce toggle-buttons of Angular Material

I'm in trouble to create a component like mat-button-toggle-group of material
I create a simple container with an ng-content that wraps buttons and inside it some customized buttons. THe number of components-button can change...
<container-buttons-wrapper>
<component-button>Test 1</component-button>
<component-button>Test 2</component-button>
<component-button>Test 3</component-button>
</container-buttons-wrapper>
component-button has inside only a button tag
<button (click)="setActive()" [ngClass]="active? 'active-class' : 'no-active-class'"><ng-content></button>
I defined a function setActive() that toggle active value
setActive() {
this.active = !this.active
}
But I can't find a solution to control the other buttons into container. I want to reproduce exactly what mat-button-toggle-group. Is possible to define an eventEmitter inside the template html?
Problem: when a button changes its status to "active", container should change the state of the remaining buttons to "inactive".
Solution: Implement two-way communication between buttons and the container:
a button should be able to notify the container that it became active
a container should be able to set the remaining buttons to inactive state (or, a button should be able to know whether it is active or not from the container)
Basically, the container becomes a holder of the shared state for itself and all nested buttons. This state is made available to nested buttons via DI. State can be handled by a separate service, or it can be a part of the container component itself for simplicity (the latter approach is implemented in Material):
const CONTAINER = new InjectionToken<ContainerComponent>();
#Directive({
providers: [{provide: CONTAINER, useExisting: forwardRef(() => ContainerComponent)}]
})
class ContainerComponent {
private selectedButton: ButtonComponent | null = null
toggleButton(button: ButtonComponent) {
if (this.selectedButton = button) {
this.selectedButton = null
} else {
this.selectedButton = button;
}
}
isSelected(button: ButtonComponent): boolean {
return this.selectedButton = button
}
}
#Component({template: `
<button [class.selected]="isSelected()" (click)="onClick()">
<ng-content></ng-content>
</button>
`})
class ButtonComponent {
constructor(#Inject(CONTAINER) private container: ContainerComponent) {}
isSelected() {
return this.container.isSelected(this)
}
onClick() {
this.container.toggleButton(this)
}
}
Update: Pre-selecting a button
How do we set some button as "selected" initially?
Approach 1
One way is doing smth similar to what Material does.
Add "selected" input to a button componentn
In the Container we would read all buttons via ContentChildren
Whenever the input changes, button should update the state in container.
A challenge is that now we have two sources of truth for "selected" flag in a button (button's input and the state coming from the container) - so we need to reconcile them, and the overall code becomes more involved.
Approach 2
Alternatively, let's assume your toggle component has some sort of "value" property. Kind of like html <select> element - each option has value property, and the selected property of the <select> is derived based on that.
In this case, we would have an input in ContainerComponent that allows to set initial value:
class ContainerComponent {
#Input() selected: any
toggleButton(value: any) {
if (this.selected !== value) {
this.selected = value
} else {
this.selected = null
}
}
}
class ButtonComponent {
#Input() value: any
onClick() {
this.container.toggleButton(this.value)
}
}
// usage
<container selected="option-1">
<my-button value="option-1"><my-button>
<my-button value="option-2"><my-button>
</container>

How to show Angular Material Tooltip on component initialization

I want to show an Angular Material tooltip when its component is initialized/loaded.
I know I can add an HTML attribute to show it when an event happens. My overall goal is to have the tooltip showing when the component loads, then hide after a few seconds.
I've tried the following:
<div (load)="tooltip.show()"
#tooltip="matTooltip"
matTooltip="blah blah">
</div>
YoukouleleY is almost correct, you need to put it into ngAfterViewInit() and add setTimeout() to make it work:
#ViewChild('tooltip') tooltip: MatTooltip;
constructor(private cd: ChangeDetectorRef) { }
ngAfterViewInit() {
this.tooltip.show();
this.cd.detectChanges();
setTimeout(() => this.tooltip.hide(2000));
}
Added update with changeDetectorRef to avoid ExpressionChangedAfterItHasBeenCheckedError. Hope that helps.
Try this:
#ViewChild('tooltip') tooltip: MatToolTip;
ngOnInit() {
this.tooltip.show();
this.tooltip.hide(2000);
}

Add css to component via service in Angular2, is it possible?

I have an component A with 5 pictures. Only 1 picture has colour and is clickable, the other 4 are grey with help of this css class
.not_opened{
-webkit-filter: grayscale(85%);
}
And are not clickable.
If I click on the first picture, I change component to component B (A disappears, because it is separate component, not child or parent) , do some manipulations in the new second component B and then I click the button, which returns me to component A. Everything stays the same there, but I would like to make 2 picture not grey ( so delete/change this class not_opened from picture 2) and make it clickable, then if I click picture 2 I go to third component C and then back and third picture is now not grey and clickable and so on...
How could I make something like this?
First thought was to make 4 different components, each with own css stylesheet, but maybe there is another way?..
Maybe somehow with help of service?
Could you please advice me something?
First I suggest you to introduce a view-model notion to your project. View-model contains information on how to render a particular model. In your case you can pass something like Array<ImageViewModel> between components A and B. You can pass this data through some service if you find it suitable for your case, or you can use parent component, e.g.:
Parent component template:
<component-a [images]="images" *ngIf="active === 'a'" (onImageSelected)="handleImageSelected($event)"></component-a>
<component-b [images]="images" *ngIf="active === 'b'" (onSettingsEditCompleted)="handleSettingsEditCompleted()"></component-b>
Parent component code:
.... {
images: Array<ImageViewModel> = [];
active: string = "a";
constructor(private _service: ImageService) {
// Lets imagine that service returns array of image urls.
this._service.fetchImages().subscribe((urls) => {
this.images = urls.map(url => ({src: url, disabled: true}));
if (this.images.length > 0) {
this.images[0].disabled = false;
}
});
}
handleImageSelected(image: ImageViewModel) {
this.active = "b";
console.log("Image selected", image);
}
handleSettingsEditCompleted() {
this.active = "a";
}
}
And where ImageViewModel is something like:
interface ImageViewModel {
src: string;
disabled: boolean;
}
Now in componentA use [ngClass] directive in order to change image availability.
ComponentA template:
<img *ngFor="let image of images" [src]="image.src" [ngClass]="{'not-opened': image.disabled}" (click)='!image.disabled && handleImageSelected(image)'/>
ComponentA styles:
.not-opened {
filter: grayscale(85%); // Why do you use -webkit version only?
-webkit-filter: grayscale(85%);
}
ComponentA code:
... {
#Output()
onImageSelected = new EventEmitter<ImageViewModel>();
#Input()
images: Array<ImageViewModel> = [];
handleImageSelected(image: ImageViewModel) {
this.images[1].disabled = false;
this.onImageSelected.emit(image);
}
}
Read about #Input and #Output annotations at Angular documentation if something is not clear.

Create custom script for DOM Manipulation

I'm currently working on an Angular 2 Project where I have a menu that should be closable by a click on a button. Since this is not heavy at all, I would like to put it outside of Angular (without using a component for the menu).
But I'm not sure of how to do it, actually I've just put a simple javascript in my html header, but shouldn't I put it somewhere else?
Also, what the code should be? Using class, export something? Currently this is my code:
var toggleMenuButton = document.getElementById('open-close-sidebar');
var contentHolder = document.getElementById('main-content');
var menuHolder = document.getElementById('sidebar');
var menuIsVisible = true;
var updateVisibility = function() {
contentHolder.className = menuIsVisible ? "minimised" : "extended";
menuHolder.className = menuIsVisible ? "open" : "closed";
}
toggleMenuButton.addEventListener('click', function() {
menuIsVisible = !menuIsVisible;
updateVisibility();
});
Finally moved to something with MenuComponent and a service, but I'm still encountering an issue.
MenuService.ts
#Injectable()
export class MenuService {
isAvailable: boolean = true;
isOpen: boolean = true;
mainClass: string = "minimised";
sidebarClass: string = "open";
updateClassName() {
this.mainClass = this.isOpen ? "minimised" : "extended";
this.sidebarClass = this.isOpen ? "open" : "closed";
}
toggleMenu(newState: boolean = !this.isOpen) {
this.isOpen = newState;
this.updateClassName();
}
}
MenuComponent.ts
export class MenuComponent {
constructor(private _menuService: MenuService) { }
public isAvailable: boolean = this._menuService.isAvailable;
public sidebarClass: string = this._menuService.sidebarClass;
toggleMenu() {
this._menuService.toggleMenu();
}
}
MenuComponent.html
<div id="sidebar" [class]="sidebarClass" *ngIf="isAvailable">
...
<div id="open-close-sidebar"><a (click)="toggleMenu()"></a></div>
The action are rightly triggered, if I debug the value with console.log, the class name are right but it didn't change the value of the class. I thought the binding was automatic. And I still do not really understand how to change it. Do I have to use Emmit like AMagyar suggested?
The advantage of using angular2 above your own implementation, greatly outweigh the marginal benefit in performance you will get from using plane JavaSccript. I suggest not going on this path.
If you however do want to continue with this, you should export a function and import and call this function inside the ngAfterViewInit of your AppComponent. The exported function should add the click EventListener and (important) set the document.getElementById variables. Because your script possibly won't be able to find those elements yet when it's loaded.
But let me emphasise once more, that angular2 is optimised for exactly these tasks, and once you get more familiar with it, it will also be a lot easier to code it.
update
For inter component communication you should immediately think about a service. Just create a service which stores the menu state and add this to your global ngModule providers array. For instance:
export class MenuService {
public get menuOpen(): boolean {
return this._menuOpen;
}
private _menuOpen: boolean;
public openMenu() : void {
this._menuOpen = true;
}
public closeMenu() : void {
this._menuOpen = false;
}
public toggleMenu() : void {
this._menuOpen = !this._menuOpen;
}
}
You can then inject this service into your menu component and bind the classes open/closed and minimized/extended to the MenuService.menuOpen.
#Component({
selector : 'menu'
template : `
<button (click)="menuService.toggleMenu()">click</button>
<div id="open-close-sidebar" [class.open]="menuService.menuOpen"></div>
`
})
export class MenuComponent {
constructor(public menuService: MenuService){}
}
For other component you can use the same logic to see if the menu is open or closed
update #2
You have to use a getter to get the value from menuService. There is only one way binding:
export class MenuComponent {
constructor(private _menuService: MenuService) { }
public get isAvailable(): boolean {
return this._menuService.isAvailable;
}
public get sidebarClass(): string {
return this._menuService.sidebarClass;
}
toggleMenu() {
this._menuService.toggleMenu();
}
}
FYI, it's better practice to use [class.open] instead of a string class name. If you want to do it like that, it will only require minimal change in your current css.
The main reason of why I want to avoid using Angular component is the
fact that my manipulation should be done over all the website and not
just the "menu" component.
You can create many components in Angular 2, it's easy and very practical.
The action will change the class on my menu (located in my menu
component) and on my main content (located outside of the component).
I don't know how to do it, and I'm not sure that this is the best
way... Maybe by binding the service value directly... –
The main content can have a child that is the Menu itself.
Take a look in this link. There are many solutions, one of them is to "emit" the child changes to the parent.
If you need an example I can provide one quickly.

how to mix a decision of with an observer and synced actions? (Angular)

I have six square views in my project, I have to options to click on them:
click on one and and it get selected with some css fade color on top of it to show its selected. this already works.
select couple of them and having the same css fade color to show the squares are selected. this works to some degree but messed with something else.
so to recognize which mode im at I have radio buttom that call multipleSelectionMode.
so this is the code:
this is my square component html file.
<div *ngIf="oneChoosed" layout="row" layout-align="center center" class="one-chose">
</div>
<div *ngIf="multipleChoosed" layout="row" layout-align="center center" class="omult-chose">
</div>
in this component class I have 2 inputs I get from the main screen component:
#Input() oneChoosed: boolean;
#Input() multipleChoosed: boolean;
this is have I update this inputs from the main screen component html file:
[oneChoosed]="isOneChoosed(squares)"
[multipleChoosed]="isMultipleChoosed(squares)"
and in the main screen component class I get the click events and the id's of the squares, it looks like this:
export class mainScreenComp implements OnInit, OnDestroy {
multipleSelectionMode: Observable<boolean>;
oneChoosenIds: string[];
multipleChosenIds: string[];
_oneChoseSquareIdsSubscription: Subscription;
_multipleChoseSquaresIdsSubscription: Subscription;
ngOnInit() {
this.multipleSelectionMode = this.brainComp.curSelectionTyp.map(selecType => selecType === SelectionType.multiple);
this._oneChoseSquareIdsSubscription = this.brainComp.choosingOption.subscribe(
(chos: ChoosingOption) => {
switch (chos.option) {
case ChoosingOption.oneChose:
this.oneChoosenIds = chos.chosedIds.squares;
break;
case ChoosingOption.finishedOneSel:
this.oneChoosenIds = undefined;
break;
}
}
);
this._multipleChoseSquaresIdsSubscription = this.brainComp.choosingOption.subscribe(
(chos: Chosed) => {
this.multipleChosenIds = chos.squares;
}
);
}
isOneChoosed(square: SquaresIQ): boolean {
return (this.oneChoosenIds ? this.oneChoosenIds.includes(square.squareId) : false);
}
isMultipleChoosed(square: SquaresIQ): boolean {
return (this.multipleChosenIds ? this.multipleChosenIds.includes(square.squareId) : false);
}
so basically I add squares id's to the arrays oneChoosenIds and multipleChosenIds and check if they empty or not to decide what to return,
this works, BUT, when im in oneChoosing state where I can
only choose one, when I click on it I see both css square covers that shows that the square is selected...cause they have different design I can notice it.
so it means this *ngIf="oneChoosed" and this *ngIf="multipleChoosed" both returned true...so I thought to put extra condition with my observer multipleSelectionMode in the _multipleChoseSquaresIdsSubscription subscription, but this is async cause its an observer and the other actions are sync so its not working properly. do you have a suggestion for me to solve it?

Categories

Resources