Checking an event only in one page of the app in angular2 - javascript

i'm developing my first app in angular 4.0.
My problem is pretty simple but i would like to know if there is a best practice to solve my issue.
I have an header with position: 'fixed' and when the user scrolls the page this element changes some of its properties (height, background-size..) by adding a 'small' class dynamically.
This is my component
import {Component, OnInit, HostListener} from '#angular/core';
#Component({
selector: 'my-header',
templateUrl: './my-header.component.html',
styleUrls: ['./my-header.component.css']
})
export class HeaderComponent implements OnInit {
scrollState: boolean;
constructor() { }
ngOnInit() {
this.scrollState = false;
}
#HostListener('window:scroll', [])
toggleScrollState() {
if(window.pageYOffset == 0){
this.scrollState = false;
}
else{
this.scrollState = true;
}
}
}
and this the html
<header class="bk-blue clearfix" [ngClass]="{small: scrollState}">
<a class="sx" href="#">Login</a>
<a class="dx arrow-down"></a>
<a class="dx" href="#">It</a>
</header>
Everything works fine but this should happen only in the home page. In the other page the header element should already be in the 'small' state without any DOM manipulations based on the scroll event.
I was thinking of checking the current route to set an additional variable (false if the current route matches the home page path, true otherwise) and put that in OR with scrollState. Something like this:
<header class="bk-blue clearfix" [ngClass]="{small: notHome || scrollState}">
By doing so, however, i can't avoid calling the listener with its implications in term of reduced performance.
What is for you the best approach to avoid calling the listener even in internal pages where it is not necessary?

well, I would do this using ActivatedRouteSnapshot
#Component({...})
class Header implements OnInit {
readonly snapshot: ActivatedRouteSnapshot = null;
constructor(private route: ActivatedRoute) {
this.snapshot = route.snapshot;
}
ngOnInit() {
// if this.snapshot is HOMEPAGE
// then
// subscribe to the scroll and switch class when you need.
}
}
You could also set a property on your route.data which tells you to animate or not the header.
const routes: Routes = [
{
path: '',
component: HomeRouteComponent,
data: { ANIMATE_HEADER: true }
}
];
// header.ts
ngOnInit() {
if(this.snapshot.data.ANIMATE_HEADER) {
// do stuff here
}
}

Related

Expression Changed After it has Been Checked When Using #HostBinding

I created a breadcrumb component, I have a service BreadcrumbService that has a function that reads the url path and converts them to an array of segments (this.breadService.getUrlPathSegments()). When breadcrumbs is loaded or updated, I get the following error:
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value for 'click-enabled': 'true'. Current value: 'false'.
What is the correct way to handle this? The code works the way I intended, but I need to handle the error message.
What I am trying to do is disable click events on the last item in the breadcrumb list, so when you click on it, none of the events fire. This all works even though I receive the error message.
What I am doing is when the view is checked, update the value of each breadcurmb's clickable state. This is done just like this:
#Component({
selector: 'breadcrumbs',
styleUrls: ['./breadcrumbs.component.scss'],
template: `
<ng-content select="breadcrumb"></ng-content>
`,
encapsulation: ViewEncapsulation.None
})
export class Breadcrumbs implements AfterViewChecked {
#Input() disableLast = true;
#ContentChildren(Breadcrumb, { descendants: false })
breadcrumbs!: QueryList<Breadcrumb>;
ngAfterViewChecked() {
this.enableDisableLast();
}
enableDisableLast() {
if (this.breadcrumbs && this.breadcrumbs.length > 0) {
this.breadcrumbs.forEach(item => { item.clickable = true; });
this.breadcrumbs.last.clickable = !this.disableLast;
}
}
}
Next in the breadcrumb I have a #HostBinding(), that updates the class of the element. Which is done like this:
#Component({
selector: 'breadcrumb',
styleUrls: ['./breadcrumb.component.scss'],
template: `
<button>{{label}}</button>
`
})
export class Breadcrumb {
#HostBinding('class.click-enabled')
get clickEnabled() { return this.clickable; }
}
I then combine the two in the component that I am using them with a forEach to create the child breadcrumbs. I also listen for navigation changes to re-generate the array of breadcrumb segments to keep the breadcrumb display up-to-date with the current path.
#Component({
selector: 'app-root',
templateUrl: `
<breadcrumbs>
<breadcrumb *ngFor="let crumb of breadcrumbs" [label]="crumb.label|titlecase" [routerLink]="crumb.uri"></breadcrumb>
</breadcrumbs>
`,
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
breadcrumbs: BreadcrumbSegment[] = [];
constructor(
private router: Router,
private breadService: BreadcrumbService
) { }
ngOnInit() {
this.router.events.subscribe(val => {
if (val instanceof NavigationEnd) {
// Returns an array formatted as: {label:string; uri:string;}[]
this.breadcrumbs = this.breadService.getUrlPathSegments();
}
});
}
}
I am not sure if this is the optimal solution, but it is working for my needs. When the Breadcrumbs component view is initialized, I set the QueryList to dirty, then pipe a delay before I subscribe to the changes. This stops the error from showing up and runs the change detection.
#Component({...})
export class Breadcrumbs implements AfterViewChecked {
ngAfterViewInit() {
// Set to dirty so the changes emit at least one time.
this.breadcrumbs.setDirty();
this.breadcrumbs.changes.pipe(delay(1)).subscribe(() => {
this.enableDisableLast();
});
}
}

