Consider the below code, or see this plunk: https://plnkr.co/edit/mwPHfz?p=info.
Master
import {Component} from "angular2/core";
import {DetailComponent} from "./detail.component";
import {User} from "./user";
import {MOCKUSERS} from "./mock-users";
#Component({
directives: [DetailComponent],
selector: "master",
template: `
<h2>Master</h2>
<div>
<ul>
<li *ngFor="#user of users" (click)="selectUser(user)">
{{user.firstName}} {{user.lastName}}
</li>
</ul>
</div>
<div>
<detail [user]="selectedUser"></detail>
</div>
`
})
export class MasterComponent {
users: User[] = MOCKUSERS;
selectedUser: User;
selectUser(user: User) {
this.selectedUser = user;
}
}
Detail
import {Component, Input} from "angular2/core";
import {User} from "./user";
#Component({
selector: "detail",
template: `
<h2>Detail</h2>
<div>
<div>
<input type="text" [(ngModel)]="user.firstName">
</div>
<div>
<input type="text" [(ngModel)]="user.lastName">
</div>
<div>
<button type="submit">Submit</button>
</div>
</div>
`
})
export class DetailComponent {
#Input()
user: User;
}
User
export class User {
firstName: string,
lastName: string
}
When you click on a name in the list (master) the form (detail) should be updated. The first name and last name properties should appear in the firstName and lastName inputs respectively.
However, when you look in the developer tools, you can see the following error when loading the app:
EXCEPTION: TypeError: l_user0 is undefined in [user.firstName in DetailComponent#4:27]
So somehow the binding to user doesn't seem to be accepted. Can anyone tell why?
You are passing an undefined object (before selecting a user) to DetailComponent. You should only render the DetailComponent if there is a selectedUser.
<div *ngIf="selectedUser">
<detail [user]="selectedUser"></detail>
</div>
Working plunker. In index.html, I had to replace the imported angular js file from angular.min.js to angular.dev.js to fix an unrelated issue on your plunker.
<ul class="list-group users-list">
<li class="list-group-item"
*ngFor="let user of users"
(click)="selectUser(user)"
[class.active]="user === activeUser">
{{ user.name }} ({{ user.username }})
</li>
</ul>
you should initilize selectedUser parametes.
export class MasterComponent {
users: User[] = MOCKUSERS;
selectedUser: User = null;
selectUser(user: User) {
this.selectedUser = user;
}
}
<div>
<detail [user]="selectedUser"></detail>
</div>
#Component({
selector: "detail",
template: `
<h2>Detail</h2>
<div *ngIf="user !== null">
<div>
<input type="text" [(ngModel)]="user.firstName">
</div>
<div>
<input type="text" [(ngModel)]="user.lastName">
</div>
<div>
<button type="submit">Submit</button>
</div>
</div>
`
})
export class DetailComponent {
#Input() user: User = null;
}
Related
This is probably a dumb question with a simple answer but I didn't really know how to find the answer.
I'm using Angular with Micro Frontends and created my own UI library. Now I want to use one of my custom components the "Card - < ng-mfe-card >" and insert HTML inside the imported component. Simplified like this ->
<ng-mfe-card>
<div class='someClass'></div>
</ng-mfe-card>
Login component where I import the Card component and want to insert the HTML inside the Card component.
<ng-mfe-card [title]="'Login'">
<form class="login-form" (ngSubmit)="login()">
<label>
Username:
<input type="text" name="username" [(ngModel)]="username" />
</label>
<label>
Password:
<input type="password" name="password" [(ngModel)]="password" />
</label>
<button type="submit">Login</button>
</form>
<div *ngIf="isLoggedIn$ | async">User is logged in!</div>
</ng-mfe-card>
Card UI component imported from Library, tag the importee can use to insert HTML under < div class="card-content" >
<div class="card">
<div class="card-header">
<img class="card-header-image" src="{{ imageURL }}" width="100vw">
<h1 class="card-header-title">{{ title }}</h1>
</div>
<div class="card-content">
<!-- HTML from component that imports this Card component -->
</div>
<div *ngIf="footer != ''" class="card-footer">{{ footer }}</div>
</div>
card.component.ts
import { Component, Input, OnInit } from '#angular/core';
#Component({
selector: 'ng-mfe-card',
templateUrl: './card.component.html',
styleUrls: ['./card.component.css']
})
export class CardComponent implements OnInit {
#Input() imageURL: string = 'https://image.url';
#Input() title: string = 'Title';
#Input() footer: string = 'Footer';
constructor() { }
ngOnInit(): void {
}
}
My task: The received value from service 1 or 2, depending on the value, change the styles in the html component. Using the ng-switch directive, I expect to compare the value received from the object with the specified template value. Styles are not applied. The main problem is that the styles are not applied
My component:
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, Self } from '#angular/core';
import { ConfigService, IProductListItem, SliderValueOptions } from 'common-modules';
import { Router } from '#angular/router';
import { debounceTime, filter, takeUntil, tap } from 'rxjs/operators';
import { NgOnDestroy } from '../../core/services/destroy.service';
import { BehaviorSubject } from 'rxjs';
import { OnInit, OnDestroy } from '#angular/core';
#Component({
selector: 'app-product-item',
templateUrl: './product-item.component.html',
styleUrls: ['./product-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [NgOnDestroy],
})
export class ProductItemComponent implements OnInit, OnDestroy {
#Input() product: IProductListItem;
#Input() doubleClickEnabled: boolean;
// tslint:disable-next-line:no-output-on-prefix
#Output() onAddToCart = new EventEmitter<any>();
themeProperties: number;
visibility: boolean = false;
constructor(private _router: Router, private _config: ConfigService, #Self() private onDestroy$: NgOnDestroy) {}
ngOnInit() {
this._config.marketplaceConfig.pipe(takeUntil(this.onDestroy$)).subscribe((config) => {
this.themeProperties = config.structure?.theme;
});
}
}
My html:
<mat-card class="card">
<div [ngSwitch]="themeProperties">
<ng-template *ngswitchcase="2">
{{ themeProperties }}
<mat-card id="cardbackground">
<a class="card__link" (click)="redirectToProduct(product)" (dblclick)="doubleClicked(product)">
<div
style="display: none"
class="card__promo"
*ngIf="product.priceWithDiscount && product?.promoIndividual?.percentDiscount"
>
-{{ product?.promoIndividual?.percentDiscount }}%
</div>
<!-- <b2b-modern-image mat-card-image [src]="product.photos[0]"></b2b-modern-image> -->
<img class="mat-card-image" [src]="product.photos" alt="" />
<mat-card-content class="card__content" [class.has-padding-top]="product && product['discount']">
<div class="super-sale">супер скидка</div>
<div *ngIf="product && product['discount']" class="discount">-{{ product['discount'] }}%</div>
<div
*ngIf="product && product.promoIndividual && product?.promoIndividual?.percentDiscount"
class="discount"
>
-{{ product?.promoIndividual?.percentDiscount }}
%
</div>
<ng-template #defaultPrice>
<!-- <span class="price">{{product.price}} ₽</span>-->
</ng-template>
<div class="rating">
<ngx-stars color="#ffd700" [readonly]="true" [initialStars]="product.rating || 0"> </ngx-stars>
<span class="ml-3">{{ product.views || 0 }} отзывов</span>
</div>
<h3 class="name wrap-two-line" [matTooltip]="product?.feed" matTooltipPosition="above">
{{ product | localize: 'name' }}
</h3>
<div class="а">
<div
class="d-flex align-items-center"
*ngIf="product.priceWithDiscount && product.priceWithDiscount != product.price; else defaultPrice"
>
<span class="price old">{{ product.price }}₽</span>
</div>
<p class="price">{{ (product.priceWithDiscount || 0).toFixed(1) }}₽</p>
</div>
</mat-card-content>
</a>
<mat-card-footer class="card__footer">
<a routerLink="/confirm-order" [queryParams]="{ products: product.id }" mat-raised-button color="primary">
Купить сейчас
</a>
<button mat-button color="primary" (click)="addToCart(product)">
<mat-icon>add_shopping_cart</mat-icon>
</button>
</mat-card-footer>
</mat-card>
</ng-template>
</div>
</mat-card>
change component to:
themeProperties$: Observable<number>;
...
ngOnInit() {
this.themeProperties$ = this._config.marketplaceConfig.pipe(map((config) => config.structure?.theme))
}
change template to:
<div [ngSwitch]="themeProperties$ | async">
<ng-container *ngSwitchCase="2">
....
</ng-container>
issues:
casing wrong on ngSwitchCase directive
OnPush change detection not running correctly, async pipe resolves
ng-template doesn't render to DOM. it's a template for use elsewhere. use ng-container instead.
The spelling of the template switch statement is case sensitive, should be *ngSwitchCase.
Also the ng-template does not work in this case, you need to use ng-container
So please change your statements to this layout, and all should work:
<ng-container *ngSwitchCase="2">
also because you are using ChangeDetectionStrategy.OnPush you need to trigger the change detection manually for the ngSwitch.
Here is a working example.
constructor(private _router: Router, private _config: ConfigService, #Self() private onDestroy$: NgOnDestroy, private cdr: ChangeDetectorRef) {}
ngOnInit() {
this._config.marketplaceConfig.pipe(takeUntil(this.onDestroy$)).subscribe((config) => {
this.themeProperties = config.structure?.theme;
this.cdr.detectChanges(); // <=== here, after setting the value
});
}
I am building an Angular Movie DB, the two components I am working with are the "search-movie" component and the "single-view" component. The "search-movie" component displays a list of movies, the "single-view" component shows further details of a given movie.
I would like it so that when I click on a movie from the list in the "search-movie" component, the page renders the "single-view" component for that movie.
I have managed to set this up so that navigating to the "single-view" path with a movie id (i.e. /movie/tt4425200) directly from the URL, correctly loads up the info for that movie by it's ID, so that's all correctly set up.
I just can't seem to connect the clicking of a movie in the "search-movie" component, to then successfully navigate to the correct "single-view" path. So using the example above, clicking on the relevant movie loads up the "single-view" component with the URL path /movie/tt4425200.
I'm sure that this is a case of using #Input to communicate between the two components, but I just can't figure it out.
search-movie.component.ts:
import { Component, OnInit, Input } from '#angular/core';
import { FormControl } from '#angular/forms';
import { DataService } from '../data.service';
#Component({
selector: 'app-search-movie',
templateUrl: './search-movie.component.html',
styleUrls: ['./search-movie.component.css']
})
export class SearchMovieComponent implements OnInit {
searchControl = new FormControl('');
movieResults: any = [];
constructor(private data: DataService) { }
ngOnInit() {}
/* GET request */
getData(event) {
const film = event.target.value;
this.data.searchFilm(film)
.subscribe( (res) => {
res = this.movieResults = res;
console.log(res);
});
}
}
search-movie.component.html:
<div class="container">
<h1 class="is-size-3">Find your favorite movies...</h1>
<form (ngSubmit)="onSubmit()">
<input
type="text"
(keyup)="getData($event)"
placeholder="Start typing..."
[formControl]="searchControl" />
</form>
{{searchControl.value}}
</div>
<div class="container">
<ul>
<li id="list-item" *ngFor="let x of movieResults; let i = index;">
<img [src]="x.Poster" onerror="this.src='../assets/images/default-poster.jpg'">
<p id="movie-title" class="is-size-5">{{x.Title}} ({{x.Year}}) </p>
</li>
</ul>
</div>
single-view.component.ts:
import { Component, OnInit, Input } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { DataService } from '../data.service';
#Component({
selector: 'app-single-view',
templateUrl: './single-view.component.html',
styleUrls: ['./single-view.component.css']
})
export class SingleViewComponent implements OnInit {
singleDetails: any = {};
constructor(
private data: DataService,
private route: ActivatedRoute) { }
ngOnInit() {
/** GENERATE FILM DETAILS BY movie/id where id is the imdbID */
const id = this.route.snapshot.paramMap.get('id');
this.data.moviebyID(id)
.subscribe( (res) => {
res = this.singleDetails = res;
});
}
}
single-view.component.html:
<div class="container singleDisplayContainer">
<div class="columns is-mobile">
<div class="column" id="poster-column">
<img id="poster" [src]="singleDetails.Poster" [alt]="singleDetails.Title" onerror="this.src='../assets/images/default-poster.jpg'">
</div>
<div id="info-column" class="column">
<div class="columns">
<div class="column is-three-fifths">
<h1 class="is-size-3">{{singleDetails.Title}} <span class="is-size-4">({{singleDetails.Year}})</span> </h1>
<p style="font-size: 14px"> {{singleDetails.Rated}} | {{singleDetails.Runtime}} | {{singleDetails.Genre}} | {{singleDetails.Released}} </p>
</div>
<div class="column">
<div class="columns">
<div class="column is-one-third">
<img id="star-rating" width="50px" src="../../assets/images/star-rating.png" alt="{{singleDetails.Title}}">
</div>
<div class="column">
<p style="font-size: 2em"> {{singleDetails.imdbRating}}<span style="font-size: 12px">/10</span></p>
<p>{{singleDetails.imdbVotes}}</p>
</div>
</div>
</div>
</div>
<br>
<p>Plot: {{singleDetails.Plot}} </p>
<br>
<p>Directed by: {{singleDetails.Director}} </p>
<br>
<p>Writers: {{singleDetails.Writer}} </p>
<br>
<p>Actors: {{singleDetails.Actors}} </p>
<br>
<p onerror="this.style.display='none'">Box office: {{singleDetails.BoxOffice}} </p>
</div>
</div>
</div>
In Your UL -> LI, add routerLink property and pass the movie id as the second param
[routerLink]="['/movie/,x.movieId]">
<ul>
<li id="list-item" *ngFor="let x of movieResults; let i = index;" [routerLink]="['/movie/,x.movieId]">
<img [src]="x.Poster" onerror="this.src='../assets/images/default-poster.jpg'">
<p
id="movie-title"
class="is-size-5">{{x.Title}} ({{x.Year}}) </p>
</li>
</ul>
Looks like you have everything set up, all you need to do is navigate to single-view on click of the movie.
search-movie.component.ts
showDetails(id : string) {
this.router.navigate(['movie',{ id }]);
}
search-movie.component.html
<!-- in showDetails you will pass id of the current movie while looping -->
<li id="list-item" *ngFor="let x of movieResults; let i = index;" (click)="showDetails(x.id)">
<img
[src]="x.Poster"
onerror="this.src='../assets/images/default-poster.jpg'"
>
<p
id="movie-title"
class="is-size-5">{{x.Title}} ({{x.Year}}) </p>
</li>
This will navigate to single-view page. is this what you want right?
or you need to load single-view in the same page as the list?
I'm relatively new to Vue and super stuck with this error message when I try to make this reset email modal work in my Vue project:
Property or method "sendResetMail" is not defined on the instance but
referenced during render. Make sure that this property is reactive,
either in the data option, or for class-based components, by
initializing the property.
I have no idea what I need to do to make this work. I followed the Vue documentation and declared resetEmail in the data option.
ForgotPassword.vue:
<template>
<section>
<a #click="isComponentModalActive = true">
Forgot Password?
</a>
<b-modal :active.sync="isComponentModalActive" has-modal-card>
<modal-form v-bind="resetEmail"></modal-form>
</b-modal>
</section>
</template>
<script>
import firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/firestore'
const ModalForm = {
props: ['resetEmail'],
template: `
<form #submit.prevent="sendResetMail">
<div class="modal-card" style="width: auto">
<header class="modal-card-head">
<p class="modal-card-title">Forgot password?</p>
</header>
<section class="modal-card-body">
<b-field label="Email">
<b-input
type="email"
:value="resetEmail"
placeholder="Your email"
required>
</b-input>
</b-field>
</section>
<footer class="modal-card-foot">
<button class="button" type="button" #click="$parent.close()">Close</button>
<button class="button is-primary">Reset</button>
</footer>
</div>
</form>
`
}
export default {
components: {
ModalForm
},
data () {
return {
isComponentModalActive: false,
resetEmail: '',
feedback: null
}
},
methods: {
sendResetMail () {
var auth = firebase.auth()
var emailAddress = this.resetEmail
auth.sendPasswordResetEmail(emailAddress).then(function () {
// Email sent.
console.log('email send')
}).catch(function (error) {
// An error happened.
this.feedback = error.message
console.log(error.message)
})
}
}
}
</script>
This is the file where I use the ForgotPassword.vue component,
Login.vue:
<template>
<section class="section">
<div class="container">
<div class="columns">
<div class="column"></div>
<div class="column is-half">
<div class="box">
<h1 class="title">Login</h1>
<form #submit.prevent="login">
<b-field label="Email" :message="feedback" :type="type">
<b-input placeholder="Email" icon="email" type="email" v-model="email">
</b-input>
</b-field>
<b-field label="Password" :message="feedback" :type="type">
<b-input placeholder="Password" type="password" icon="textbox-password" password-reveal v-model="password">
</b-input>
</b-field>
<button type="submit" class="button is-primary is-fullwidth">Login</button>
<div class="field">
<div class="control">
<p class="control has-text-centered">
<ForgotPassword/>
</p>
</div>
</div>
<div class="field">
<div class="control">
<p class="control has-text-centered">
Don't have an account?
<a href="/register">
<router-link :to="{ name: 'Signup' }">
Signup
</router-link>
</a>
</p>
</div>
</div>
</form>
</div>
</div>
<div class="column"></div>
</div>
</div>
</section>
</template>
<script>
import firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/firestore'
import ForgotPassword from '#/components/auth/ForgotPassword'
export default {
name: 'login',
metaInfo: {
// specify a metaInfo.title, this title will be used
title: 'Login',
// all titles will be injected into this template
titleTemplate: '%s | Wterdrops.com'
},
components: {
ForgotPassword
},
data () {
return {
email: null,
password: null,
feedback: null,
type: null
}
},
methods: {
login () {
if (this.email && this.password) {
firebase
.auth()
.signInWithEmailAndPassword(this.email, this.password)
.then(cred => {
this.$router.push({
name: 'Dashboard'
})
})
.catch(err => {
this.feedback = err.message
})
this.feedback = null
} else {
this.feedback = 'Please fill in both fields.'
this.type = 'is-danger'
}
}
}
}
</script>
<style>
.login {
max-width: 400px;
margin-top: 60px;
}
</style>
I would be very grateful if someone can explain to me what I'm missing :)
You are referencing
<form #submit.prevent="sendResetMail">
inside your ModalForm component.
The problem is that this template is going to look for the method sendResetMail on that ModalForm component template when it gets rendered since you the code is referencing it. However this sendResetMail method is not directly associated on that.
You can consider using a mix-in if you need to use the sendResetMail in many places , or maybe just move that method directly to the same component "ModalForm" that is referencing it.
You can also look into for example eventBus to trigger the method by emitting an event.
The simplest option if you only need to call it from the MOdalForm component is to just move the sendResetMail direcly to that component. I believe that would probably fix your issue.
So I'm sending a movie object from a parent to a child component. I use #Input from the core module to do this.
Now, I am able to use this data inside the template to display my view, but when I try to access it in for example editReview() then this.movie returns undefined.
Does anyone know how to fix this?
Child Component: the console.log returns undefined
export class ReviewListComponent implements OnInit {
#Input('movie') movie;
constructor(private reviewService: ReviewService){}
editReview() {
console.log(this.movie);
}
}
Child Component Template: I am able to display my name and the content of the review. What's not working unfortunately is when I click the button it says that this.movie is undefined
<ul class="list-group">
<li *ngFor="let review of movie?.reviews" class="list-group-item">
<p>{{review.user.firstName}} {{ review.user.lastName}}</p>
<hr>
<p>{{review.content}}</p>
</li>
</ul>
<button (click)="editReview()">Button</button>
Edit: I added the components and templates of the two parent components: The first one is the movie component where I retrieve the movie object asynchronously, the second is the review component which contains the review-list component that I had already posted above
movie.component.ts
import {Component, OnInit} from "#angular/core";
import { MovieService} from "./movie.service";
import {ActivatedRoute} from "#angular/router";
#Component({
selector: 'app-movie',
templateUrl: './movie.component.html',
styleUrls: ['./movie.component.css']
})
export class MovieComponent implements OnInit{
rating;
movie;
movieId;
constructor(private movieService: MovieService, private route: ActivatedRoute){
}
checkRating(){
return typeof this.rating === 'number';
}
ngOnInit(){
this.route.params.subscribe(params => {
this.movieId = params.id;
var userId = localStorage.getItem('userId');
this.movieService.getMovie(this.movieId)
.subscribe(movie => {
this.movie = movie.obj;
this.movie.averageRating = Number(this.movie.averageRating);
console.log(this.movie);
});
if (userId){
this.movieService.getRating(userId, this.movieId)
.subscribe( result => {
this.rating = result.rating;
})
}
});
}
addRating(score) {
var userId = localStorage.getItem('userId');
this.movieService.addRating(score, userId, this.movie._id)
.subscribe(data => {
this.rating = data.obj.rating;
});
}
}
movie.component.html: this gives movie object to review.component through element app-review
<div class="container">
<div class="col col-md-4 col-md-offset-2">
<img [src]="movie?.pictureUrl" class="img-responsive" alt="hello">
</div>
<div class=" col col-md-6">
<h2>{{movie?.title}} ({{movie?.year}})</h2>
<h3>Average Score:</h3>
<span class="glyphicon glyphicon-star"></span><span><h1>{{movie?.averageRating}}</h1></span><br>
<h3>My Score:</h3>
<div class="row">
<div class="col">
<div *ngIf="!checkRating()" ngbDropdown class="d-inline-block">
<button class="btn btn-outline-primary" ngbDropdownToggle>Rate The Movie!</button>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<button value="1" (click)="addRating(1)" class="dropdown-item">1</button>
<button value="2" (click)="addRating(2)" class="dropdown-item">2</button>
<button value="3" (click)="addRating(3)" class="dropdown-item">3</button>
<button value="4" (click)="addRating(4)" class="dropdown-item">4</button>
<button value="5" (click)="addRating(5)" class="dropdown-item">5</button>
</div>
</div>
<div *ngIf="checkRating()">
<h1>{{rating}}</h1>
</div>
</div>
</div>
<h4>{{movie?.director}}</h4>
<h4>{{movie?.actors}}</h4>
<h4>{{movie?.country}}</h4>
<p> {{movie?.description}}</p>
</div>
</div>
<hr class="col-md-8 col-md-offset-2">
<div class="col-md-8 col-md-offset-2">
<ng-container *ngIf="!!movie">
<app-review [movie]="movie"></app-review>
</ng-container>
</div>
review.component.ts
import {Component, ElementRef, Input, OnInit, ViewChild} from "#angular/core";
import {NgForm} from "#angular/forms";
import {ReviewService} from "./review.service";
import {ActivatedRoute} from "#angular/router";
#Component({
selector: 'app-review',
templateUrl: './review.component.html',
styleUrls: ['./review.component.css']
})
export class ReviewComponent implements OnInit{
movieId;
#Input('movie') movie;
#ViewChild('textArea') textArea;
constructor(private reviewService: ReviewService, private route: ActivatedRoute){}
ngOnInit(){
this.route.params.subscribe( params => {
this.movieId = params.id
});
};
onClear(form: NgForm) {
form.resetForm()
}
onSubmit(form: NgForm) {
let content = form.value.review;
let userId = localStorage.getItem('userId');
let movieId = this.movieId;
this.reviewService.addReview(content, userId, movieId)
.subscribe( result => {
console.log(result.obj);
this.movie.reviews.unshift(result.obj);
form.resetForm()
})
}
}
review.component.html: this gives movie object to the review-list component through app-review-list
<ng-container *ngIf="!!movie">
<app-review-list [movie]="movie"></app-review-list>
</ng-container>
<hr>
<form ngNativeValidate (ngSubmit)="onSubmit(f)" #f="ngForm">
<div class="form-group">
<label for="review">Write A Review:</label>
<textarea
#textArea
(keyup.enter)="onSubmit(f)"
rows="4"
cols="50"
type="text"
id="review"
ngModel
class="form-control"
name="review"
required
></textarea>
</div>
<button type="button" class="btn btn-danger" (click)="onClear(f)">Clear</button>
<button class="btn btn-primary" type="submit">Save</button>
</form>
you can access it in ngOnInit() just implement OnInit interface