Using #Output() with dynamically added components - javascript

I'm creating a shopping list.
It will be made out of two components: shopping-cart and shopped-item.
The shopping-cart has a button that dynamically adds a new shopped-item in a <div>.
The shopped-item after being added can be marked as active or unmarked so I created an EventEmmiter that changes the value marked/unmarked.
But since the component is added dynamically I don't know where to add it in shopping-cart component...
How can I make it work like this:
After the shopped-item is added it appears in an array with marked/unmarked value that changes when it's clicked in the shopped-item component?
Cheers!
Shopped-item.ts file:
export class ShoppedItemComponent implements OnInit {
_ref:any;
removeObject(){
this._ref.destroy();
}
#Output() statusChange = new EventEmitter<{status: boolean}>();
marked;
unmarkItem () {
this.marked = !this.marked;
this.statusChange.emit({status: this.marked});
}
constructor() {
}
}
Shopping-cart.ts file:
export class ShoppingCartComponent implements OnInit {
#ViewChild('boughtItems', { read: ViewContainerRef }) boughtItems:
ViewContainerRef;
constructor(
private resolver: ComponentFactoryResolver
) { }
isMarked = [];
shoppedItemStatus (statusChange: {status: boolean}) {
this.isMarked.push({
status: statusChange.status
})
}
addItem() {
const shoppedItem =
this.resolver.resolveComponentFactory(ShoppedItemComponent);
const component = this.boughtItems.createComponent(shoppedItem);
component.instance._ref = component;
}
}
Shopping-cart.html file:
<div #boughtItems>
<button (click)="addItem()">ADD</button>
</div>

Why are you creating the components by hand?
I would use a *ngFor in the view
<div #boughtItems>
<button (click)="addItem()">ADD</button>
<div *ngFor="let item of items">
<app-item-bought xxxx="item" (markToggle)="myFunction(item)"></app-item-bought>
</div>
</div>
where xxxx is a field of your class ShoppedItemComponent decorated with Input('xxxx').
(markToggle) is the name of the emitter in ShoppedItemComponent and myFunction(item) is a function defined in the Shopping-cart that will receive the item that has fired the event.
Hope it helps!

Related

How to get a dynamically generated element in Angular without querySelector?