Angular. How to switch component depending on service's actions

Lets say I have 2 components, aComponent and bComponent. I have them redered inside the AppComponent
<app-a>
<app-b>
And I have service myService that has method .trigger().
What I want is to show only aComponent, but whenever I call myService.trigger() from another part of code, it would switch and show bComponent. That's perfect implementation that I can't reach.
Question is: Is it possible to do so? And if not what is the best closest solution.
The only working solution I got:
I added .trigger() inside AppComponent
export class AppComponent {
title = 'spa';
show: boolean = false;
trigger() {
this.show = true;
}
}
And rendered components like so:
<div *ngIf="!show; else show">
<app-a></app-a>
</div>
<ng-template #show>
<app-b></app-b>
</ng-template>
Then whenever I want to trigger switching, I add instance of the app to the constructor and call it's method:
export class AnotherComponent implements OnInit {
constructor(
private app: AppComponent
) {}
ngOnInit(): void {
this.app.trigger();
}
}
Even though it's working pretty good, I myself see that it's a dirty solution. Components are not intended to be used inside another components, but Services are.
You can use Subject from rxjs library for that.
In your service file:
// a-service.service.ts
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs';
#Injectable({ providedIn: 'root' })
export class AService {
private subject = new Subject<any>();
trigger(state: boolean) {
this.subject.next(state);
}
getTrigger(): Subject<any> {
return this.subject;
}
}
and in your AppComponent:
// app.component.ts
...
private show = false;
constructor (private aService: AService) { }
ngOnInit() {
this.aService.getTrigger().subscribe(state => {
this.show = state;
});
}
the template can be as you provided - it's fine:
<div *ngIf="!show; else show">
<app-a></app-a>
</div>
<ng-template #show>
<app-b></app-b>
</ng-template>
And if you want to trigger from another component, you do it like this:
// another.component.ts
...
constructor (private aService: AService) { }
ngOnInit() {
this.aService.trigger(true);
}
One way to communicate between different components and services which aren't directly related, is via 'Subjects'.
You can try to create a subject and pass in values to it from myService.trigger(). And you can subscribe to that subject from whichever component you want to access that trigger data.

Get data on the Component load

