Angular2 - why doesn't async variable work with [hidden] directive? - javascript

I have links that I need to hide or show based on the services to or permissions of the current user. I have these services accessible via an Observable object and was hoping to use it in my template to determine what to hide/show.
I noticed though that this works for *ngIf but not for the [hidden] directive. I can't find any info on why this is?
Example:
#Component({
selector: 'my-app',
template: `
<a href="" *ngIf="(services$ | async)?.SPECIAL_FEATURE === true">
Save dropdown</a>
<a href="" [hidden]="(services$ | async)?.SPECIAL_FEATURE === false">
[Hidden] search</a>
<a href="" [hidden]="hideOtherLink">
The other link</a>
`
})
export class AppComponent implements OnInit {
name = 'Angular 5';
hideOtherLink = false;
services$: Observable<object>;
ngOnInit() {
this.services$ = Observable.timer(2000)
.map(() => {
return {
SPECIAL_FEATURE: true,
SPECIAL_THING: true
}
});
setTimeout(() => {
this.hideOtherLink = true;
}, 2000);
}
}

You got confused in your conditions there.
This one means
*ngIf="(services$ | async)?.SPECIAL_FEATURE === false"
If the condition is true, then show it
While this one means
[hidden]="(services$ | async)?.SPECIAL_FEATURE === false"
If the condition is true, then hide it.
They are the opposit of each other, that's why it is "not working".
EDIT You edited your question to set the ngIf to true, So I tested it on stack and I think I got your issue.
In your hidden condition, you test if the value of SPECIAL_FEATURE is equal to false.
This means
hide the content only if the special feature variable is equal to false.
But this doesn't cover the undefined or null values, for instance, which are falsy values.
Try this :
[hidden]="!(services$ | async)?.SPECIAL_FEATURE"

Related

Is there an Angular way of doing: document.getElementById(id).style.display = "none";

I have a div with the id of 1. I'm trying to set the display to none dynamically. Is there an Angular way of doing this. Currently, I'm using vanilla javascript. I was asking about doing this dynamically because there will be over 60 divs that will be created from an array.
In my html
<div *ngFor="let item of items; i = index;">
<div id={{i}} (click)=hideDiv()></div>
</div>
In my method
hideDiv() {
return document.getElementById('1').style.display = "none";
}
That works but I'm looking for the Angular way of doing the above.
It was suggested that I use #ViewChild. Here's what I've changed. I can't use a Template Reference Variable as the html divs are created dynamically. Unless someone can let me know how to create the template variables dynamically. Although I don't think it's possible to create template variables with a loop.
#ViewChild('imgId', { static: true }) elementRef: ElementRef<HTMLDivElement>;
imgId: string;
Then in the method I have:
this.imgId = event.path[0].attributes[1].value;
this.elementRef.nativeElement.style.display = "none";
The event.path[0].attributes[1].value gets me the id of the image. The imgId shows when I console log it. It's still not changing the display on the div to none. Also I'm getting the error:
Cannot read properties of undefined (reading 'nativeElement')
Yes, you can use the ViewChild query in Angular to do this. In your component, define a query like this:
#ViewChild('#1') elementRef: ElementRef<HTMLDivElement>;
Implement the AfterViewInit interface in your component, and inside it, use this:
this.elementRef.nativeElement.style.display = "none";
You can simply use ngIf for this
Component
shouldDisplay: boolean = true;
hide(): void {
this.shouldDisplay = false;
}
show(): void {
this.shouldDisplay = true;
}
Html
<button (click)="hide()">Hide</button>
<button (click)="show()">Show</button>
<div *ngIf="shouldDisplay">this is the content</div>
Here is the working example
This is the Angular way:
template
<div *ngIf="showMe"></div>
or
<div [hidden]="!showMe"></div>
TypeScript:
showMe: boolean;
hideDiv() {
this.showMe = false;
}
For dynamic items where your don't know how many you will get the best approach would be to add a directive that would store and adjust that for you:
#Directive({ selector: '[hide-me]' })
export class HideDirective {
#Input() id!: string;
#HostBinding('style.display')
shouldShow: string = '';
}
then in your component just address them by ID:
#Component({
selector: 'my-app',
styleUrls: ['./app.component.css'],
template: `
<div *ngFor="let item of items; let index = index;">
<div hide-me id="{{index}}" (click)="hideDiv(index)">Some value</div>
</div>
`,
})
export class AppComponent {
#ViewChildren(HideDirective) hideDirectives!: QueryList<HideDirective>;
items = [null, null, null];
hideDiv(id: number) {
this.hideDirectives.find((p) => p.id === id.toString()).shouldShow = 'none';
}
}
Stackblitz: https://stackblitz.com/edit/angular-ivy-pnrdhv?file=src/app/app.component.ts
An angular official example: https://stackblitz.com/edit/angular-ivy-pnrdhv?file=src/app/app.component.ts
How about passing the div reference to the hideDiv method directly in the Dom using a template variable like this.
<div *ngFor="let item of items; i = index;">
<div #divElement (click)=hideDiv(divElement)></div>
And in your hide div method you will have access to the element directly
hideDiv(div) { div.style.display = "none";}
Here is a Stackblitz example
https://stackblitz.com/edit/angular-ivy-w1s3jl
There are many ways to do this, but in my opinion this is a simple solution the achieves your goal with less code.
PS:
It is always recommended to use the angular Renderer2 to manipulate Dom elements. This service has the method setStyle which you can use for your code.