I am currently creating my own toastr service as seen in the GIF below
What I want to achieve
https://stackblitz.com/edit/angular-ivy-tgm4st?file=src/app/app.component.ts
But without queryselector. From what i have read, you should not be using queryselector for retrieving elements in the DOM in angular
The issue
Whenever I click the CTA button I add a toast element to an array of toasts which the component is subscribed to and utilizes to update the DOM.
The toasts are generated like this:
export class ToastComponent implements OnInit {
constructor(private toast: ToastService, protected elementRef: ElementRef) {}
toasts = this.toast.Toasts;
<div
class="toast-wrapper wobble-animation"
*ngFor="let t of toasts.value"
(click)="DestroyToast(t, $event)"
What I want
I want to add an eventlistener to the toast whenever 'animationend' to destroy the HTML element. I already do this by when clicking with this line of code:
DestroyToast(element, event): void {
event.target.classList.remove('wobble-animation');
event.target.classList.add('slide-out-animation');
event.target.addEventListener('animationend', () => {
this.toasts.value.splice(this.toasts.value.indexOf(element), 1);
});
}
My initial thought was to subscribe to the array and use that as an eventlistener for when something is pushed. I would then use a function to fetch the latest toast and add another eventlistener, the 'animationend' one.
I tried the method like this:
ngOnInit(): void {
this.toast.Toasts.subscribe((args) => {
this.UpdateToasts();
});
}
UpdateToasts() {
let toastElements = document.querySelectorAll('.toast');
console.log(toastElements);
}
But unfortunately it is too slow and always returns null on the first event.
I think that I have read that using querySelector in angular is generally bad practice. So the question is:
How to get a dynamically generated element in Angular without querySelector?
FULL CODE
Toast.Component.ts
import { ToastService } from './../../services/toast.service';
import { toast } from './toast.model';
import { Component, OnInit, ElementRef } from '#angular/core';
import { Observable } from 'rxjs';
#Component({
selector: 'app-toast',
templateUrl: './toast.component.html',
styleUrls: ['./toast.component.scss'],
})
export class ToastComponent implements OnInit {
constructor(private toast: ToastService, protected elementRef: ElementRef) {}
toasts = this.toast.Toasts;
ngOnInit(): void {
this.toast.Toasts.subscribe((args) => {
this.UpdateToasts();
});
}
ngOnDestroy() {
this.toasts.unsubscribe();
}
DestroyToast(element, event): void {
event.target.classList.remove('wobble-animation');
event.target.classList.add('slide-out-animation');
event.target.addEventListener('animationend', () => {
this.toasts.value.splice(this.toasts.value.indexOf(element), 1);
});
}
UpdateToasts() {
let toastElements = document.querySelectorAll('.toast');
console.log(toastElements);
}
}
Toast.Component.html
<div class="toast-container">
<div
class="toast-wrapper wobble-animation"
*ngFor="let t of toasts.value"
(click)="DestroyToast(t, $event)"
>
<div
class="toast default"
[ngClass]="{ 'slide-out-animation': t.TimeLeft < 1 }"
>
<div class="notification-count" *ngIf="t.Count > 1">
{{ t.Count }}
</div>
<div class="content-container">
<p class="title">
{{ t.Title }}
</p>
<p class="content">{{ t.Content }}</p>
</div>
<span class="progress">
<span
class="real-progress"
[ngStyle]="{ 'width.%': t.PercentageCompleted }"
></span>
</span>
</div>
</div>
</div>
Toast.Service.ts
import { Injectable } from '#angular/core';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { toast } from '../components/toast/toast.model';
#Injectable({
providedIn: 'root',
})
export class ToastService {
public Toasts = new BehaviorSubject<Array<object>>([]);
constructor() {}
Toast(Title: string, Message?: string, Style?: string, Timer?: number) {
const toastModel = new toast({
Title: Title,
Content: Message,
Timer: Timer,
Style: Style,
TimeLeft: Timer,
Count: 1,
PercentageCompleted: 100,
});
this.AddToast(toastModel);
}
private AddToast(toast: toast) {
const currentArr = this.Toasts.value;
const updatedToast = [...currentArr, toast];
let timer = setInterval(function () {
toast.PercentageCompleted = toast.TimeLeft / (toast.Timer / 100);
toast.TimeLeft = toast.TimeLeft - 10;
if (toast.TimeLeft <= 0 || !toast.TimeLeft) {
clearInterval(timer);
}
}, 10);
this.Toasts.next(updatedToast);
}
}
Link to website with live code ModernnaMedia
I'm not 100% sure I understood you correctly, there seem to be two animationend events going on.
I want to add an eventlistener to the toast whenever 'animationend' to destroy the HTML element.
You can bind that directly in the template:
<div
*ngFor="let toast of toasts"
#toastEl
(animationend)="DestroyToast(toastEl)"
class="toast">
</div>
DestroyToast(toastEl: HTMLElement) {
// …
}
Like mentioned by others already, using ViewChildren would be the "Angular" way to do it, instead of queryselector. We can also with ViewChildren subscribe to changes of the querylist we are listening to! I think that is probably suitable for your code...
So first, attach a ref to the toasts, here I just call it myToasts:
<div
#myToasts
class="toast default"
[ngClass]="{ 'slide-out-animation': t.TimeLeft < 1 }"
>
OK, now declare the querylist in the component:
#ViewChildren('myToasts') myToasts: QueryList<ElementRef>;
Now you can simply subscribe to the changes in AfterViewInit and do whatever you need to do with the elements:
ngAfterViewInit() {
this.myToasts.changes.subscribe(toasts => {
console.log('Array length: ', toasts.length);
console.log('Array of elements: ', toasts.toArray())
})
}
if you add rxjs delay function after your observable variable like below
this.toast.Toasts.pipe(delay(0)).subscribe(()=>{this.UpdateToasts();})
you will not get null reference error.
and if you don't want to use queryselector you can use angular viewchildren
for more information visit angular documentation site.
https://angular.io/api/core/ViewChildren

