I have a problem. In my Angular project I have 2 components with an two-way bound parameter. The parameter is an object id. Both components use the same service which stores the list with objects. The parent contains the list where you can select the object and the child shows the selected object details. Here is the code of the parent:
<div>
<table>
<tr>
<th scope="col">Title</th>
</tr>
<tr *ngFor="let offer of service1.findAll();"
[style.background-color]="offer.rowClicked ? '#ed9b82' : ''"
(click)="highlightClickedRow(offer)">
<td>{{offer.title}}</td>
</tr>
</table>
</div>
<div>
<app-detail3 [editedOfferId]="offerSelectedId" (editedOfferChanged)="offerSelectedId=$event"></app-detail3>
</div>
with the typescript:
#Component({
selector: 'app-overview3',
templateUrl: './overview3.component.html',
styleUrls: ['./overview3.component.css']
})
export class Overview3Component implements OnInit {
public offerSelectedId: number = -1;
constructor(public service1: OffersService) { }
ngOnInit(): void {
}
public highlightClickedRow(offer :Offer) {
let offers: Offer[] = this.service1.findAll();
for (let i = 0; i < offers.length; i++) {
if (offers[i] != offer) {
offers[i].rowClicked = false;
}
else {
offers[i].rowClicked = true;
this.offerSelectedId = offers[i].id;
}
}
}
}
And here is the detail component:
<div id="content" *ngIf="editedOfferId != -1">
<div id="data-table">
<table>
<tr>
<th scope="row" colspan="2">Selected offer details(id = {{service.findById(editedOfferId)!.id}})</th>
</tr>
<tr>
<th scope="row">Title:</th>
<td><input type="text" id="txtTitle" (input)="checkObjectChanged()"></td>
</tr>
<tr>
<th scope="row">Description:</th>
<td><input type="text" id="txtDescription" (input)="checkObjectChanged()" value="{{service.findById(editedOfferId)!.description}}"></td>
</tr>
<tr>
<th scope="row">Status:</th>
<td>
<select (input)="checkObjectChanged()" id="txtStatus">
<option *ngFor="let key of keys" [ngValue]="key" [value]="status[key]" [label]="status[key]" [selected]="service.findById(editedOfferId)!.auctionStatus === key"></option>
</select>
</td>
</tr>
<tr>
<th scope="row">Highest Bid:</th>
<td><input id="txtHighestBid" (input)="checkObjectChanged()" value="{{service.findById(editedOfferId)!.valueHighestBid}}"></td>
</tr>
</table>
</div>
</div>
<div *ngIf="editedOfferId == -1">
<label id="lblNothingSelected">Nothing has been selected yet</label>
</div>
With the typescript:
#Component({
selector: 'app-detail3',
templateUrl: './detail3.component.html',
styleUrls: ['./detail3.component.css']
})
export class Detail3Component implements OnInit {
#Input() editedOfferId: number = -1;
#Output() editedOfferChanged = new EventEmitter<number>();
public selectedOffer: Offer = new Offer("", "", new Date(), AuctionStatus.NEW, 0);
status = AuctionStatus
keys: Array<number>;
constructor(public service: OffersService) {
this.keys = Object.keys(this.status).filter(k => !isNaN(Number(k))).map(Number);
this.selectedOffer = service.findById(this.editedOfferId)!;
}
ngOnInit(): void {
}
}
Now when I click on an object in the parent component, the details will be loaded in de detail component, but when I edit for example the title input field and then change the object in the parent, I would expect that the data of the new selected object will be loaded. This happens, but only the fields that were not edited at that moment, so when I edit the title, everything will be loaded correctly, but the value of the title will remain the same. Even tough the object has a different title, the value that I was typing stays in the input field. Why is this happening and how can I fix this?
First of all , you did not implement your save changes business logic of your details component , so , when you make some changes in your object , all changes you made will not save then will not be loaded in your parent component using service1. so recommend implementing a save change service request to apply changes .
to do that exactly how you want by your code logic , Use one of those solutions:
Solution 1:
Create an update method for each offer field in your OffersService.
class OffersService {
...
updateTitle(offerId:number,newTitle:string){ ... }
updateDescription(offerId,newDesc:string){ ... }
....
}
In your edit component use onFocuseOut event ( angular focus Out event depend to your angular version ) to each control field , example for title:
<tr>
<th scope="row">Title:</th>
<td><input type="text" id="txtTitle" (focusout)="service1.updateTitle(editedOfferId,$event.target.value)"></td>
</tr>
please do the same thing for each other fields and make sure your update.. methods save offer values as well .
Solution 2:
create a service method in service1 that saves all changes of a specific Offer object. example:
class OffersService {
...
updateOfferById(id:number,newOffer:Offer) { ... }
...
}
make submit changes button in your details component .html , and in its handler implement change detection and call :
service1.updateOfferById(editedOfferId,changedOffer).subscribe( ... );
the difference between the two methods , the first apply changes without using user action to do like submit change button, the input focusOut event do that for each field instead of saving all changes at once .
Instead of using the offerSelectedId in your Overview3Component, change your variable to store the entire object reference.
So instead of public offerSelectedId: number = -1;,
use something like public offerSelected: Offer = new Offer(...)
Then set the selected item (this.offerSelected = offers[i];)
and pass the reference (offerSelected) into the your child component Detail3Component.
#Component({
selector: 'app-overview3',
templateUrl: './overview3.component.html',
styleUrls: ['./overview3.component.css']
})
export class Overview3Component implements OnInit {
public offerSelected: Offer = new Offer("", "", new Date(), AuctionStatus.NEW, 0);
allOffers: Offer[] = [];
constructor(public service1: OffersService) { }
ngOnInit(): void {
this.allOffers = this.service1.findAll();
}
public highlightClickedRow(offer :Offer) {
for (let i = 0; i < this.allOffers.length; i++) {
if (offers[i] != offer) {
offers[i].rowClicked = false;
}
else {
offers[i].rowClicked = true;
this.offerSelected = offers[i]; // set with entire object
}
}
}
}
Remember to make the required changes in the child component as well, changing from just the object ID #Input() editedOfferId: number = -1;
to the object #Input() selectedOffer: Offer;.
#Component({
selector: 'app-detail3',
templateUrl: './detail3.component.html',
styleUrls: ['./detail3.component.css']
})
export class Detail3Component implements OnInit {
// NOTE: This object would be binded back to the source
#Input() selectedOffer: Offer = new Offer("", "", new Date(), AuctionStatus.NEW, 0);
constructor() {
}
ngOnInit(): void {
}
}
Passing the entire object reference like this will allow you to change the values and have them reflected in the parent, in this case the allOffers array.
There should be no need for the EventEmitter #Output() editedOfferChanged = new EventEmitter<number>(); unless you want to know if they changed something.
Just note that this will only update the data in the current state and will not be saved on refresh unless you implement some saving logic.
Also this is by no means the best solution but it is a solution.
You've not implemented save changes logic in the detail3 component and values are not linked to any object or variable. It can be done in multiple ways. You should pass an offer object to the child component instead of an id so that you don't have to find selected offer from all the offers. Bind selected offer object to your forms using ngModel or use reactive forms.
Since data passed to child component is object and any change in object will be reflected to main array (you can prevent that by using spread operator when passing value to child component if you don't want these change to be reflected to main array and fire an event to manually to save data on change).
I would prefer reactive forms and submit/save button to save change. I've implemented an example in stackblitz.
"the value of the title will remain the same"
<tr>
<th scope="row">Title:</th>
<td><input type="text" id="txtTitle" (input)="checkObjectChanged()"></td>
</tr>
Your child component don't assign value for title.
value="{{service.findById(editedOfferId)!.title}}"
It seem the parent component doesn't reload the list offers after child component already updated it.
I suggest you change the function:
(editedOfferChanged)="offerSelectedId=$event"
To
(editedOfferChanged)="onEditedOffer($event)"
For easily debug and do few tasks at parent component after offer was updated at child component.
For Example
onEditedOffer(event){
console.log('onEditedOffer',event;
offerSelectedId = event;
//do something to update list or single offer.
//i choose easy way "reload list offers"
offers: Offer[] = this.service1.findAll();
}
it is possible to simply use Ngmodel with your variable.
[(ngModel)]="YourVar"
in the tag you want and then on the .ts side you can reuse this same variable by using the name of the variable
Related
I have two components first and second. From the second component, I am calling the first component. In the first component, I have a matslider module and I want to get that slider on/off status to my second component ts file. So I am getting that value in first, but don't know how to pass that to the second component.
first.component.html
<div>
<mat-slide-toggle class="toggles"
(change)="OnToggle($event)
[(ngModel)]="selected">Toggle</mat-slide-toggle>
</div>
first.component.ts
#Input() selected=false;
public OnToggle(event)
{
this.selected = event.selected;
}
second.component.html
<div class="container">
<app-first> </app-first>
</div>
I think you can use an output event in the first component and bind to it in the second component.
here it is example:
First Component:
#Output() selectedChange = new EventEmitter<boolean>();
public OnToggle(event) {
this.selected = event.selected;
this.selectedChange.emit(this.selected);
}
SecondComponent:
<app-first (selectedChange)="onSelectedChange($event)"></app-first>
public onSelectedChange(selected: boolean) {
console.log(selected);
}
I am working with inputs but I am not really sure about how is the configuration of the navigation done (I guess that it are predefined behaviours).
I am not in the last input the enter key goes to the next one. This one is working as I want.
Nevertheless, when I am on the last input, when I press enter, it automatically clicks on the next button.
This is what I am trying to avoid. Is there any way to change this behaviour? Just to close the keyboard or to click on another button?
I have tried with keyup.enter and it pseudo works. It calls to the method but also clicks on the next button
HTML
<input
type="text"
class="form-control"
id="validationCustomSurname"
placeholder="e.g. Lopez"
required
(keyup.enter)="onNavigate(1, 'forward')"
[(ngModel)]="values.store.surname"
name="surname"
/>
This method should work on a phone, so I guess that keydown is not an option since $event.code does not give me any code in the phone.
Some time ago I make a directive see stackblitz that you apply in a div (or in a form) in the way
<form [formGroup]="myForm" (submit)="submit(myForm)" enter-tab>
Each input or button add a reference variable #nextTab like
<input name="input1" formControlName="input1" #nextTab/>
<button type="button" #nextTab/>
</form>
The directive use ContentChildren to add a keydown.enter to all the components that have #nextTab to focus to the next control
export class EnterTabDirective {
#ContentChildren("nextTab") controls: QueryList<any>
nextTab
constructor(private renderer: Renderer2, private el: ElementRef) {
}
ngAfterViewInit(): void {
this.controls.changes.subscribe(controls => {
this.createKeydownEnter(controls);
})
if (this.controls.length) {
this.createKeydownEnter(this.controls);
}
}
private createKeydownEnter(querycontrols) {
querycontrols.forEach(c => {
this.renderer.listen(c.nativeElement, 'keydown.enter', (event) => {
if (this.controls.last != c) {
let controls = querycontrols.toArray();
let index = controls.findIndex(d => d == c);
if (index >= 0) {
let nextControl = controls.find((n, i) => n && !n.nativeElement.attributes.disabled && i > index)
if (nextControl) {
nextControl.nativeElement.focus();
event.preventDefault();
}
}
}
})
})
}
Here's a very simple approach, with just a few lines of code:
First, in your Template when you dynamically create your Input elements: 1. populate the tabIndex attribute with a unique number, 2. populate a super-simple custom "Tag" Directive with the same unique number as the tabIndex, and 3. set up a Keydown "Enter" event listener:
Template:
<ng-container *ngFor="let row in data">
<input tabindex ="{{row[tabCol]}}" [appTag]="{{row[tabCol]}}" (keydown.enter)="onEnter($event)" . . . />
</ng-container>
In your component, your super-simple event-listener onEnter():
#ViewChildren(TagDirective) ipt!: QueryList<ElementRef>;
onEnter(e: Event) {
this.ipt["_results"][(<HTMLInputElement>e.target).tabIndex%(+this.ipt["_results"].length-1)+1].el.nativeElement.focus();
}
Note: The modulus (%) operation is just to make sure that if you're at the last Input, you'll get cycled back to the first input.
Super-simple, bare-minimum "Tag" Directive
import { Directive, ElementRef, Input } from '#angular/core';
#Directive({
selector: '[appTag]'
})
export class TagDirective {
#Input('appTag') id: number;
constructor(public el: ElementRef) { }
}
There's probably even a way to get rid of the "Tag" `Directive altogether and make it even more simple, but I haven't had time to figure out how to do that yet . . .
Objective: Get a collection of values based on the dropdown selection and place them in hidden input fields to be included in my model;
The relative html:
<select class="selectFoo" (change)="onSelect($event.target.value)" name="FooName" ngModel>
<option selected="selected">--Select--</option>
<option *ngFor="let foo of foos" [value]="foo.ID">{{foo.Name}}
</option>
</select>
<input type="hidden" [value]="fooAddress" name="FooAddress" ngModel/>
In the code above I called a function named OnSelect to get the data about the selected foo. The foos are populated using a webservice call. Here is the snippet from my ts file.
import { Component, OnInit } from '#angular/core';
import { Foo } from './model';
import { DataService } from './data.service';
#Component({
moduleId: module.id,
selector: 'add-on',
templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
foos : Foo[];
selectedFoo: Foo;
fooAddress: string;
onSelect(fooID){
this.selectedFoo = null;
for(var i = 0; i < this.foos.length; i++)
{
console.log(this.foos[i].ID);
if(this.foos[i].ID == fooID){
this.selectedFoo = this.foos[i];
this.fooAddress = this.selectedFoo.Address.toString();
}
}
}
}
I originally tried one way binding my value to the selectedFoo but I was getting an error indicating my Address value wasn't defined. I noticed I could set the value equal to selectedFoo and it didn't error. So i created a new variable that was set to the fooAddress based on the selected foo. I get no value even though while stepping through the code I see it has a value.
How can I get my value to populate so I can use it in my model? Let me know if I need to provide anything else.
Thanks!
If I am correctly understanding what you are after then something like this would work:
<select name="FooName" [(ngModel)]="selectedFoo">
<option>--Select--</option>
<option *ngFor="let foo of foos" [ngValue]="foo" >{{foo.Name}}</option>
</select>
<input type="hidden" [value]="selectedFoo?.Address" name="FooAddress" />
//Assuming your 'foo' items are e.g. { ID: 1, Name: 'Hello', Address: '123 Hello St'}
Here you can bind the Address property of the selectedFoo directly to your hidden input field, rather than needing to handle the (change) event.
The current official docs only shows how to dynamically change components within an <ng-template> tag. https://angular.io/guide/dynamic-component-loader
What I want to achieve is, let's say I have 3 components: header, section, and footer with the following selectors:
<app-header>
<app-section>
<app-footer>
And then there are 6 buttons that will add or remove each component: Add Header, Add Section, and Add Footer
and when I click Add Header, the page will add <app-header> to the page that renders it, so the page will contain:
<app-header>
And then if I click Add Section twice, the page will now contain:
<app-header>
<app-section>
<app-section>
And if I click Add Footer, the page will now contain all these components:
<app-header>
<app-section>
<app-section>
<app-footer>
Is it possible to achieve this in Angular? Note that ngFor is not the solution I'm looking for, as it only allows to add the same components, not different components to a page.
EDIT: ngIf and ngFor is not the solution I'm looking for as the templates are already predetermined. What I am looking for is something like a stack of components or an array of components where we can add, remove, and change any index of the array easily.
EDIT 2: To make it more clear, let's have another example of why ngFor does not work. Let's say we have the following components:
<app-header>
<app-introduction>
<app-camera>
<app-editor>
<app-footer>
Now here comes a new component, <app-description>, which the user wants to insert in between and <app-editor>. ngFor works only if there is one same component that I want to loop over and over. But for different components, ngFor fails here.
What you're trying to achieve can be done by creating components dynamically using the ComponentFactoryResolver and then injecting them into a ViewContainerRef. One way to do this dynamically is by passing the class of the component as an argument of your function that will create and inject the component.
See example below:
import {
Component,
ComponentFactoryResolver, Type,
ViewChild,
ViewContainerRef
} from '#angular/core';
// Example component (can be any component e.g. app-header app-section)
import { DraggableComponent } from './components/draggable/draggable.component';
#Component({
selector: 'app-root',
template: `
<!-- Pass the component class as an argument to add and remove based on the component class -->
<button (click)="addComponent(draggableComponentClass)">Add</button>
<button (click)="removeComponent(draggableComponentClass)">Remove</button>
<div>
<!-- Use ng-template to ensure that the generated components end up in the right place -->
<ng-template #container>
</ng-template>
</div>
`
})
export class AppComponent {
#ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef;
// Keep track of list of generated components for removal purposes
components = [];
// Expose class so that it can be used in the template
draggableComponentClass = DraggableComponent;
constructor(private componentFactoryResolver: ComponentFactoryResolver) {
}
addComponent(componentClass: Type<any>) {
// Create component dynamically inside the ng-template
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentClass);
const component = this.container.createComponent(componentFactory);
// Push the component so that we can keep track of which components are created
this.components.push(component);
}
removeComponent(componentClass: Type<any>) {
// Find the component
const component = this.components.find((component) => component.instance instanceof componentClass);
const componentIndex = this.components.indexOf(component);
if (componentIndex !== -1) {
// Remove component from both view and array
this.container.remove(this.container.indexOf(component));
this.components.splice(componentIndex, 1);
}
}
}
Notes:
If you want to make it easier to remove the components later on, you can keep track of them in a local variable, see this.components. Alternatively you can loop over all the elements inside the ViewContainerRef.
You have to register your component as an entry component. In your module definition register your component as an entryComponent (entryComponents: [DraggableComponent]).
Running example:
https://plnkr.co/edit/mrXtE1ICw5yeIUke7wl5
For more information:
https://angular.io/guide/dynamic-component-loader
Angular v13 or above - simple way to add dynamic components to DOM
parent.component.html
<ng-template #viewContainerRef></ng-template>
parent.component.ts
#ViewChild("viewContainerRef", { read: ViewContainerRef }) vcr!: ViewContainerRef;
ref!: ComponentRef<YourChildComponent>
addChild() {
this.ref = this.vcr.createComponent(YourChildComponent)
}
removeChild() {
const index = this.vcr.indexOf(this.ref.hostView)
if (index != -1) this.vcr.remove(index)
}
Angular v12 or below
I have created a demo to show the dynamic add and remove process.
The parent component creates the child components dynamically and removes them.
Click for demo
Parent Component
// .ts
export class ParentComponent {
#ViewChild("viewContainerRef", { read: ViewContainerRef })
VCR: ViewContainerRef;
child_unique_key: number = 0;
componentsReferences = Array<ComponentRef<ChildComponent>>()
constructor(private CFR: ComponentFactoryResolver) {}
createComponent() {
let componentFactory = this.CFR.resolveComponentFactory(ChildComponent);
let childComponentRef = this.VCR.createComponent(componentFactory);
let childComponent = childComponentRef.instance;
childComponent.unique_key = ++this.child_unique_key;
childComponent.parentRef = this;
// add reference for newly created component
this.componentsReferences.push(childComponentRef);
}
remove(key: number) {
if (this.VCR.length < 1) return;
let componentRef = this.componentsReferences.filter(
x => x.instance.unique_key == key
)[0];
let vcrIndex: number = this.VCR.indexOf(componentRef as any);
// removing component from container
this.VCR.remove(vcrIndex);
// removing component from the list
this.componentsReferences = this.componentsReferences.filter(
x => x.instance.unique_key !== key
);
}
}
// .html
<button type="button" (click)="createComponent()">
I am Parent, Create Child
</button>
<div>
<ng-template #viewContainerRef></ng-template>
</div>
Child Component
// .ts
export class ChildComponent {
public unique_key: number;
public parentRef: ParentComponent;
constructor() {
}
remove_me() {
console.log(this.unique_key)
this.parentRef.remove(this.unique_key)
}
}
// .html
<button (click)="remove_me()">I am a Child {{unique_key}}, click to Remove</button>
I have a stockTakeDetail component which is used on a tr tag as a table row... i'm using the component with the [componentName] attribute selector on the table row, the stockTakeDetail component emits a removeItem value when a button is clicked which i'm not sure at all how to register that event with the it's parent component... Normally I would do something like this:
<stockTakeDetail (removeItem)="removeItem($event)"></stockTakeDetail>
In my parent component I have the following code:
<tr *ngFor="let stockTakeDetailItem of stockTakeDetailList" [stockTakeDetail]="stockTakeDetailItem"></tr>
Any idea how I could add the event on my parent html tr? Or is there a way to get the event directly in the parent's controller?
my code:
stock-take-detail.html:
<td>{{stockTakeDetail.ProductDetail_Name</td>
<td>{{stockTakeDetail.Qty}}</td>
<td><button class="btn btn-xs" (click)="removeCaptureItem({{stockTakeDetail.IDKey}})"><span class="glyphicon glyphicon-trash"></span></button></td>
stock-take-detail.ts:
import { Component, Input, EventEmitter, Output, OnInit } from '#angular/core';
import { StockTakeDetailModel } from '../../models/stock-take-detail.model';
#Component({
selector: '[stockTakeDetail]',
templateUrl: './stock-take-detail.component.html',
styleUrls: ['./stock-take-detail.component.css']
})
export class StockTakeDetailComponent implements OnInit {
#Input() stockTakeDetail: StockTakeDetailModel;
#Output() removeItem = new EventEmitter<string>();
removeCaptureItem(iDKey: string) {
this.removeItem.emit()
}
constructor() { }
ngOnInit() {
}
}
table from stock-take.html:
<table class="table table-hover">
<thead>
<tr>
<th>Desc</th>
<th>Qty</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<!--<tr *ngFor="let stockTakeDetailItem of stockTakeDetailList" [stockTakeDeTail]="stockTakeDetailItem"></tr>-->
</tbody>
</table>
This might help; parent has an EventEmitter, you pass it into the children in the ngFor. Any of the children that use that emitter to emit an event, all the other children will hear it if you want (they just subscribe), and the parent will hear any component that uses its emitter to emit an event (e.g. the children) if you subscribe. Very easy, works great.
<ul>
<li *ngFor="let btn of buttons">
// Bind the parent's broadcast listener, to the child Input.
<my-child [broadcastListener]="broadcasterParent">
</my-child>
</li>
</ul>
Parent:
import { Component, EventEmitter } from '#angular/core';
...
constructor ) {
// this is the instance given to the child as Input
this.broadcasterParent = new EventEmitter ( );
// Note if you want the parent to hear the event,
// subscribe to the event emitter, just like the children do.
this.broadcastListener.subscribe( ( b ) => this.onBroadcast ( b ) );
}
onBroadcast ( evt ) {
... you will hear when the child emits an event using the emitter
}
Child:
import { Component } from '#angular/core';
...
constructor ( ) {
// This is almost certainly better used in OnInit.
// It is how the child subscribes to the parent's event emitter.
// You only need to do this if you want to hear events.
this.broadcastListener.subscribe( ( b ) => this.onBroadcast ( b ) );
}
onButtonClick ( evt ) {
// Any component anywhere that has subscribed
// to this emitter will hear this event, e.g. the parent or whatever
// andwill hear it
this.broadcastListener.emit ( evt );
}
// Only needed if the child subscribed to the parent's emitter
onBroadcast ( evt ) {
// This will be the data that was sent by whatever button was clicked
// via the subscription to the Input event emitter (parent, app, etc).
console.log ( evt );
}
ChildComponent.annotations = [
new Component ( {
... // TS would use an attribute for this
inputs: [ 'broadcastListener' ]
template: `
<div click)="onButtonClick ( $event )">
...
</div>
`
})
];
Explained in more detail here: (Peer Events in Angular 2):
www.tcoz.com/#/errata