Dynamic routing list items - javascript

I have the following code
(tags component ts)
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute, Params } from '#angular/router';
#Component({
selector: 'app-tags',
templateUrl: './tags.component.html',
styleUrls: ['./tags.component.css']
})
export class TagsComponent implements OnInit{
Tags= [
'red',
'blue',
'purple'
];
red : boolean
blue : boolean
purple : boolean
constructor(private route:ActivatedRoute ) {}
ngOnInit(){
this.Tags= this.route.snapshot.params['name']
this.route.params
.subscribe((params: Params) => {
this.red = this.Tags.includes ('red');
this.purple = this.Tags.includes ('purple');
this.blue = this.Tags.includes ('blue');
}
);
}
}
(tag component html)
<ul>
<a><li *ngIf="red">red</li></a>
<a><li *ngIf="blue">blue</li></a>
<a><li *ngIf="purple">purple</li></a>
</ul>
(app module)
const appRoutes: Routes= [
{path:'', component: AppComponent},
{path:'tags/:name', component: TagsComponent}
];
Right now when I write the URL with the colour name in it only the colour mentioned would appear in the list item which is exactly what I want to do.
example
Now everything is hardcoded I want to be able to write any other colour that is not in my array like green and get the array to update and my list items to show accordingly.
I am quite new to this so I know my question might be a bit basic but any help is appreciated
Thank you

If in your example you want to format the url like that with a , separating the tag params then split on the , and use a ngFor loop in the html to render each tag.
Try this
export class TagsComponent implements OnInit {
constructor(private route: ActivatedRoute) { }
public tags: string[];
public ngOnInit(): void {
this.setTags(this.route.snapshot.params);
this.route.params.subscribe((params) => this.setTags(params));
}
private setTags(params): void {
if (!params || !params['name']) { return; }
this.tags = params['name'].split(',');
}
}
<ul *ngIf="tags && tags.length">
<a *ngFor="let tag of tags">
<li>{{tag}}</li>
</a>
</ul>

You caon try something like this:
this.route.params.subscribe(params => {
console.log(params['name']);
});

Related

How do you filter an Observable with form input?