I have a component which needs to show the data in the grid on the component/page Load and when a button is clicked from parent component it needs refresh the grid with new data. My component is like below
export class TjlShipdateFilterComponent implements DoCheck {
tljShipDate: ShipDateFilterModel[];
constructor(private psService: ProjectShipmentService) {
}
ngDoCheck() {
// this data is from the service, trying to get it on Page load
}
#Input() filter: ShipDateFilterModel[];
//Load or refresh the data from parent when the button clicked from parent component
ngOnChanges(changes: SimpleChanges) {
}
The ngOnChanges works fine, it gets the data from the parent component and displays when the button is clicked from the parent component. But on load of the page/component the grid it doesn't show anything and says this.psService.tDate; is undefined.
Below is the service where I get the tDate
export class ProjectShipmentService {
......
constructor(service: DataService, private activatedRoute: ActivatedRoute) {
service.get<ShipDateFilterModel[]>(this.entityUrl).subscribe(x => this.tDate = x);
}
I am unsure what am I missing here. How can I achieve this scenario
It happened because when the component is loaded, the request in your service may not completed and the data may not return yet, that why tDate is undefined, try subscribe to it inside your component, also use ngOnInit() instead of ngDoCheck().
In your service:
tDate: Observable<ShipDateFilterModel[]>
constructor(service: DataService, private activatedRoute: ActivatedRoute) {
...
this.tDate = service.get<ShipDateFilterModel[]>(this.entityUrl)
}
In your component:
export class TjlShipdateFilterComponent implements OnInit, OnChanges {
tljShipDate: ShipDateFilterModel[];
constructor(private psService: ProjectShipmentService) {
}
ngOnInit() {
// this data is from the service, trying to get it on Page load
this.psService.tDate.subsribe(x => this.tljShipDate = x);
}
#Input() filter: ShipDateFilterModel[];
//Load or refresh the data from parent when the button clicked from parent component
ngOnChanges(changes: SimpleChanges) {
if (changes.filter && changes.filter.currentValue)
{
this.tljShipDate = this.filter;
}
}
}
You have a couple options here.
NgOnInit will run when the component is created, before it is rendered. This is the most common way to load data on component initialization.
If you need the data even before the component is initialized, then you may need to utilize a Resolver.
Here's an example:
import { Injectable } from '#angular/core'
import { HttpService } from 'services/http.service'
import { Resolve } from '#angular/router'
import { ActivatedRouteSnapshot } from '#angular/router'
#Injectable()
export class DataResolver implements Resolve<any> {
constructor(private http: HttpService) { }
resolve(route: ActivatedRouteSnapshot) {
return this.http.getData(route.params.id);
}
}
Then, in your route config:
{
path: 'data/:id',
component: DataComponent,
resolve: { data: DataResolver }
}
The inclusion of the ActivatedRouteSnapshot is optional, you only need it if you're using route data, like params.
Edit:
Looking at your example closer, is it possible that the ngDoCheck is firing before the psService subscription does?

Accessing child component's method when nested within another component

I'd like to be able to access the SearchResults component, (when it has been clicked), in the root component (AppComponent) as I'm looking to set different properties on the SearchResults component such as;
I'd like to set an attribute on the SearchResults component so that it shows the "close" text
Also, I'd to set the click event on the SearchResults to redirect elsewhere or actually enable it as a multi-select so that it stays selected until a user proceeds to the next step for example.
I'm trying to make the SearchResults and SearchResult components as re-usable as possible so we're able to state in the parent component which would include the <app-searchresults> selector what action we'd like our SearchResults components to actually be when they are clicked.
The only way I can really see doing this is using EventEmitter to pass the event up once through the SearchResult component then onto the parent component and then a Service to hold selected values but I'm still stuck around enabling the SearchResults component as either a component which redirects when clicked or stays selected? Is this actually possible or do I need to create a different SearchResults component for each different state I'd like?!
export class AppComponent {
#ViewChildren(SearchresultComponent) components: QueryList<SearchresultComponent>;
name = 'Angular';
ngAfterViewInit() {
this.components.changes.subscribe((r) => { console.log(r) });
}
}
SearchResults.ts
#Component({
selector: 'app-searchresults',
templateUrl: './searchresults.component.html',
styleUrls: ['./searchresults.component.css']
})
export class SearchresultsComponent implements OnInit {
#ViewChildren(SearchresultComponent) components: QueryList<SearchresultComponent>;
constructor() { }
ngOnInit() {
}
}
SearchResults.html
<h1>Search Results<h1>
<app-searchresult result ="first"></app-searchresult>
<app-searchresult result ="second"></app-searchresult>
<app-searchresult result ="third"></app-searchresult>
SearchResult.ts
#Component({
selector: 'app-searchresult',
templateUrl: './searchresult.component.html',
styleUrls: ['./searchresult.component.css']
})
export class SearchresultComponent implements OnInit {
#Input()
result: string;
isSelected: boolean;
constructor() { }
ngOnInit() {
}
toggleClickedState(){
if(!this.isSelected){
this.isSelected = !this.isSelected;
}
}
}
SearchResult.html
<div>
<p (click)=toggleClickedState() [ngClass]="isSelected? 'selected' : '' "> Search Result : {{result}}</p>
<p *ngIf="isSelected" class="cross" (click)="isSelected = false;">close</p>
<div>
I've included a link to structure of an app that references the above;
https://stackblitz.com/edit/angular-cjhovx