Losing the focus on an input that appears conditionally - Angular 7 [duplicate]

I am working a front-end application with Angular 5, and I need to have a search box hidden, but on click of a button, the search box should be displayed and focused.
I have tried a few ways found on StackOverflow with directive or so, but can't succeed.
Here is the sample code:
#Component({
selector: 'my-app',
template: `
<div>
<h2>Hello</h2>
</div>
<button (click) ="showSearch()">Show Search</button>
<p></p>
<form>
<div >
<input *ngIf="show" #search type="text" />
</div>
</form>
`,
})
export class App implements AfterViewInit {
#ViewChild('search') searchElement: ElementRef;
show: false;
name:string;
constructor() {
}
showSearch(){
this.show = !this.show;
this.searchElement.nativeElement.focus();
alert("focus");
}
ngAfterViewInit() {
this.firstNameElement.nativeElement.focus();
}
The search box is not set to focus.
How can I do that?
Edit 2022:
Read a more modern way with #Cichy's answer below
Modify the show search method like this
showSearch(){
this.show = !this.show;
setTimeout(()=>{ // this will make the execution after the above boolean has changed
this.searchElement.nativeElement.focus();
},0);
}
You should use HTML autofocus for this:
<input *ngIf="show" #search type="text" autofocus />
Note: if your component is persisted and reused, it will only autofocus the first time the fragment is attached. This can be overcome by having a global DOM listener that checks for autofocus attribute inside a DOM fragment when it is attached and then reapplying it or focus via JavaScript.
Here is an example global listener, it only needs to be placed in your spa application once and autofocus will function regardless of how many times the same fragment is reused:
(new MutationObserver(function (mutations, observer) {
for (let i = 0; i < mutations.length; i++) {
const m = mutations[i];
if (m.type == 'childList') {
for (let k = 0; k < m.addedNodes.length; k++) {
const autofocuses = m.addedNodes[k].querySelectorAll("[autofocus]"); //Note: this ignores the fragment's root element
console.log(autofocuses);
if (autofocuses.length) {
const a = autofocuses[autofocuses.length - 1]; // focus last autofocus element
a.focus();
a.select();
}
}
}
}
})).observe(document.body, { attributes: false, childList: true, subtree: true });
This directive will instantly focus and select any text in the element as soon as it's displayed. This might require a setTimeout for some cases, it has not been tested much.
import { Directive, ElementRef, OnInit } from '#angular/core';
#Directive({
selector: '[appPrefixFocusAndSelect]',
})
export class FocusOnShowDirective implements OnInit {
constructor(private el: ElementRef) {
if (!el.nativeElement['focus']) {
throw new Error('Element does not accept focus.');
}
}
ngOnInit(): void {
const input: HTMLInputElement = this.el.nativeElement as HTMLInputElement;
input.focus();
input.select();
}
}
And in the HTML:
<mat-form-field>
<input matInput type="text" appPrefixFocusAndSelect [value]="'etc'">
</mat-form-field>
html of component:
<input [cdkTrapFocusAutoCapture]="show" [cdkTrapFocus]="show">
controler of component:
showSearch() {
this.show = !this.show;
}
..and do not forget about import A11yModule from #angular/cdk/a11y
import { A11yModule } from '#angular/cdk/a11y'
I'm going to weigh in on this (Angular 7 Solution)
input [appFocus]="focus"....
import {AfterViewInit, Directive, ElementRef, Input,} from '#angular/core';
#Directive({
selector: 'input[appFocus]',
})
export class FocusDirective implements AfterViewInit {
#Input('appFocus')
private focused: boolean = false;
constructor(public element: ElementRef<HTMLElement>) {
}
ngAfterViewInit(): void {
// ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.
if (this.focused) {
setTimeout(() => this.element.nativeElement.focus(), 0);
}
}
}
This is working i Angular 8 without setTimeout:
import {AfterContentChecked, Directive, ElementRef} from '#angular/core';
#Directive({
selector: 'input[inputAutoFocus]'
})
export class InputFocusDirective implements AfterContentChecked {
constructor(private element: ElementRef<HTMLInputElement>) {}
ngAfterContentChecked(): void {
this.element.nativeElement.focus();
}
}
Explanation:
Ok so this works because of: Change detection. It's the same reason that setTimout works, but when running a setTimeout in Angular it will bypass Zone.js and run all checks again, and it works because when the setTimeout is complete all changes are completed. With the correct lifecycle hook (AfterContentChecked) the same result can be be reached, but with the advantage that the extra cycle won't be run. The function will fire when all changes are checked and passed, and runs after the hooks AfterContentInit and DoCheck. If i'm wrong here please correct me.
More one lifecycles and change detection on https://angular.io/guide/lifecycle-hooks
UPDATE:
I found an even better way to do this if one is using Angular Material CDK, the a11y-package.
First import A11yModule in the the module declaring the component you have the input-field in.
Then use cdkTrapFocus and cdkTrapFocusAutoCapture directives and use like this in html and set tabIndex on the input:
<div class="dropdown" cdkTrapFocus cdkTrapFocusAutoCapture>
<input type="text tabIndex="0">
</div>
We had some issues with our dropdowns regarding positioning and responsiveness and started using the OverlayModule from the cdk instead, and this method using A11yModule works flawlessly.
In Angular, within HTML itself, you can set focus to input on click of a button.
<button (click)="myInput.focus()">Click Me</button>
<input #myInput></input>
To make the execution after the boolean has changed and avoid the usage of timeout you can do:
import { ChangeDetectorRef } from '#angular/core';
constructor(private cd: ChangeDetectorRef) {}
showSearch(){
this.show = !this.show;
this.cd.detectChanges();
this.searchElement.nativeElement.focus();
}
I'm having same scenario, this worked for me but i'm not having the "hide/show" feature you have. So perhaps you could first check if you get the focus when you have the field always visible, and then try to solve why does not work when you change visibility (probably that's why you need to apply a sleep or a promise)
To set focus, this is the only change you need to do:
your Html mat input should be:
<input #yourControlName matInput>
in your TS class, reference like this in the variables section (
export class blabla...
#ViewChild("yourControlName") yourControl : ElementRef;
Your button it's fine, calling:
showSearch(){
///blabla... then finally:
this.yourControl.nativeElement.focus();
}
and that's it.
You can check this solution on this post that I found, so thanks to -->
https://codeburst.io/focusing-on-form-elements-the-angular-way-e9a78725c04f
There is also a DOM attribute called cdkFocusInitial which works for me on inputs.
You can read more about it here: https://material.angular.io/cdk/a11y/overview
Only using Angular Template
<input type="text" #searchText>
<span (click)="searchText.focus()">clear</span>
When using an overlay/dialog, you need to use cdkFocusInitial within cdkTrapFocus and cdkTrapFocusAutoCapture.
CDK Regions:
If you're using cdkFocusInitial together with the CdkTrapFocus directive, nothing will happen unless you've enabled the cdkTrapFocusAutoCapture option as well. This is due to CdkTrapFocus not capturing focus on initialization by default.
In the overlay/dialog component:
<div cdkTrapFocus cdkTrapFocusAutoCapture>
<input cdkFocusInitial>
</div>
#john-white The reason the magic works with a zero setTimeout is because
this.searchElement.nativeElement.focus();
is sent to the end of the browser callStack and therefore executed last/later, its not a very nice way of getting it to work and it probably means there is other logic in the code that could be improved on.
Easier way is also to do this.
let elementReference = document.querySelector('<your css, #id selector>');
if (elementReference instanceof HTMLElement) {
elementReference.focus();
}

Angular 6 [ngClass] not working with boolean in component.js

What I'm trying to do is hide text when ngState is true. When a certain element is clicked, that state is set to true. The [ngClass] should then add the hide class and hide the text. This first snippet is from the component.ts which outlines the boolean variable and the function which sets it to true.
export class MainMenuComponent implements OnInit {
ngState = false;
constructor() {
}
newGame(){
this.ngState = this.ngState === true ? false : true;
console.log(this.ngState);
}
}
This next snippet is the component html
<canvas id='sparkCanvas'></canvas>
<div class="menuBox">
<div class="title" [ngClass]="{'hide': ngState}">Dark Shards</div>
<div class="optContainer">
<ul>
<li *ngFor="let opt of opts" class="{{opt.class}}" [ngClass]="{'hide': ngState}" (click)="opt.f()">{{opt.n}}</li>
</ul>
</div>
</div>
and here is the hide class below
.hide{
opacity: 0;
}
When I replace [ngClass]="{'hide': ngState}" with [ngClass]="{'hide': true}"
It will then work as intended. What am I not understanding here?
Here is a link to my code with a working example:
https://stackblitz.com/edit/angular-fg48ro?file=src%2Findex.html
Try without Quote
<li *ngFor="let opt of opts" class="{{opt.class}}" [ngClass]="{hide: ngState}" (click)="opt.f()">{{opt.n}}</li>
EDIT
When i see your code, the issue is not related to angular, but with javascript context, you need to specifiy the context of this like
' f: this.newGame.bind(this),'
DEMO

Computed not running when updated from emitted event vue js

I'm kinda a noob at vue js, however I can't seem to understand why the allValid event isn't being emitted from the following code: (snippet here, full code in the js fiddle)
http://jsfiddle.net/sTX7y/674/
Vue.component('password-validator', {
template: `
<ul>
<regex-validation regex='.{6,}' v-on:valid='v => { isLongEnough = v }' :input='input'>
Is Long Enough
</regex-validation>
<regex-validation regex='[A-Z]' v-on:valid='v => { hasUppercase = v }' :input='input'>
Has Upper
</regex-validation>
</ul>
`,
props: [ 'input' ],
data: function(){
return {
isLongEnough: false,
hasUppercase: false,
}
},
computed:{
isValid: function(){
var valid = this.isLongEnough && this.hasUppercase;
this.$emit('allValid', valid);
return valid;
}
}
});
When viewing this using the vue chrome extension I can clearly see that isLongEnough and hasUppercase both flip from true to false, (and the validation is reflected on the output). It's just that the last isValid computed function just never seems to run...
Thanks for the help and if you see any other noob mistakes feel free to chime in on how I could do this better.
The computed function is defined correctly in the password-validator component. The only problem is you have referenced it ouside of the component scope. i.e. {{ isValid }} is in the html outside of the template. To correct this you can change the password-validator template thus:
template: `
<ul>
<regex-validation regex='.{6,}' v-on:valid='v => { isLongEnough = v }' :input='input'>
Is Long Enough
</regex-validation>
<regex-validation regex='[A-Z]' v-on:valid='v => { hasUppercase = v }' :input='input'>
Has Upper
</regex-validation>
Is Valid: {{ isValid }}
</ul>
now that the reference to the computed property isValid is inside the template, it should update accordingly.
Updated the fiddle here: jsfiddle

#HostBinding disabling class from child component Angular 4

I have an angular app which has user login and logout. I am showing up a welcome page as the home page before a user logs in. I want to enable a background image only on the welcome page. Once the user logs in, the background image must disappear. When the user logs out, he will be redirected to welcome page which must show up the background image again.
I have tried using #HostBinding in the app.component.ts by renaming the selector to 'body'.
app.component.ts
import {Component, HostBinding, Input} from '#angular/core';
import {InputMask} from "primeng/primeng";
#Component({
selector: 'body',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
path = '../assets/img/AvayaHome.jpg';
title = 'app';
toggleClass = true;
#HostBinding('class.bodyClass') isWelcomePage = this.toggleClass;
}
Here is my CSS.
app.component.css
.bodyClass {
background-image: url("../assets/img/AvayaHome.jpg");
}
Here is my index.html
<!doctype html>
<html lang="en">
<head>
<title> Something </title>
</head>
<body class="bodyClass">
<app-welcome-page></app-welcome-page>
</body>
</html>
I am enabling the css style for bodyClass by assigning toggleClass as true. Once the user logs in, I am changing the value of toggleClass (which is in the app.component.ts) from the child component.
Here is my login.component.ts
onLogin() {
console.log('onLogin() invoked:', this._email, ':' , this.password);
if (this._email == null || this.password == null) {
this.errorMessage = 'All fields are required';
return;
}
this.errorMessage = null;
this.loginservice.authenticate(this._email, this.password);
this.appComponent.toggleClass = true;
this.router.navigate(['/dashboard']);
}
The value of the toggleClass changes when the user logs in to FALSE. But I am still seeing the background image. Not sure what I am doing wrong. Any help will be appreciated.
As an example, let's take a look at this code:
var toggleClass = false;
var isWelcomePage = toggleClass;
console.log(isWelcomePage); // prints true
Cool, all works as expected.
Ten seconds later....
Some user logins:
toggleClass = true;
console.log(isWelcomePage); // prints false
Why it has not changed???
If you open any documentation or any book about javascript you can read one main rule:
Primitives are always immutable.
When we assign toggleClass variable to isWelcomePage variable using =, we copy the value to the new variable because primitives are copied by value.
Solution 1:
Change isWelcomePage property directly
onLogin() {
...
this.appComponent.isWelcomePage = true;
...
}
Solution 2
Define getter
#HostBinding('class.bodyClass')
get isWelcomePage() {
return this.toggleClass;
}
Make a function with if and else;
if (user is login) {
document.body.classList.add('bodyClass');
} else {
document.body.classList.remove('bodyClass');
}
Than call that function when ever you need, logIn logOut etc
If you want to dynamically display and hide a background you should use a conditional class with ngClass
You can read more about it here NgClass
In your case it would be
<div [ngClass]="{'bodyClass': isWelcomePage}">
...
</div>
Then bodyClass css class will only apply IF isWelcomePage is true, if it's false it won't apply and the image won't show.
Edit:
As requested, a working example: Plunkr
Hostbinding only binds stuff to host tag, in your case tag.
So if you want to manipulate the body tag, you have to do it using plan javascript from your component or also create a component in the body.
#Component({
selector: 'body',
template: `<child></child>`
})
export class AppComponent {
#HostBinding('class') public cssClass = 'class1';
}

Categories

Resources