Angular 2 dependancy injection (DI) just not working? - javascript

I'm got a couple of (beginner?) problems with my Angular2 app. Firstly a DI works for one component, but not another, and I cant see any problem.
I've got a cocktail component that includes a starring functionality, and a list of ingredients. All code below is stripped down, and it all runs fine (no console errors) - I'm just not getting the desired output
Heres the cocktail.component.ts:
import { Component } from '#angular/core'
import { CocktailService } from './cocktail.service'
import { HttpModule } from '#angular/http';
import { Observable } from 'rxjs/Rx';
#Component({
selector: 'cocktails',
templateUrl: 'app/cocktail.template.html',
providers: [CocktailService, HttpModule]
})
export class CocktailsComponent {
title: string = "Sunday Brunch Specials";
cocktails;
isStarred = false;
numLikes = 10;
ingredient_json = "Bob"; // for testing
constructor(private _cocktailService: CocktailService) {
this.cocktails = _cocktailService.getCocktails();
}
}
And the cocktail.template.html....
<h2>{{title}}</h2>
<input type="text" autoGrow />
<ul>
<li *ngFor="let cocktail of cocktails | async">
<h3>{{cocktail.name}}</h3>
<star [is-starred]="isStarred" [num-likes]="numLikes" (change)="onStarredChange($event)"></star>
<ingredients [ingredient-json]="ingredient_json"></ingredients>
<li>
The star component is getting passed isStarred and numLikes properly - all good.
Now in the ingredient component:
import {Component, Input} from '#angular/core';
import {IngredientService} from './ingredient.service';
#Component({
selector: 'ingredients',
template: '
<h4>Ingredients</h4>
<ul>
<li *ngFor="let ingredient of ingredients">{{ingredient}}</li>
</ul>'
)}
export class IngredientsComponent {
#Input('ingredient-json') ingredient_json = "Hello";
title: 'Ingredients';
ingredients;
constructor() {
// Why isn't ingredient_json outputting anything but hello? Ie nothing is going into input
console.log("Ingredient JSON = " + this.ingredient_json);
}
}
The problem I've stated in comments. The ingredient_json variable just isn't instantiated from the #Input. I'm not getting any errors, the console just always prints out 'Hello' - ie the default. The #Input isn't grabbing anything from its parent (cocktail.component.ts).
I havent included all the app, as I feel as though something is amiss in these 3 files. Like I said, the DI works on the component, so I know that the DI works, but I can't see whats wrong in my code below.
Thanks a lot

Constructor is invoked before the #input() params are passed to a component.
Implement the method ngOnChanges() in your IngredientsComponent and move your console.log() there.
Details here: https://angular.io/docs/ts/latest/api/core/index/OnChanges-class.html

Related

#Input variables inside UI components return null