Angular 4/5 Observable - How to update component template depending on observable property?

Ok I am still a newbie. I have successfully created a 'dashboard' component that has a left sidebar with links. On the right is where I have content/components displayed that I want to change dynamically depending on what link was clicked on the left sidebar (see bootstrap sample of what this dashboard looks like here, click on the toggle button to view the sidebar: https://blackrockdigital.github.io/startbootstrap-simple-sidebar/).
I have created a DashboardService that has a Subject and an Observable to allow for sibling component communication. This works great since I have a console.log() that shows this communication working (when I click on link on sidebar in SidebarComponent, I console.log() a value 'emitted' by the DashboardService that is being listened to by the SidebarComponent's sibling, DashboardSectionComponent).
The problem that I am having is that the template in DashboardSectionComponent loads the correct component section ONLY on initial load of page - once I click on a link on the side bar the content is blank and nothing is rendered.
Here is the service that allows the componenent communication:
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
#Injectable()
export class DashboardService {
private selectedComponentAlias = new Subject<string>();
constructor() {}
setSelectedComponentAlias(alias: string) {
this.selectedComponentAlias.next(alias);
}
getSelectedComponentAlias(): Observable<string> {
return this.selectedComponentAlias.asObservable();
}
}
Here is the SidebarComponent:
import { Component, OnInit } from '#angular/core';
import { DashboardService } from '../dashboard.service';
#Component({
selector: 'app-sidebar',
templateUrl: './sidebar.component.html',
styleUrls: ['./sidebar.component.css']
})
export class SidebarComponent implements OnInit {
constructor(private dashboardService: DashboardService) { }
ngOnInit() {
}
onShowSection(event) {
event.preventDefault();
const componentAlias = event.target.getAttribute('data-componentAlias');
this.dashboardService.setSelectedComponentAlias(componentAlias);
}
}
here is the DashboardSectionComponent (the one that subscribes to the observable and I want to set property that controls the template views depending on the value that was caught)
import { Component, OnInit, OnDestroy } from '#angular/core';
import { Subscription } from 'rxjs/Subscription';
import { DashboardService } from '../dashboard.service';
#Component({
selector: 'app-dashboard-section',
templateUrl: './dashboard-section.component.html',
styleUrls: ['./dashboard-section.component.css']
})
export class DashboardSectionComponent implements OnInit, OnDestroy {
private subscrition: Subscription;
selectedComponentAlias: string = 'user-profile';
constructor(private dashboardService: DashboardService) {
}
ngOnInit() {
this.subscrition = this.dashboardService.getSelectedComponentAlias()
.subscribe((selectedComponentAlias: string) => {
this.selectedComponentAlias = selectedComponentAlias;
console.log('user clicked: ',this.selectedComponentAlias);
});
}
ngOnDestroy() {
this.subscrition.unsubscribe();
}
}
Finally here is the template for DashboardSectionComponent which might have wrong syntax:
<div *ngIf="selectedComponentAlias == 'my-cards'">
<app-cards></app-cards>
</div>
<div *ngIf="selectedComponentAlias == 'user-profile'">
<app-user-profile></app-user-profile>
</div>
<div *ngIf="selectedComponentAlias == 'user-settings'">
<app-user-settings></app-user-settings>
</div>
Again, this works great (selectedComponentAlias is 'user-profile' on page load by default). But it goes blank after I click on a Sidebar link....
Thanks.
this was easy - like #RandyCasburn pointed out, this was a matter of getting the routing working properly.

Categories