I'm working on my first Angular2 project (building a Pokemon web app), and I keep getting the following error messages when trying to load the page:
failed to compile.
/home/mattlayton1986/workspace/pokedex/src/app/pokedex.service.ts (26,20): Cannot find name 'p'.
/home/mattlayton1986/workspace/pokedex/src/app/pokedex.service.ts (26,23): Cannot find name 'i'.
The error is occurring inside my ./pokedex-service.ts' file, which loads the data from the API and gets injected into the component. Here is all the code in myPokedexService` file:
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import 'rxjs/add/operator/toPromise';
#Injectable()
export class PokedexService {
private baseUrl: string = 'https://pokeapi.co/api/v2/pokemon/';
private baseSpriteUrl: string = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/';
constructor(private http: Http) { }
getPokemon(offset: number, limit: number) {
return this.http.get(
`${this.baseUrl}?offset=${offset}&limit=${limit}`
).toPromise().then(
response => response.json().results
).then(items => items.map(
(poke, idx) => {
const id: number = idx + offset + 1;
return {
id,
name: poke.name,
sprite: `${this.baseSpriteUrl}${id}.png`
};
}
).map(getTypes(p, i))); // <-- error occurs here
}
}
function getTypes(pokemon, id) {
return this.http.get(
`${this.baseUrl}${id}`
).toPromise().then(
response => response.json().results
).then(item => item.map(
poke => {
return {
poke,
type1: poke.types.type[0].name,
type2: poke.types.type[1].name
}
}
));
}
For reference, in case it helps, here is my main component and its template, which makes use of the service to load Pokemon data:
app.component.html
<h1>Angular 2 Pokedex</h1>
<span>This is a sample app using Angular 2 RC5. Check out the source code here</span>
<hr />
<div class="pokedex">
<div class="pokedex-pokemon" *ngFor="let p of pokemon" [pokemonTypes]="p">
<div class="pokedex-pokemon-id">
{{p.id}}
</div>
<img [ngClass]="{'hidden': !p.imageLoaded}" class="pokedex-pokemon-sprite" (load)="p.imageLoaded = true" [attr.src]="p.sprite" />
<div class="pokedex-pokemon-name">
{{ p.name | capitalize }}
</div>
<div class="pokedex-pokemon-type1">
{{ p.types.type1 }}
</div>
<div calss="pokedex-pokemon-type2">
{{ p.types.type2 }}
</div>
</div>
</div>
<button class="load-button" (click)="loadMore()" [disabled]="isLoading">
<span *ngIf="!error">
<span *ngIf="isLoading">Loading...</span>
<span *ngIf="!isLoading">Load more</span>
</span>
<span *ngIf="error">
Loading failed
</span>
</button>
app.component.ts
import { Component } from '#angular/core';
import { OnInit } from '#angular/core';
import { PokedexService } from './pokedex.service';
import { Pokemon } from './pokemon';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
pokemon: Pokemon[] = [];
isLoading: boolean = false;
error: boolean = false;
constructor(private pokedexService: PokedexService) {}
ngOnInit() {
// Loads the initial data.
this.loadMore();
}
loadMore() {
this.isLoading = true;
// User the Pokedex service
// to load the next 9 Pokemon.
this.pokedexService.getPokemon(this.pokemon.length, 9)
.then(pokemon => {
pokemon = pokemon.map(p => {
p.imageLoaded = false;
return p;
});
this.pokemon = this.pokemon.concat(pokemon);
this.isLoading = false;
this.error = false;
})
.catch( () => {
this.error = true;
this.isLoading = false;
});
}
}
I've called my map function's callback with a parameter for the current item in the array I'm mapping and the index of that item, and then included formal parameters for both in the function definition of getTypes, so I'm not sure where the error is coming from or how to resolve it. Any help in clearing this up is greatly appreciated.
You aren't defining the parameters p or i.
What you want to do is:
.map((p, i) => getTypes(p, i));
passing a function to map that has both p and i defined in it's scope.
In your case you're mapping over the result of another map (which returns an array) so you would want to destructure the array as below:
.map(([p, i]) => getTypes(p, i));
Which will take the array and split the variable assignments out for you. The same as doing:
.map(arr => {
const p = arr.p;
const i = arr.i;
return getTypes(p, i);
});
but a lot more succinct.
I think it should be
).map(v => getTypes(v.name, v.id))); // <-- error occurs here
Related
I'm finishing my pokedex and one of the things that's been bothering me is the fact that the cards that angular creates with ngFor are coming out of order. I'm just starting to use Angular and I wanted some help with this.
demo - https://pokedex-jrsbaum.vercel.app
HTML
<div class="container">
<div class="row">
<div class="col-md-4 ml-2 mt-2"
*ngFor="let pokemon of pokemons" >
<div class="card-container">
<div class="card">
<div
class="front"
[ngStyle]="{
'background-color': getColors(pokemon.types[0]?.type.name)
}"
>
TS
import { Component, OnInit } from '#angular/core';
import { PokemonService } from 'src/app/services/pokemon.service';
#Component({
selector: 'app-pokemon-list',
templateUrl: './pokemon-list.component.html',
styleUrls: ['./pokemon-list.component.css'],
})
export class PokemonListComponent implements OnInit {
pokemons: any[] = [];
species: any[] = [];
constructor(public pokemonService: PokemonService) {}
ngOnInit(): void {
this.pokemonService.getPokemons().subscribe((response: any) => {
response.results.forEach((result: { name: string }) => {
this.pokemonService
.getDetails(result.name)
.subscribe((uniqResponse: any) => {
this.pokemons.push(uniqResponse);
});
this.pokemonService
.getSpecies(result.name)
.subscribe((uniqResponse: any) => {
this.species.push(uniqResponse);
});
});
});
}
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root',
})
export class PokemonService {
constructor(private http: HttpClient) {}
//Get Pokemons
getPokemons() {
return this.http.get(`https://pokeapi.co/api/v2/pokemon?limit=151`);
}
//Get More Pokemon Data
getDetails(name: string) {
return this.http.get(`https://pokeapi.co/api/v2/pokemon/${name}`);
}
getSpecies(name: string) {
return this.http.get(`https://pokeapi.co/api/v2/pokemon-species/${name}/`);
}
}
What I think is happening is that ngFor is making the call and stacking the cards, and the ones that are ready it releases before.
*ngFor orders array elements based on array indices. In your code, values pushed into pokemons array are fetched from some service call, which returns results asynchronously. This explains the random ordering.
To solve this, one solution is to use the array indices when inserting values into pokemons array.
Replace
this.pokemons.push(uniqResponse);
with
const index = uniqResponse.id - 1;
this.pokemons[index] = uniqResponse;
Check out my StackBlitz Demo. You can see the pokemon Ids displayed in order.
this problem is driving me crazy.
I have an array defined within a service, which is used in 3 other components:
This is the service, file products.service.ts (notice the product array of Products)
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Product } from './../models/Product';
import { ProductForm, productFormToProduct } from './../models/ProductForm';
// #Injectable({
// providedIn: 'root'
// })
const apiUrl = 'http://localhost:3000/products';
#Injectable()
export class ProductsService {
public products: Product[] = [];
constructor(private http: HttpClient) {}
getProducts() {
return this.http.get(apiUrl)
}
deleteProduct(p: Product) {
// this.products = this.products.filter(prod => prod.id !== p.id);
const i = this.products.indexOf(p);
this.products.splice(i,1);
return this.http.delete(apiUrl + "/" + p.id)
}
storeNewProduct(pf: ProductForm) {
const idList = this.products.map((x) => {return x.id});
const i = Math.max(...idList) + 1;
const p = productFormToProduct(pf);
p.id = i;
this.products.push(p);
return this.http.post(apiUrl, p)
}
}
This is the component where i subscribe to getProducts, and fill the array (file products.component.ts):
import { Component, OnInit } from '#angular/core';
import { ProductsService } from '../../shared/services/products.service';
import { Product } from '../../shared/models/Product';
#Component({
selector: 'app-products',
templateUrl: './products.component.html',
styleUrls: ['./products.component.scss']
})
export class ProductsComponent implements OnInit {
products: Product[] = [];
searchText: string = "";
constructor(private productsService: ProductsService) {}
ngOnInit(): void {
this.productsService.getProducts()
.subscribe((data: Product[]) => {
this.productsService.products = data;
this.products = this.productsService.products;
})
}
}
And this is the component where i subscribe to deleteProduct (file product-card.component.ts):
import { Component, Input, OnInit } from '#angular/core';
import { ProductsService } from '../../services/products.service';
import { Product } from './../../models/Product';
#Component({
selector: 'app-product-card',
templateUrl: './product-card.component.html',
styleUrls: ['./product-card.component.scss']
})
export class ProductCardComponent implements OnInit {
constructor(private productsService: ProductsService) {}
ngOnInit(): void {
}
#Input() product: Product
public buttonDeleteFunction() {
this.productsService.deleteProduct(this.product).subscribe();
}
}
The problem is, when i click on some delete product button, i have this weird behaviour:
Before click:
After click:
Here is the products.component.html file:
<div class="products__header">
<h3 class="products__heading">
Listado de productos ({{ products.length }})
</h3>
<input
class="products__search"
placeholder="Buscador"
type="search"
[(ngModel)]="searchText"
/>
</div>
<p *ngFor="let p of products">{{ p.name }}</p>
<p>{{ products }}</p>
<div class="products__list">
<app-product-card
*ngFor="let p of products | filterNames: searchText"
[product]="p"
></app-product-card>
</div>
Why do i get the expected behaviour in only two of the four places where i use the products list?
I know i can use an Output to manually remove the item from the list when i click the button, but i have been told that services are used instead of Inputs/Outputs when i want to share between multiple components, so i'd rather not use an Output for this
When you use your approach with common data on service layer then a common pitfall is that Angular does not detect the changes that affect your component. In that case you must inform your component for those changes using an emmiter.
Use an emmiter on service
productUpdated :EventEmitter = new EventEmitter();
deleteProduct(p: Product) {
// this.products = this.products.filter(prod => prod.id !== p.id);
const i = this.products.indexOf(p);
this.products.splice(i,1);
this.productUpdated.emit(this.products);
return this.http.delete(apiUrl + "/" + p.id)
}
And then listen for that change ProductsComponent
export class ProductsComponent implements OnInit {
products: Product[] = [];
searchText: string = "";
constructor(private productsService: ProductsService) {}
ngOnInit(): void {
this.productsService.getProducts()
.subscribe((data: Product[]) => {
this.productsService.products = data;
this.products = this.productsService.products;
})
this.productsService.productUpdated.subscribe( (data) => {
this.products = data;
});
}
I got upload.service and 2 different modules with some components.
upload.service , upload.ts is imported to components->
Upload.module (
upload.component (everything works fine (i can upload and get photo
url data from database)
)
Ui.module )
upload.component (same function but I can't see photo , no url)
)
Working component (upload.component) :
import { Component, OnInit } from '#angular/core';
import { UploadService } from '../shared/upload.service';
import { Upload } from '../shared/upload';
import { Observable } from 'rxjs/Observable';
#Component({
selector: 'uploads-list',
templateUrl: './uploads-list.component.html',
styleUrls: ['./uploads-list.component.scss'],
})
export class UploadsListComponent implements OnInit {
uploads: Observable<Upload[]>;
showSpinner = true;
constructor(private upSvc: UploadService) { }
ngOnInit() {
this.uploads = this.upSvc.getUploads();
this.uploads.subscribe(() => this.showSpinner = false);
}
}
Not working component ( ui.component 0 errors) :
import {Component, OnInit} from '#angular/core';
import { UploadService } from "../../uploads/shared/upload.service";
import { Upload } from "../../uploads/shared/upload";
import { Observable } from "rxjs/Observable";
#Component({
selector: 'top-nav',
templateUrl: './top-nav.component.html',
styleUrls: ['./top-nav.component.scss'],
})
export class TopNavComponent implements OnInit {
uploads: Observable<Upload[]>;
show = false;
showSpinner = true;
toggleCollapse() {
this.show = !this.show;
}
constructor(private upSvc: UploadService) { }
ngOnInit() {
this.uploads = this.upSvc.getUploads();
console.log("paimama upload :", this.uploads)
this.uploads.subscribe(() => this.showSpinner = false);
}
}
I think the main problem is that I am trying to use these functions and variables from one service in two different modules - components. How to retrieve it so it would work ?
Edit (the function I am calling) :
getUploads() {
this.uploads = this.db.list(`${this.auth.userId}`).snapshotChanges().map((actions) => {
return actions.map((a) => {
const data = a.payload.val();
const $key = a.payload.key;
return { $key, ...data };
});
});
return this.uploads;
}
Html (working one in upload.component) :
<h3>File Uploads</h3>
<div *ngFor="let upload of uploads | async">
<upload-detail [upload]='upload'></upload-detail>
<img src="{{upload.url}}" alt="">
</div>
<loading-spinner *ngIf="showSpinner"></loading-spinner>
<hr>
<upload-form></upload-form>
HTML (not working ui.component)
<div *ngFor="let upload of uploads | async">
<img src="{{upload.url}}" alt="">
</div>
or
It seems that you are re-initializing your service observable each time the getUploads() method is called. This causes each component to be subscribing to a different observable, so they will not be in sync.
Don't do this if you want all components to subscribe to the same stream.
Change this:
getUploads() {
this.uploads = this.db.list(`${this.auth.userId}`).snapshotChanges().map((actions) => {
return actions.map((a) => {
const data = a.payload.val();
const $key = a.payload.key;
return { $key, ...data };
});
});
return this.uploads;
}
To this:
getUploads() {
if (!this.uploads) {
this.uploads = this.db.list(`${this.auth.userId}`).snapshotChanges().map((actions) => {
return actions.map((a) => {
const data = a.payload.val();
const $key = a.payload.key;
return { $key, ...data };
});
});
}
return this.uploads;
}
Now each component should get the same exact observable stream.
Let me know if that doesn't help.
Update:
Another approach you can take is to create a custom subject and use that as an observable. Subscribe to that observable in both components, and when you get uploads from the db you can add the upload to the stream. Each component will get the upload you add to that stream.
#Injectable()
export class UploadService {
uploadSubject: ReplaySubject<Upload> = new ReplaySubject();
upload$: Observable<Upload> = this.uploadSubject.asObservable();
getUploads() {
this.http.get(url).subscribe((upload: Upload) => {
this.uploadSubject.next(upload);
});
}
}
Now in any component, you can just subscribe to upload$ and any time the getUploads() is called, every single component subscribing to upload$ will get the value.
export class Component1 {
upload: Upload;
constructor(private uploadService: UploadService) {
uploadService.upload$.subscribe((upload: Upload) => this.upload = upload);
}
}
export class Component2 {
upload: Upload;
constructor(private uploadService: UploadService) {
uploadService.upload$.subscribe((upload: Upload) => this.upload = upload);
}
}
With this approach, just make sure to call getUploads somewhere, because the components are not calling it; they are just listening for the value it will broadcast.
Update 2:
Now I am modifying your code to work with my example above. Since this is using your code, it may not work because I may be missing certain things you have in your environment. But the original "Update" above works 100%, so use that as a guideline to get your code working. Just read the code and make sure you understand what it does and you will see that it will work for what you're doing.
UploadService:
#Injectable()
export class UploadService {
uploadStream: ReplaySubject<Upload[]> = new ReplaySubject();
uploads$: Observable<Upload[]> = this.uploadStream.asObservable();
constructor() {
this.getUploads();
}
getUploads() {
this.db.list(`${this.auth.userId}`)
.map((actions: Action[]) => {
return actions.map((action: Action) => {
const data = action.payload.val();
const $key = action.payload.key;
return new Upload({ $key, ...data });
});
})
.subscribe((uploads: Upload[]) => {
this.uploadStream.next(uploads);
});
}
}
UploadsListComponent:
import { Component } from '#angular/core';
import { UploadService } from '../shared/upload.service';
#Component({
selector: 'uploads-list',
templateUrl: './uploads-list.component.html',
styleUrls: ['./uploads-list.component.scss'],
})
export class UploadsListComponent {
constructor(public uploadService: UploadService) {}
}
UploadListComponent template:
<h3>File Uploads</h3>
<div *ngFor="let upload of uploadService.uploads$ | async">
<upload-detail [upload]='upload'></upload-detail>
<img src="{{upload.url}}" alt="">
</div>
<loading-spinner *ngIf="showSpinner"></loading-spinner>
<hr>
<upload-form></upload-form>
Now you can take the same component approach as above on your second component.
How to update component when route changes. I have this component :
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { ListService } from '../list/list.service';
#Component({
selector: 'view',
template: `
<div *ngIf="!entity">
<p>Select <b (click)="showRow()">row {{entity}}</b>!</p>
</div>
<div *ngIf="entity">
<p >{{entity.id}}</p>
<p >{{entity.name}}</p>
<p >{{entity.weight}}</p>
<p >{{entity.symbol}}</p>
</div>
`,
styles: []
})
export class ViewComponent implements OnInit {
constructor(
private route: ActivatedRoute,
private service: ListService
) {
this.route.params.subscribe(params => {
const id = parseInt(params['id']);
if (id) {
const entity = this.service.getRow(id);
this.entity = entity
}
});
}
entity;
showRow() {
console.log(this.entity);
}
ngOnInit() {
}
}
in this.entity inside constructor i have desired object but when i execute showRow this.entity is undefined, what i'm doing wrong ? I have tried to change property to different name, and it didn't work as expected, if any one knows how to resolve this or point me to right direction.
EDIT:
getRow from service
getRow(id) {
console.log(id, 'test');
return this.datasource.find(row => row.id === id);//returns good row
}
Move your code to ngOnInit() method and check will you get value or not.
ngOnInit() {
this.route.params.subscribe(params => {
const id = parseInt(params['id']);
if (id) {
const entity = this.service.getRow(id);
this.entity = entity
}
});
}
I believe you need to define/initialize the entity that is positioned above showRow, such as:
const entity = Entity;
or something along those lines. apologies I am quite new to angular as well.
I found answer to my problem, i just needed to put router-outlet in template just like that :
....
....
template: `
<router-outlet>
<div *ngIf="row?.id; else elseBlock">
<div>
<p>{{row.id}}</p>
<p>{{row.name}}</p>
<p>{{row.weight}}</p>
<p>{{row.symbol}}</p>
</div>
</div>
</router-outlet>
`,
EDIT: Comment by OP:
"Sorry , but I think I had had slight typo in enviroment/environment, sorry for wasting your time ,it seems to work now"
I have having trouble passing data from app components to child component in angular 2 . I recently started toying with angular 2 and trying to understand how it works. I tried to used the concept shown in this tutorial to do pass data to child component
https://angular.io/docs/ts/latest/tutorial/toh-pt3.html
But I think I am missing something
Here is my project: App component:
import { Component, ViewChild } from '#angular/core';
import { WorkflowService } from './components/workflow_display/workflow.service';
import { WorkflowDisplayComponent } from './components/workflow_display/workflow-display.component';
import { PropertyService } from './shared/property.service';
import '../../public/css/styles.css';
#Component({
selector: 'my-app',
template: require('./app.component.html')
})
export class AppComponent {
title = 'Hello World';
#ViewChild("taskDisplay") workflowDisplay: WorkflowDisplayComponent;
myEnvironment: String; //the variable I am trying to bind from
errorMessage: String;
workbenchBaseUrl : String = 'workbenchBaseUrl';
public selectedNavID : String = 'workspace_control_workStreamView';
public isWorkOrdersCollapsed = false;
public isWorkStreamsCollapsed = false;
constructor(private _propertyService : PropertyService){
}
ngOnInit(): void {
this._propertyService.getValue(this.workbenchBaseUrl)
.subscribe(environment => this.myEnvironment = environment,
error => this.errorMessage = <any>error);
}
}
app.component.html
<div>
<div>
<div>
<!--some html-->
<main class="col-sm-9 offset-sm-3 col-md-10 offset-md-2 pt-3 mh-100">
<workflow-display [environment] ="myEnvironment" #taskDisplay></workflow-display>
</main>
</div>
</div>
</div>
WorkDisplay component
import { Component, Input} from '#angular/core';
import { OnInit } from '#angular/core';
import { IGrcTask } from './grc-task';
import { WorkflowService } from './workflow.service';
import { PropertyService } from '../../shared/property.service';
#Component({
selector: 'workflow-display',
template: require('./workflow-display.component.html')
})
export class WorkflowDisplayComponent implements OnInit {
taskMode: string = 'workstream'; // 'workorder' or 'workstream' to currently identify the columns to display
taskQuery: string = 'process=workstream&taskStatus=RUNNING'; // the query parameters to pass to the tasks web service
workbenchUrl: string = 'http://localhost:8081'; // workbench URL
workbenchTaskPage: string = 'wsIndex'; // workbench page to use to open tasks
infoMessage: string;
errorMessage: string;
tasks: IGrcTask[];
currentTask: IGrcTask;
#Input()
environment: String; //the variable I am trying to bind to
workbenchBaseUrl : String = 'workbenchBaseUrl';
constructor() {
}
//called when user clicks a row
openTask(event: any, task: any) {
// this.environment is still undefined
window.open(this.environment + this.workbenchTaskPage + "?taskId=" + task.taskId + "&activitiWorkflow=true");
}
}
WorkDisplay.component.html
<--!some html-->
<tbody *ngIf='(taskMode == "workorder") && tasks && tasks.length'>
<ng-container *ngFor='let task of tasks; let i=index'>
<tr (click)="setCurrentTask($event, task)" (dblclick)="openTask($event, task)"
<--!some html-->
Property.service.ts
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
/**
* Service return Property value/values from the project property file
*
*/
#Injectable()
export class PropertyService {
//ReST Url for the PopertyService on the back end
private _url = '/grcworkflow/resources/grcWorkflow/environment/';
constructor(private _http: Http) {}
/**
* Method return an Observable<String -> Value> for any property
* Method make an http get call to the server to fetch the property
* #Param key for the property in the property file
*/
getValue(key: String): Observable<String> {
return this._http.get(this._url+key)
.map((response: Response) => <String> response.text())
.do(data => console.log('All: ' + data))
.catch(this.handleError);
}
private handleError(error: Response) {
return Observable.throw(error.json().error || 'Server error');
}
}
NOTE I have removed some function definitions and variable from the components which might be irrelevant.
I am trying to bind myEnviroment value of the app.component enviroment value. myEnviroment get set when proerty service returns a string. Although enviroment value still stays undefined .
I am looking for one way binding i.e when myEnvironment(parent) changes environment(child) should change too. But this doesn't seem to happen. Please help out here