I have a component with this "countries$" variable:
countries$!: Observable<Country[]>;
that I'm populating with this data in an "ngOnInit" like this:
ngOnInit(){
this.countries$ = this.apiService.getAllCountries();
}
and I'm accessing this variable/Observable in the html template like this:
<div>
<app-country-card *ngFor="let country of countries$ | async" [country]="country"></app-country-card>
</div>
I want to include a search bar that filters the countries down to whatever is typed in.
I thought I could use the filter function inside a pipe like this:
searchFilterCountries(searchTerm: string){
this.countries$.pipe(filter((country: any) => country.name.common.toLowerCase().includes(searchTerm.toLowerCase())))
}
and put the input in the html template like this:
<input type="text" class="form-control" (input)="searchFilterCountries($event.target.value)"/>
so that the filter function would fire every time theres an input, narrowing down the list of countries on display.
This doesn't work however. I'm getting the typescript error:
Object is possibly 'null'.ngtsc(2531)
Property 'value' does not exist on type 'EventTarget'.ngtsc(2339)
Then I found a "sample" of a working filtered list here on Material UI
https://material.angular.io/components/autocomplete/examples (The FILTER one)
I attempted to implement this and came up with this code:
export class HomeComponent {
countries$!: Observable<Country[]>;
myControl = new FormControl('');
constructor(private apiService: ApiService) { }
ngOnInit(){
this.countries$ = this.apiService.getAllCountries();
}
private _filter(value: string): Observable<Country[]> {
const filterValue = value.toLowerCase();
return this.countries$.pipe(filter(option => option.name.common.toLowerCase().includes(filterValue))) <----ERROR #2
}
}
It doesn't work however. I think because the values are observables, not the data inside the observable.
I have squiggly lines showing a TS error under the under the "name" property in "option.name.common" saying:
option.name.common TS error
Property 'name' does not exist on type 'Country[]'
If I do this instead though:
option => option[0].name.common.toLowerCase().includes(filterValue)))
the error goes away, but I wouldn't be able to search all the values if I did that.
Am I on the right track here? Am I using the right operators? How do I fix the TS errors? I'm new to angular and don't know all the operators available. If I use mergeMap/switchMap will that solve my problem? If I do fix the typescript errors would it even work? Or is my approach wrong?
Can somebody help me get this working?
I would like to expand on your current code and suggest some changes like this:
export class HomeComponent {
allCountries: Country[] = [];
countries$!: Observable<Country[]>;
myControl = new FormControl('');
constructor(private apiService: ApiService) {}
ngOnInit() {
this.apiService
.getAllCountries()
.subscribe((countries) => (this.allCountries = countries));
this.countries$ = combineLatest({
searchTerm: this.myControl.valueChanges.pipe(startWith('')),
countries: this.apiService
.getAllCountries()
.pipe(tap((countries) => (this.allCountries = countries))),
}).pipe(map(({ searchTerm }) => this._filter(searchTerm)));
}
private _filter(value: string | null): Country[] {
if (value === null) {
return this.allCountries;
}
const filterValue = value?.toLowerCase();
return this.allCountries.filter((country) =>
country.name.common.toLowerCase().includes(filterValue)
);
}
}
So we're keeping the original country list in a separate variable, and we are using the form control's valueChange event to filter the countries that we need to display.
The template should look like this:
<input type="text" [formControl]="myControl" />
<div *ngFor="let country of countries$ | async">
<div>Name: {{ country.name.common }}</div>>
</div>
Example pipe
import { Pipe, PipeTransform } from '#angular/core';
import { Country } from './country';
#Pipe({
name: 'filterList',
})
export class FilterListPipe implements PipeTransform {
transform(countries: Country[]|null, searchText: string): Country[] {
if(!countries) return []
return countries.filter(country=>country.name.indexOf(searchText) != -1);
}
}
app.component.html
<form [formGroup]="controlsGroup">
<input type="text" formControlName="searchInput"/>
<div *ngFor="let country of countries | async | filterList:searchText">
<div>Name: {{country.name}}</div>
<div>Ranking: {{country.ranking}}</div>
<div>Metric: {{country.metric}}</div>
</div>
</form>
app.component.ts
import { Component } from '#angular/core';
import { FormBuilder, FormControl, FormGroup } from '#angular/forms';
import { Observable, of } from 'rxjs';
import { Country } from './country';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'piper-example-app';
searchText = ''
controlsGroup: FormGroup
constructor(public fb:FormBuilder){
this.controlsGroup = fb.group({
searchInput: new FormControl('')
})
this.controlsGroup.get('searchInput')?.valueChanges.subscribe(value => this.searchText=value)
}
countries: Observable<Country[]> = of([{
name: 'United States of America',
ranking: 1,
metric: 'burgers per capita'
},
{
name: 'China',
ranking: 9000,
metric: 'power level lower bound'
}])
}
Admittedly I'm doing a few things that are "dirty" here where filtering the incoming observable stream of arrays of countries might be a bit more efficient. Also note you'd need to still expand the filter function to check all the properties (can use for(prop in obj) type loop to iterate over all properties to see if any of them matches the searchText or adjust the criteria as see fit.
Bit more of a complete example showing the filter part with different types of properties being filtered slightly differently:
filter-list.pipe.ts (alternative)
import { Pipe, PipeTransform } from '#angular/core';
import { Country } from './country';
#Pipe({
name: 'filterList',
})
export class FilterListPipe implements PipeTransform {
transform(countries: Country[]|null, searchText: string): Country[] {
if(!countries) return []
return countries.filter(country => {
let foundMatch = false;
let property: keyof typeof country
for(property in country) {
if(typeof country[property] === 'string') {
if((country[property] as string).indexOf(searchText) != -1)
foundMatch = true
}else {
if((country[property] as number) == parseInt(searchText))
foundMatch = true
}
}
return foundMatch
});
}
}

How to use same functionalities of function present in one component to another based on click to use same class binding in both components in angular