Angular - How to get data from subscribed service few times and not overwrite previous results

I have a service which has following method:
room-service.ts
#Injectable()
export class RoomService {
private readonly _equipment: BehaviorSubject<EquipmentDto[]> = new BehaviorSubject([]);
public equipment$ = this._equipment.asObservable();
getEquipmentForRoom(roomId: number) {
this.restService.getEquipmentForRoom(roomId).subscribe(res => {
this._equipment.next(res);
});
}
room-component.ts:
#Component()
export class RoomsComponent implements OnInit {
#Input() room: RoomEntity;
equipment$: Observable < EquipmentDto[] > ;
equipmentList: Array < EquipmentDto > ;
constructor(private equipmentService: EquipmentService) {}
ngOnInit() {
this.equipment$ = this.equipmentService.equipment$;
this.equipmentService.getEquipmentForRoom(this.room.id);
this.equipment$.subscribe(items => {
this.equipmentList = items;
});
room-component.html
<div *ngFor="let eq of room.equipmentList">
<!-- list my equipment here -->
</div>
Now I have a parent component which contains multiple Room Components (those are added programmatically based on the amount of rooms). Anyway, list of equipment is the same for each of the rooms. It looks like once subscribed, the data in first components is overwritten by the component created as the last one.
My question is, how can I get a proper data for each of the rooms using the observable from my service?
You can use this approach with single BehaviorSubject only when your data is the only source of this data.
Instead, you can change your getEquipmentForRoom(roomId: number) like this:
getEquipmentForRoom(roomId: number) {
return this.restService.getEquipmentForRoom(roomId);
}
And then subscribe to it in the compoment:
this.equipmentService.getEquipmentForRoom(this.room.id).subscribe(items => {
this.equipmentList = items;
});
And I agree with Alexander, this component should be dumb as possible.
try to build pipes instead of subscriptions
#Injectable()
export class RoomService {
// return an observable.
getEquipmentForRoom$(roomId: number) {
return this.restService.getEquipmentForRoom(roomId);
}
#Component()
export class RoomsComponent implements OnInit {
#Input() room: RoomEntity;
equipment$: Observable<EquipmentDto[]>;
constructor(private equipmentService: EquipmentService){}
ngOnInit() {
// simply share observable.
this.equipment$ = this.equipmentService.getEquipmentForRoom$(this.room.id);
});
<div *ngFor="let eq of equipment$ | async"> <!-- add async here -->
<!-- list my equipment here -->
</div>
You can fetch the data once in your parent and pass the data to your child components.
That child component (RoomsComponent) should be dumb and not doing requests
Adding onto Alexander’s answer.
The problem is that you are subscribing to the same BehaviourSubject for all the components and the components are taking the latest roomData that is emitted by the BehaviourSubject, which would be the last RoomComponent.
It would work if the BehaviuorSubject holds all the rooms which are fetched by the parent component, and passing each room data to each RoomComponent using #Input.

How do you access the for loop of the parent component within a child component which has the inputs?

So I am stuck on this. I am trying to get the Parent component to talk or integrate with the child component.
Here is the parent component which basically has the for loop used to iterate or generate more links if a user wants to add more or presses the button to add more.
<div class="section url-wrapper">
<div *ngFor="let url of urls; let i = index;" class="link input-wrapper">
<childComponent></childComponent>
<button class="button bad icon-only" (click)="removeURL(i)">
<i class="far fa-times"></i>
</button>
</div>
</div>
The parent component should only be able to register and display the output of the child component.
This is an example of the child component
<div class="section url-wrap">
<input aria-label="URL Title" placeholder="Title" type="text" [value]="urls[i].title" (ngModel)="urls[i].title" name="url.title.{{ i }}"
(input)="updateTitle(i, $event.target.value)">
<input aria-label="URL" placeholder="https://example.com" type="text" [value]="urls[i].url" (ngModel)="urls[i].url" name="url.url.{{ i }}"
(input)="updateUrl(i, $event.target.value)">
</div>
I need help both allowing the parent component to register input from the child component and being able to iterate from the for loop from the parent if it is possible.
Please let me know if you need more information such as the component files or clarification
The below code & example will demonstrate how data flows from parent -> child -> parent by using the #Input() and #Output() directives.
Working Example Here
parent.component.ts
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-parent',
template: `
<div class="section url-wrapper">
<div *ngFor="let url of urls" class="link input-wrapper">
<app-child [url]="url" (updateUrl)="onUrlUpdate($event)"></app-child>
</div>
</div>
`
})
export class ParentComponent implements OnInit {
public urls = [
{url: "https://example.com", title: "Example1"},
{url: "https://example.com", title: "Example2"},
{url: "https://example.com", title: "Example3"},
]
constructor() { }
ngOnInit() {
}
onUrlUpdate($event) {
// completely overkill, but just used to demonstrate a point
var url = this.urls.find(_url => {
// we can see here that the $event.url is actually the same object as the urls[i] that was
// passed to the child. We do not lose the reference when it is passed to the child or back
// up to the parent.
return $event.url === _url
});
if (url) {
url[$event.prop] = $event.newValue;
}
console.log(`Updated URL's "${$event.prop}" property with the value "${$event.newValue}"`);
}
}
child.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'app-child',
template: `
<div class="section url-wrap">
<input aria-label="URL Title" placeholder="Title" type="text" [value]="url.title"
(input)="handleUrlUpdate($event, 'title')"/>
<input aria-label="URL" placeholder="https://example.com" type="text" [value]="url.url"
(input)="handleUrlUpdate($event, 'url')"/>
</div>
`,
})
export class ChildComponent implements OnInit {
#Input() url; // passed in from parent via [url] property on <app-child>
#Output() updateUrl = new EventEmitter();
constructor() { }
ngOnInit() {
// this.url is now available for the life of the child component (assuming it was passed by the parent)
}
handleUrlUpdate($event, propToUpdate) {
// overkill, but used to demonstrate a point
this.updateUrl.emit({url: this.url, prop: propToUpdate, newValue: $event.target.value});
}
}
The stardard way to let components speack each others is with input-output:
You can pass values from parent to children with #Input for example:
Parent code:
<childComponent [someInputValue]="hello"></childComponent>
Children code:
#Input() someInputValue; //this property will be "hello"
and you can pass values (after being triggered) from children to parent:
Children code:
#Output() itemSelectedOutput: EventEmitter<any> = new EventEmitter();
buttonClicked() {
this.itemSelectedOutput.emit("clicked");
}
Parent code:
<childComponent [someInputValue]="hello" (itemSelectedOutput)="someParentMethod($event)"></childComponent>
someParentMethod(event: any) {
console.log(event);
}
You can reach the same thing with ISubscription but I suggest you to use the way above
Hope it can help
I wouldn't do it this way in particular. If the children have to know about the parents, then your architecture should be adjusted