Hint: Im very new to angular. I hope this question gets answered, as its the most detailed explanation of my issue I can give with my current knowledge/vocabulary in Angular.
I have the following Setup:
My App component defines a PageNavigation array and fills it in the constructor:
PageNavigation Interface
import {Route} from "#angular/router";
export interface PageNavigation {
displayName : string;
route : Route;
}
App Component
import {Component} from '#angular/core';
import {Router} from "#angular/router";
import {PageNavigation} from "src/app/navigation/page-navigation";
import {TemplatesComponent} from "src/app/pages/templates/templates.component";
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.less']
})
export class AppComponent {
pageNavigations: PageNavigation[];
constructor(public router: Router) {
this.pageNavigations = [
{displayName: "Templates", route: {path: "templates", component: TemplatesComponent}}
]
this.pageNavigations.forEach(pageNavigation => { //<-- I use the Array for Routing but that's probably not interesting for my problem
this.router.config.push(pageNavigation.route);
});
}
}
I then want to create a Sidebar inside my App Component, and hand over the array
SideBar Component
import {Component, Input, OnInit} from '#angular/core';
import {PageNavigation} from "src/app/navigation/page-navigation";
import {Router} from "#angular/router";
#Component({
selector: 'tara-sidebar',
templateUrl: './sidebar.component.html',
styleUrls: ['./sidebar.component.less']
})
export class SidebarComponent {
public selectedRoute?: any;
constructor(public router : Router) { }
#Input('pageNavigations') pageNavigations : PageNavigation[] | any;
navigate(): void {
alert(this.selectedRoute);
// this.router.navigate([this.selectedRoute]).then(function () {
// // success
// }, function () {
// // error
// });
}
}
The corresponding html:
app.component.html
<tara-sidebar [pageNavigations]="pageNavigations"></tara-sidebar>
<router-outlet></router-outlet>
In the app.component.html I just "hand" the values over
sidebar.component.html
<p-listbox [options]="pageNavigations" [(ngModel)]="selectedRoute" optionLabel="displayName" optionValue="route" (ngModelChange)="navigate()" ></p-listbox>
In the SideBar, I create a prime-ng listbox. It displays the correct Label (in my case "Templates"). However, clicking on it, triggering the navigate() method, I get an alert with either "null" or "[object] [object]" (it alternates between the two, meaning first click = null, second = [object] [object], third = null,...
If possible, can you please explain me whats going on, as I expected to either always get null (with Input beeing "triggered" after the navigate()), or the code to just be working and giving me the data I have clicked on.
Clicked on Templates first time
Clicked on Templates secondtime
I have already tried changing the variable type of selectedRoute from any to PageNavigation, but without success it just was giving me undefined in the alert.
This is can happen because an alert gets the old value of selectedRoute. Try to do this:
navigate(value): void {
alert(value);
}
and inside a template:
(ngModelChange)="navigate($event)"
As the returned type of the listbox wasnt PageNavigation but route, when I used alert(value.path) I was presented with a value. However, it still works only once and after that returns null and throwing an error...

Angular - Using custom directive to load several components is returning undefined

I am developing an app and for now, I have a dynamic grid generator which divides the space in the screen to fit several components dynamically. So, the component encharged of this must render the components after angular has rendered the page. In order to achieve that I've followed the angular dynamic component loader guide (https://angular.io/guide/dynamic-component-loader).
So I am in a point where I do have the component where the other components must be rendered, I have my custom directive to render the components.
The directive
#Directive({
selector: '[componentLoader]'
})
export class ComponentLoaderDirective {
constructor (
public ViewContainerRef: ViewContainerRef
) {}
}
Now the component ( grid component )
grid.component.ts
// ... Stuff above
export class GridComponent implements OnInit {
#Input() public items: gridItem[] = [];
#ViewChild(ComponentLoaderDirective) componentLoader: ComponentLoaderDirective | undefined;
constructor(
private sanitizer: DomSanitizer,
private componentFactoryResolver: ComponentFactoryResolver
) {}
ngOnInit(): void { this.processRow(this.items) }
processRow( row: gridItem[] ) {
// Some grid related stuff ...
for ( let item of row ) {
// Stuff performed over every item in each grid row
this.renderComponentIfNeeded(item)
}
}
renderComponentIfNeeded( item: gridItem ):void {
if ( item.components ) {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(component);
let viewContainerRef = this.componentLoader.ViewContainerRef;
viewContainerRef.clear();
let componentRef = viewContainerRef.createComponent<any>(componentFactory);
componentRef.instance.data = item;
console.log('Directive ', this.componentLoader, 'ComponentRef: ', componentRef);
}
}
And the HTML of the component:
<!-- Dynamic grid generation using ng-template and ng-content. This is generated several times using the *ngFor, for every item in the items array we will have a componentLoader -->
<ng-template componentLoader>
</ng-template>
There is a lot more content in these files but for simplicity I will only post this, If you need more code just tell me.
Okay, so my problem is that when I access to this.contentLoader the returned value is just undefined, so this.componentLoader.viewContainerRef causes an error because componentLoader is undefined.
I've tried adding the exportAs property to the directive's decorator and it is giving exacly the same error.
I've also tried to add the directive in the module declarations without success, and changed the <ng-template componentLoader> to <ng-template #loader=componentLoader> which causes a different error ( No directive has 'componentLoader' exportAs or something like this )
PS: In the ´´´this.componentFacotryResolver.resolveComponentFactory(component)``` I successfully have each component that has been given to the grid.
I prefer you not to solve my issue but to point me in the right direction and help me see what am I doing wrong in order to improve myself.
Any help will be much appreciated :)
I've managed to solve this issue in a very simple way.
I was trying to do too many things inside the grid component so I removed to code related to the component loader and moved it into a single component, called ComponentLoaderComponent.
Inside the component I've setted up all the logic in the same way than I did in the grid component. So now I have a new ts file like this:
import { Component, ComponentFactoryResolver, Input, OnInit, ViewChild } from '#angular/core';
import { ComponentLoaderDirective } from 'src/app/shared/directives/componentLoader.directive';
#Component({
selector: 'component-loader',
templateUrl: './component-loader.component.html',
styleUrls: ['./component-loader.component.css']
})
export class ComponentLoaderComponent implements OnInit {
#Input() public component: any;
#ViewChild(ComponentLoaderDirective, { static: true }) componentLoader!: ComponentLoaderDirective;
constructor(
private componentFactoryResolver: ComponentFactoryResolver
) { }
ngOnInit(): void {
this.loadComponent();
}
loadComponent():void {
if (this.component) {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.component);
let viewContainerRef = this.componentLoader.viewContainerRef;
viewContainerRef.clear();
let componentRef = viewContainerRef.createComponent<any>(componentFactory);
}
}
And an HTML like this:
<ng-template componentLoader>
</ng-template>
Now from the grid component I only have to call the ComponentLoader for every component I want to add to the grid, so the grid html will look like this:
<div
*ngIf=" gridItem.components && gridItem.components.length > 0"
class="component-container"
>
<component-loader
*ngFor="let component of gridItem.components"
[component]="component">
</component-loader>
</div >
Now the components are getting loaded correclty, anyways I still don't know what I was missing in before.

Display ContentFul RichText in Angular

I have a blog feed in my Angular App connected with Contentful. Thanks to the Contentful javascript sdk.
https://www.contentful.com/developers/docs/javascript/tutorials/using-contentful-in-an-angular-project/
I'm trying to display the Title and the Text field. Here is my code:
import { Component, OnInit } from '#angular/core';
import {Observable} from 'rxjs';
import {ContentfulService} from '../../services/contentful/contentful.service';
import { Entry } from 'contentful';
#Component({
selector: 'app-blog',
templateUrl: './blog.component.html',
styleUrls: ['./blog.component.scss']
})
export class BlogComponent implements OnInit {
private posts: Entry<any>[] = [];
constructor(private postService: ContentfulService) {}
ngOnInit() {
this.postService.getPosts()
.then(posts => {
this.posts = posts;
console.log(this.posts);
});
}
}
And the html:
<div *ngFor="let post of posts">
{{ post.fields.title }}
<div>{{ post.fields.text }}</div>
</div>
The title field is displayed well because it is just a string field but the text field is RichText and display [object Object].
Indeed it contain several object. It seems like the Object is divided in several pieces.
https://www.contentful.com/developers/docs/concepts/rich-text/
Does somebody have already display Contentful RichText in an Angular App ?
Is there a specific way to do it?
First, you must install rich-text-html-renderer from your terminal:
npm install #contentful/rich-text-html-renderer
Then, you can import it from your Component:
import { documentToHtmlString } from '#contentful/rich-text-html-renderer';
and use it, simply like that:
_returnHtmlFromRichText(richText) {
if (richText === undefined || richText === null || richText.nodeType !== 'document') {
return '<p>Error</p>';
}
return documentToHtmlString(richText);
}
Finally, 'call the function' from your html like so:
<div [innerHtml]="_returnHtmlFromRichText(post.fields.text)">
</div>
You can also add some options to customise your rich text, more information here. Also, you should code a function similar to _returnHtmlFromRichText in your Contentful service to be able to reuse it later.
I created an Angular library that can render rich text using Angular components: https://github.com/kgajera/ngx-contentful-rich-text
Why use this over #contentful/rich-text-html-renderer? If you need to customize the default mark-up, it allows you to use your Angular components which you can't do using the documentToHtmlString function and [innerHTML].

Is it possible to determine which modal / component I will show when I click on a component?

I'm working on small Angular 5 project, and I have a simple component which is representing an food product and it looks like this:
[![enter image description here][1]][1]
This component is nested component, it is part of my main component.
When I click on this component, quantity component/modal is shown:
<app-product-item (onInfoEdit)="InfoModal.show($event)"></app-product-item>
But now I would like to show another modal if some condition is satisfied, for example
If condition is satisfied then show this quantity modal (which is component itself) otherwise show some another modal (which is another component), so:
<app-product-item "if something is satisfied than this code on the right is ok otherwise lets display another modal with different method" (onInfoEdit)="InfoModal.show($event) (onSomethingElse)="AnotherModal.show($event)></app-product-item>
So I'm not sure if this is even possible because I need to show 2 different modals ( different components ) on same component's click, so basically if product has some property defined than show quantity info, otherwise show product info, and quantity info and product info are separated components..
Thanks guys
Cheers
You would be looking at something like this:
<app-product-item #productItem
(click)="productItem?.product?.property ? InfoModal.show($event) : AnotherModal.show($event)"
(onInfoEdit)="InfoModal.show($event)"
(onSomethingElse)="AnotherModal.show($event)">
</app-product-item>
where "property" would be a property of "product", which would be an object defined inside your Product Item Component.
Please also note that the conditional statements inside templates are not recommended and should be carried to the component.
You can do it by creating component which will conditionally render one of two components. One condition will load info component and other condition will load quantity component. Let's call this conditional modal something like: food-modal, inside food-modal.component.html template the code could look like:
<h1>CONDITIONAL MODAL CONTAINER</h1>
<ng-container *ngIf="product.name === 'Chicken'">
<app-info [product]="product"></app-info>
</ng-container>
<ng-container *ngIf="product.name === 'Cow'">
<app-quantity [product]="product"></app-quantity>
</ng-container>
And inside food-modal.component.ts the code could look like:
import { Component, OnInit, Input } from '#angular/core';
import { Product } from '../models/Product.model';
#Component({
selector: 'app-food-modal',
templateUrl: './food-modal.component.html',
styleUrls: ['./food-modal.component.css']
})
export class FoodModalComponent implements OnInit {
#Input()
public product: Product;
constructor() {}
ngOnInit() {}
}
All you have to do now is call this component where you want it and pass in Product model. For sake of demonstration I'll put everything inside app component in order to load food-modal. The app.component.html could look like:
<app-food-modal [product]="chickenProduct">
</app-food-modal>
And inside app.component.ts code could be like this:
import { Component } from '#angular/core';
import { Product } from './models/Product.model';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
public chickenProduct: Product;
constructor() {
this.chickenProduct = {
id: 1,
quantity: 2,
name: 'Chicken'
};
}
}
Now app component will pass Product object named chickenProduct to food-modal component and bind it to modal's product property. After that the conditional rendering will happen.
Rest of the code could look like:
info.component.html:
<p>
This modal is for INFO. Product name is: {{ product.name }}
</p>
info.component.ts:
import { Component, OnInit, Input } from '#angular/core';
import { Product } from '../models/Product.model';
#Component({
selector: 'app-info',
templateUrl: './info.component.html',
styleUrls: ['./info.component.css']
})
export class InfoComponent implements OnInit {
#Input()
public product: Product;
constructor() { }
ngOnInit() {
}
}
quantity.component.html:
<p>
This modal is for QUANTITY. Product name is: {{ product.name }}
</p>
quantity.component.ts:
import { Component, OnInit, Input } from '#angular/core';
import { Product } from '../models/Product.model';
#Component({
selector: 'app-quantity',
templateUrl: './quantity.component.html',
styleUrls: ['./quantity.component.css']
})
export class QuantityComponent implements OnInit {
#Input()
public product: Product;
constructor() { }
ngOnInit() {
}
}
product.model.ts:
export interface Product {
id: number;
quantity: number;
name: string;
}
I've implemented solution like this and everything worked fine! Change name property of product to Cow and conditional fire will happen and load quantity component, take it back to Chicken and conditional fire will load info component.
I assume you just wanted to know how to trigger conditional firing, so that's why I've done this by hard coding string, checking whether name of product is Chicken or Cow.

#Input() Not Passing As Expected Between Parent-Child Components in Angular 2 App

I am trying to abstract out a tabular-data display to make it a child component that can be loaded into various parent components. I'm doing this to make the overall app "dryer". Before I was using an observable to subscribe to a service and make API calls and then printing directly to each component view (each of which had the tabular layout). Now I want to make the tabular data area a child component, and just bind the results of the observable for each of the parent components. For whatever reason, this is not working as expected.
Here is what I have in the parent component view:
<div class="page-view">
<div class="page-view-left">
<admin-left-panel></admin-left-panel>
</div>
<div class="page-view-right">
<div class="page-content">
<admin-tabs></admin-tabs>
<table-display [records]="records"></table-display>
</div>
</div>
</div>
And the component file looks like this:
import { API } from './../../../data/api.service';
import { AccountService } from './../../../data/account.service';
import { Component, OnInit, Input } from '#angular/core';
import { Router, ActivatedRoute } from '#angular/router';
import { TableDisplayComponent } from './../table-display/table-display.component';
#Component({
selector: 'account-comp',
templateUrl: 'app/views/account/account.component.html',
styleUrls: ['app/styles/app.styles.css']
})
export class AccountComponent extends TabPage implements OnInit {
private section: string;
records = [];
errorMsg: string;
constructor(private accountService: AccountService,
router: Router,
route: ActivatedRoute) {
}
ngOnInit() {
this.accountService.getAccount()
.subscribe(resRecordsData => this.records = resRecordsData,
responseRecordsError => this.errorMsg = responseRecordsError);
}
}
Then, in the child component (the one that contains the table-display view), I am including an #Input() for "records" - which is what the result of my observable is assigned to in the parent component. So in the child (table-display) component, I have this:
import { AccountService } from './../../../data/account.service';
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'table-display',
templateUrl: './table-display.component.html',
styleUrls: ['./table-display.component.less']
})
export class TableDisplayComponent {
#Input() records;
constructor() {
}
}
Lastly, here's some of the relevant code from my table-display view:
<tr *ngFor="let record of records; let i = index;">
<td>{{record.name.first}} {{record.name.last}}</td>
<td>{{record.startDate | date:"MM/dd/yy"}}</td>
<td><a class="bluelink" [routerLink]="['/client', record._id ]">{{record.name.first}} {{record.name.last}}</a></td>
When I use it with this configuration, I get "undefined" errors for the "records" properties I'm pulling in via the API/database. I wasn't getting these errors when I had both the table display and the service call within the same component. So all I've done here is abstract out the table-display so I can use it nested within several parent components, rather than having that same table-display show up in full in every parent component that needs it.
What am I missing here? What looks wrong in this configuration?
You need to protect against record being null until it comes in to your child component (and therefore it's view).
Use Elvis operators to protect your template:
<tr *ngFor="let record of records; let i = index;">
<td>{{record?.name?.first}} {{record?.name?.last}}</td>
<td>{{record?.startDate | date:"MM/dd/yy"}}</td>
<td><a class="bluelink" [routerLink]="['/client', record?._id ]"> {{record?.name?.first}} {{record?.name?.last}}</a></td>
You can also assign your input to an empty array to help with this issue:
#Input() records = [];

Categories