I'm using class binding to add and remove classes based on click of sidebar button to minimize and maximize the sidebar.
I need same functionality to add and remove class in footer component also to maximize and minimize width of the footer based on click made in sidebar component.
sidebar.component.html
<nav class="sidebar-container" [ngClass]="minimize ? 'minimize' : ''">
<ul>
<li class="sidebar-minimize">
<a (click)="clickEvent()">Minmax</a>
</li>
</ul>
</nav>
sidebar.component.ts
import { Component, OnInit } from '#angular/core';
import { Router } from '#angular/router';
#Component({
selector: 'app-sidebar',
templateUrl: './sidebar.component.html',
styleUrls: ['./sidebar.component.scss']
})
export class SidebarComponent implements OnInit {
minimize: boolean = false;
ngOnInit(): void {
}
clickEvent() {
this.minimize = !this.minimize;
}
}
But i need same actions to be performed in footer component to add another CSS class through class binding followed by same clickEvent() method because in my situation sidebar component and footer components are interdependent on this clickEvent() function
For eg,
<footer (click)="clickEvent()" [ngClass]="minimize ? 'minimize':''">
<p>footer works</p>
</footer>
Is it possible to use same clickEvent() function in footer component for class binding
So in this case you can use a service. For example in your service you have a property like below:
#Injectable({
providedIn: 'root'
})
export class TestService {
private minimize: boolean;
public minimizeSubject: Subject<void>;
constructor() {
this.minimizeSubject = new Subject<void>();
this.minimize = false;
}
set setMinimize(minimize: boolean) {
this.minimize = minimize;
this.minimizeSubject.next();
}
Sidebar component should look like below:
export class SidebarComponent{
public minimize: boolean;
constructor(private testService: TestService) {
this.minimize = false;
}
public onClicked(): void {
this.minimize = !this.minimize;
this.testService.setMinimize = this.minimize;
}
In the end the footer component:
export class FooterComponent implements OnInit, OnDestroy {
private subscription: Subscription;
constructor(private testService: TestService) {
this.subscription = new Subscription();
}
ngOnInit(): void {
this.subscription = this.testService.minimizeSubject.subscribe(minimize => {
this.minimize = minimize;
});
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}

Data not passing through with route in Angular 6

I have a application with a table of cars:
This is my code:
Carcomponent.html
<tbody>
<tr *ngFor="let car of allCars; index as carId" \>
<td [routerLink]="['/cars', carId]">{{car.carId}}</td>
<td>{{car.brand}}</td>
<td>{{car.model}}</td>
<td>{{car.color}}</td>
<td>{{car.topSpeed }}</td>
</tr>
</tbody>
I have register the route like this:
{ path: 'cars/:carId', component: CardetailsComponent }
And this is my CarDetails.ts file:
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { CarVM } from '../viewmodels/car-vm';
import { CarService } from '../services/car.service';
#Component({
selector: 'app-cardetails',
templateUrl: './cardetails.component.html',
styleUrls: ['./cardetails.component.css']
})
export class CardetailsComponent implements OnInit {
car: any;
carList: any;
constructor(private route: ActivatedRoute, private carservice: CarService) { }
ngOnInit() {
this.route.paramMap.subscribe(params => {
this.car = params.get('carId');
});
}
getCarList() {
this.carList = new CarVM();
this.carservice.getCarById(this.carList.carId).subscribe((res: any) => {
this.carList = res.data;
console.log(this.carList)
})
}
}
And on my Cardetails.html I want to show the selected car like this:
<h2>Car Details</h2>
<div *ngIf="car">
<h3>{{ car.brand }}</h3>
<h4>{{ car.model }}</h4>
<p>{{ car.color }}</p>
</div>
The routing is working fine and fetching the cars is working. Now I want to select one car and see the brand, model, color on the next page. I use a viewmodel for this:
export class CarVM {
CarId: number;
Brand: string;
Model: string;
Color: string;
TopSpeed: number;
}
How can I see the selected car on the next page?
I have followed this tutorial:
https://angular.io/start/routing
Ok, you seem to be bit confused. In cardetails component you want to process carId from route parameters and use it to get car details. You can either get them from server, or have the service return already loaded details of all cars.
Let's say we are trying to make it happen getting the first way, it might look like this:
import { map, switchMap } from 'rxjs/operators';
ngOnInit() {
this.getCar();
}
private getCar(): void {
this.route.paramMap.pipe(
map(params => params.get('carId')),
switchMap(carId => {
return this.carservice.getCarById(carId);
})
).subscribe(
res => {
this.car = res;
console.log('#My car:', this.car);
}
);
}
First, you'll get the carId from route.paramMap, map it using rxjs map, then use switchMap to call you carservice.getCarById(carId) and have it return Observable to which you can subscribe. This should do the trick. Don't forget to properly map it/create CarVM object from it.
The problem is, you don't have CarVM object properly on CardetailsComponent. You are only getting carId into CarVM here: this.car = CarVM[+params.get('carId')];
First you need to create CarVM properly with your class variables. And the you can call your index.
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { CarVM } from '../viewmodels/car-vm';
#Component({
selector: 'app-cardetails',
templateUrl: './cardetails.component.html',
styleUrls: ['./cardetails.component.css']
})
export class CardetailsComponent implements OnInit {
car: any;
carList: any;
constructor(private route: ActivatedRoute) { }
ngOnInit() {
this.route.paramMap.subscribe(params => {
this.car = params.get('carId');
});
}
getCarList(){
this.carList = new CarVM();
//call your service here to fill your carList variable and once you get car list, you will be able to access variable using with your index (this.car).
}
}