Javascript Angular 4 Change ngClass from another Component

I currently have this code in my app.component.ts
app.component.html
<div [ngClass]="myclass">
...rest of the content here
</div>
This I have the this:
<button (click)="changeClass('myFavClass')">Change Class to myFavClass</div>
app.component.ts
export class AppComponent {
myclass: string;
changeClass(myclass) {
this.myclass = myclass;
}
}
Now, all this works fine BUT I now want to put the triggering button on another component.
If I put this on another component:
<button (click)="changeClass('myFavClass')">Change Class to myFavClass</div>
How can I get it to change the class?
There are two ways you can do this you can use output with an EventEmit
Or you can set up a service that monitors the changes to a variable and use that as the control point for the change.
Personally, I use services for this instance as its easier to manage the code and its flow.
This answer has all the code in you need to look at.
Changing a value in two different components at the same time Angular 2
Hope that helps
There are at least two options. Subject and Observable or if this another component is a parent you can use #Input.
Subject and Observable method:
angular guide Highly recommended to read whole page.
Some component
export class SomeComponent {
constructor(private ClassService: ClassService) { }
private changeClass(class) {
this.ClassService.changeClass(class);
}
}
Another Component
export class AnotherComponent implements OnInit, OnDestroy {
constructor(private ClassService: ClassService) { }
private class: string = "";
private subscribtion: Subscribtion;
ngOnInit(): void {
this.Subscribtion = this.ClassService.someClass$.subscribe(
(class) => { this.class = class; }
)
}
ngOnDestroy(): void {
this.Subscribtion.unsubscribe();
}
}
Service
#Injectable();
export class ClassService{
constructor() { }
private someClassSource= new Subject<string>();
someClass$= this.someClassSource.asObservable();
changeClass(class) {
this.someClassSource.next(class);
}
}
taken from my answer
#Input method:
angular guide
This is very simple, when you click button changeClass method will change elClass which will be passed to another component by #Input decorator, every change of #Input will cause a detect changes which will detect that value has changed so class will change to myClass.
Parent component
parent.component.html
<another-component [elementClass]="elClass"></another-component>
<button (click)="changeClass('myClass')">change class<button>
parent.component.ts
export class ParentComponnet {
private elClass: string = "";
changeClass(class: string) {
elClass = class;
}
}
Another component (must be child component)
another.component.html
<div [ngClass]="elementClass">
another.component.ts
export class AnotherComponent {
#Input() elementClass: string;
}
There is also Child to Parent interaction via #Output (emitting event) angular guide