Angular 6 filter with breadcrumb shows error

I have done one application and I used breadcrumb in my application.
My code is like this:
import { Component, OnInit, ViewEncapsulation } from '#angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '#angular/router';
import { BreadCrumb } from './breadcrumb';
import { map, filter} from 'rxjs/operators';
#Component({
selector: 'app-breadcrumb',
templateUrl: './breadcrumb.component.html',
styleUrls: ['./breadcrumb.component.css'],
encapsulation: ViewEncapsulation.None
})
export class BreadcrumbComponent implements OnInit {
breadcrumbs$ = this.router.events
.filter((event) => event instanceof NavigationEnd)
.distinctUntilChanged()
.map(event => this.buildBreadCrumb(this.activatedRoute.root));
// Build your breadcrumb starting with the root route of your current activated route
constructor(private activatedRoute: ActivatedRoute,
private router: Router) {
}
ngOnInit() {
}
buildBreadCrumb(route: ActivatedRoute, url: string = '',
breadcrumbs: Array<BreadCrumb> = []): Array<BreadCrumb> {
// If no routeConfig is avalailable we are on the root path
const label = route.routeConfig ? route.routeConfig.data[ 'breadcrumb' ] : 'Home';
const path = route.routeConfig ? route.routeConfig.path : '';
// In the routeConfig the complete path is not available,
// so we rebuild it each time
const nextUrl = `${url}${path}/`;
const breadcrumb = {
label: label,
url: nextUrl
};
const newBreadcrumbs = [ ...breadcrumbs, breadcrumb ];
if (route.firstChild) {
// If we are not on our current path yet,
// there will be more children to look after, to build our breadcumb
return this.buildBreadCrumb(route.firstChild, nextUrl, newBreadcrumbs);
}
return newBreadcrumbs;
}
}
BreadCrumbs HTML is this:
<ol class="breadcrumb">
<li *ngFor="let breadcrumb of breadcrumbs$ | async"
class="breadcrumb-item">
<a [routerLink]="[breadcrumb.url, breadcrumb.params]">
{{ breadcrumb.label }}
</a>
</li>
</ol>
If I compile this then it shows this error:
ERROR in src/app/share/components/breadcrumb/breadcrumb.component.ts(20,4): error TS2339: Property 'filter' does not exist on type 'Observable<Event>'.
I have tried many times and searched Google but have not found the solution. Can anyone please help me and tell me where I’m wrong?
use the pipable operator
breadcrumbs$ = this.router.events.pipe(
filter((event) => event instanceof NavigationEnd),
distinctUntilChanged(),
map(event => this.buildBreadCrumb(this.activatedRoute.root))
);
Doc

Use AngularJS 2 Component like a function in the element attribute

Hi guys!
I'm learning AngularJS 2 for a while now and now creating my own app based on Laravel 5 REST API. Anyway - that isn't very important atm.
What is important is that I want to provide the translation for the whole application and I found an issue that is hard to solve for me.
So - from the beginning... I'm created my ResourcesService that's translating the string:
getTranslation ( key: string, replace: Array<TranslationReplace> = null, locale: string = null, fallback: boolean = null ): Observable<Resource> {
var params = "key=" + key +
( replace ? "&replace=" + JSON.stringify(replace) : '') +
( locale ? "&locale=" + locale : '') +
( fallback ? "&fallback=" + fallback : '');
var headers = new Headers({'Content-Type':'application/x-www-form-urlencoded'});
return this.http.post(this.apiUrl + 'getTranslation', params, {headers: headers})
.map(this.extractData)
.startWith({ name: 'Loading...', value: 'Translating...' })
.catch(this.handleError);
}
And I created a TranslateComponent that's providing the translation, here's the whole component:
import {Component, Input, Injectable, OnInit, OnChanges, SimpleChange} from "#angular/core";
import {ResourcesService} from "../services/resources.service";
import {TranslationReplace} from "../models/TranslationReplace";
#Component({
selector: 'translate',
template: `{{translation}}`
})
#Injectable()
export class TranslateComponent implements OnInit, OnChanges {
#Input() ref: string;
#Input() replace: Array<TranslationReplace>;
#Input() locale: string;
#Input() fallback: boolean;
private translation: string;
constructor(private resourcesService: ResourcesService) {}
ngOnInit() : void {
this.getTranslation();
}
ngOnChanges(changes: {[propKey: string]: SimpleChange}) {
for (let propName in changes) {
if(propName == 'replace') {
this.getTranslation();
}
}
}
private getTranslation(): void {
this.resourcesService.getTranslation(this.ref, this.replace, this.locale, this.fallback).forEach(translation => this.translation = translation.value );
}
}
All is working just perfect and to call for the translation I have to simply call the selector like that:
<translate [ref]="'string.to_translate'"></translate>
But...
Now I'd like to use the translation in the attribute.
So I found the ugly way to achieve it by creating the reference of the translation and the call it in the attribute. But it's very nasty...
First of all I need to add this bit to my template:
<translate [ref]="'string.to_translate'" style="display:none;" #myStringTranslation></translate>
And next in my element call it and ask for the property by the reference:
<input type="text" [(ngModel)]="input" #input="ngModel [placeholder]="myStringTranslation.translation">
And I really don't like the idea.
What I'm looking for is to call it somehow, I don't know... emit it? And make it looks better. Don't create extra elements.
So my question is:
Can I do it better? Can I somehow call the translation directly from the attribute without the reference?
** ----- UPDATE ----- **
Ok, I learn my lesson :) Thanks to Meir for showing me the right direction and also the Angular.io site for the tutorials.
So finally I added a TranslateDirective to my application:
import {Directive, Input, ElementRef, OnChanges, OnInit, SimpleChange, Renderer} from "#angular/core";
import {TranslationReplace} from "../models/TranslationReplace";
import {ResourcesService} from "../services/resources.service";
#Directive({
selector: '[translate]'
})
export class TranslateDirective implements OnInit, OnChanges {
#Input('translate') ref: string;
#Input('translateReplace') replace: Array<TranslationReplace>;
#Input('translateLocale') locale: string;
#Input('translateFallback') fallback: boolean;
#Input('translateAttr') attr: string;
private translation: string;
constructor(
private elRef: ElementRef,
private renderer: Renderer,
private resourcesService: ResourcesService
) {}
ngOnInit():void {
this.getTranslation();
}
ngOnChanges(changes: {[propKey: string]: SimpleChange}):void {
for (let propName in changes) {
if(propName == 'replace') {
this.getTranslation();
}
}
}
private getTranslation(): void {
if(this.attr)
this.resourcesService.getTranslation(this.ref, this.replace, this.locale, this.fallback).forEach(translation =>
{
this.translation = translation.value;
this.renderer.setElementAttribute(this.elRef.nativeElement,this.attr,this.translation);
});
}
}
And now can easily add the translations to the attributes like that:
<input type="text" [(ngModel)]="input" #input="ngModel [translate]="'string.to_translate'" [translateAttr]="'placeholder'">
Thanks for your help!!
You can turn it into an attribute directive:
#Directive({
selector: 'translate'
})
export class TranslateDirectiev {
#Input() translate: string;
constructor(private elRef: ElementRef){}
ngOnChanges(changes: SimpleChanges): void {
if(this.translate){
var translatedText: string = translateSvc.translate(this.translate);
this.renderer.setElementProperty(this.elementRef.nativeElement, 'innerHTML', translatedText);
}
}
}
This is a simple example without the service injection. Also, for input fields you might need to have a different approach and update the value attribute and not the innerHtml

Categories

Resources