ngFor doesn't fires after update depending variable in Angular2

I have 2 components: CommandListComponent and CommandLineComponent. Inside of a CommandListComponent template i handle a click event on a text string:
CommandListComponent template:
<li *ngFor="#command of commandList" class="b-command-list__command"><span (click)="checkCommand(command)" class="b-command-list__text">{{command}}</span></li>
commandlist.component.ts
import {CommandLineComponent} from "./commandline.component";
...
export class CommandListComponent {
commandLineComponent: any;
constructor(private _commandLine: CommandLineComponent) {
this.commandLineComponent = _commandLine;
}
checkCommand(command: string): void {
this.commandLineComponent.add(command);
}
}
When click is fired i pass choosen command to add method of a CommandLineComponent:
export class CommandLineComponent {
commands: string[] = [];
add(command: string): void {
if (command) this.commands.push(command);
console.log(this.commands);
}
}
And within a template of a CommandLineComponent i print a list of a commands with *ngFor:
<li *ngFor="#command of commands" class="b-command-textarea__command">{{command}}</li>
But *ngFor doesn't fires when i choose a command and commands array of a CommandLineComponent updated. So, data binding is not working. commands array updates successfully:
Thank you for help.
The problem is the way you reference the commandLineComponent component. If there is a relation between them you could use the ViewChild decorator
class CommandListComponent {
#ViewChild(CommandLineComponent)
commandLineComponent: any;
(...)
}
If not, you need to use a shared service to share the commands list between these two components. Something like that:
export class CommandService {
commands:string[] = [];
commandAdded:Subject<string> = new Subject();
add(command: string): void {
if (command) {
this.commands.push(command);
this.commandAdded.next(command);
}
console.log(this.commands);
}
}
You need to define the service when bootstrapping your application and both components can inject it.
class CommandListComponent {
constructor(private commandService:CommandService) {
}
}
checkCommand(command: string): void {
this.commandService.add(command);
}
The CommandLineComponent component will be notified of a new command like this and can update the view accordingly:
class CommandLineComponent {
constructor(private commandService:CommandService) {
this.commandService.commandAdded.subscribe(command => {
// Update the list displayed in the component...
});
}
}

Categories

Resources