I have a component and then another component which is using it, and data is being parsed through to each other by means of a tab changing, what I am trying to do is when the page is left but the back button is pressed that the tab is remembered, to be able to do it I need to understand the data flow, the functions are identical so It's difficult to determine what is going on!
For instance, they are both using #Input and when I press a tab, both functions console if I add one in?
the component:
<div class="EngagementNavigationSecondary-Items"
*ngIf="!selectedIndividual">
<span
*ngFor="let item of navList; let i = index"
class="EngagementNavigationSecondary-Item"
[ngClass]="{
'EngagementNavigationSecondary-Item--Active': selectedItem === i,
'EngagementNavigationSecondary-Item--One' : navList.length === 1,
'EngagementNavigationSecondary-Item--Two' : navList.length === 2,
'EngagementNavigationSecondary-Item--Three' : navList.length === 3
}"
(click)="clickTab(i)">
{{ item.title | translate }}
</span>
</div>
<div *ngIf="selectedIndividual">
<span class="EngagementNavigationSecondary-Item" (click)="goBack()">
<tl-icon
size="18"
name="chevron-left">
</tl-icon>
{{ 'Go back' | translate }}
</span>
</div>
the logic:
export class EngagementNavigationSecondaryComponent implements OnInit {
#HostBinding('class.EngagementNavigationSecondary') block = true;
#Input() navList: EngagementNavigationItem[];
#Input() selectedItem: number = 0;
#Input() selectedIndividual: string;
#Output() change: EventEmitter<number> = new EventEmitter<number>();
#Output() deselectIndividual: EventEmitter<boolean> = new EventEmitter<boolean>();
className: string;
ngOnInit() {
this.className = `EngagementNavigationSecondary-Item${this.navList.length}`;
}
goBack() {
this.deselectIndividual.emit(true);
}
clickTab($event: number) {
this.selectedItem = $event;
this.change.emit($event);
}
}
and now this component being used - within the side container component:
<tl-engagement-navigation-secondary
*ngIf="navList"
[navList]="navList"
[selectedItem]="selectedTab"
[selectedIndividual]="selectedIndividual"
(change)="tabChange($event)"
(deselectIndividual)="selectedIndividual = undefined">
</tl-engagement-navigation-secondary>
logic:
export class EngagementSideContainerComponent implements OnChanges, OnInit {
#Input() focus: EngagementGraphNode;
#Input() selectedTab: number;
#Output() change: EventEmitter<number> = new EventEmitter<number>();
public navList: EngagementNavigationItem[];
public selectedIndividual: EngagementUser;
constructor(
private urlService: UrlService,
private router: Router,
private mixPanelService: MixPanelService,
private engagementService: EngagementService
) { }
ngOnInit() {
this.navList = this.getNavList();
}
tabChange(event) {
this.change.emit(event);
}
as you can see they are basically identical, so when I click on a tab, is the original component being called and then this is parsing data to the side container component? I think its important to understand so I can actually create the solution.
thanks if you can help!
Related
I'm very new to Angular, and I'm really struggling to find a concise answer to this problem. I have a Form Component Here:
(I'm excluding the directives and imports as they're not really relevant)
export class JournalFormComponent implements OnInit {
public entries: EntriesService;
constructor(entries: EntriesService) {
this.entries = entries;
}
ngOnInit(): void {
}
}
The EntriesService service just stores an array of entries:
export class Entry {
constructor (
public num: number,
public name: string,
public description: string,
public text: string
) { }
}
The Form Component template renders a <h2> and a <app-input> Component for each entry in the EntriesService, which works. That looks like this:
<div *ngFor="let entry of entries.entries">
<h2> {{ entry.num }}. {{ entry.name }} </h2>
<app-input id="{{entry.num}}"></app-input>
</div>
Here's the <app-input> Input Component:
#Component({
selector: 'app-input',
template: `
<textarea #box
(keyup.enter)="update(box.value)"
(blur)="update(box.value)">
</textarea>
`
})
export class InputComponent {
private value = '';
update(value: string) {
this.value = value;
}
getValue () {
return this.value;
}
}
The InputComponent stores the user's text perfectly, but I don't know how to pass that data to the Form Component's EntriesService to update the Entry in order to Export it or Save it later. How is this done?
I think I'm phrasing this question well, but I'm not sure. If you need clarification I'll provide it.
Not sure if it matters, but I'm using Angular 9.1.11
There are many ways to update the data from one component to another.
component to component using service or subjects
parent~child component data exchange using Input() and Output() decorators. Or by using #ViweChild() interactions.
and many more
But please do check the angular docs https://angular.io/guide/component-interaction .
Use the below simple code, u might need to include modules like FormsModule. and import Input(), Output etc
#Component({
selector: 'app-journal-form',
template: `
<div *ngFor="let entry of entries.entries; let i=index">
<h2> {{ entry.num }}. {{ entry.name }} </h2>
<app-input id="{{entry.num}}" [entry]="entry" [arrayIndex]="i" (updateEntry)="updateEntry($event)" ></app-input>
</div>`
})
export class JournalFormComponent implements OnInit {
constructor(private entries: EntriesService) {
this.entries = entries;
}
ngOnInit(): void {
}
updateEntry(event){
console.log(event);
this.entries[event.arrayIndex] = event.entry;
}
}
#Component({
selector: 'app-input',
template: `
<textarea [(ngModel)]="name"
(keyup.enter)="update()"
(blur)="update()">
</textarea>
`
})
export class InputComponent {
#Input() entry: any;
#Input() arrayIndex: number;
#Output() updateEntry: EventEmitter<any> = new EventEmitter();
name:string;
constructor() {
console.log(entry);
this.name = entry.name;
}
update(){
this.entry.name = this.name;
this.updateEntry.emit({entry: this.entry, arrayIndex});
}
}
Output event will help in this situation.
<div *ngFor="let entry of entries.entries">
<h2> {{ entry.num }}. {{ entry.name }} </h2>
<app-input id="{{entry.num}}" (entryChange) = "entry.text = $event"></app-input>
</div>
app-input component
export class InputComponent {
private value = '';
#Output() entryChange = new EventEmitter<string>();
update(value: string) {
this.value = value;
this.entryChange.emit(value);
}
}
Instead of entry.text = $event you can also pass it to any save function, like saveEntry($event);
Hi I am now working on angular to build a multiselect dropdown using ng-multiselect-dropdown(https://www.npmjs.com/package/ng-multiselect-dropdown).
I used parent-child component communication through event emitter:
in child.component.ts:
import {Component, EventEmitter, Input, OnInit, Output} from '#angular/core';
import {IDropdownSettings} from 'ng-multiselect-dropdown';
export interface IMultiDropdownConfig {
placeholder: string;
header: string;
dropdownSettings: IDropdownSettings;
}
#Component({
selector: 'app-multi-dropdown',
templateUrl: './multi-dropdown.component.html',
styleUrls: ['./multi-dropdown.component.scss']
})
export class MultiDropdownComponent implements OnInit {
#Input() dropdownItems: any[];
#Input() selectedItems;
#Input() header: string;
#Input() placeholder: string;
#Input() dropdownSettings: IDropdownSettings;
#Input() loading;
#Output() itemSelectEvent = new EventEmitter();
#Output() itemDeselectEvent = new EventEmitter();
#Output() selectAllEvent = new EventEmitter();
#Output() deselectAllEvent = new EventEmitter();
#Output() selectedItemsChange = new EventEmitter();
constructor() { }
ngOnInit(): void {
}
onSelectItem(event) {
this.selectedItemsChange.emit(this.selectedItems);
}
onDeselectItem(event) {
this.selectedItemsChange.emit(this.selectedItems);
}
onSelectAll(event) {
this.selectedItemsChange.emit(this.selectedItems);
}
onDeselectAll(event) {
this.selectedItemsChange.emit(this.selectedItems);
}
}
in child.component.html:
<div class="multi-dropdown-item">
<div class="multi-dropdown-header">{{header}}</div>
<div *ngIf="!this.loading" class="multi-dropdown-body">
<ng-multiselect-dropdown
[placeholder]="placeholder"
[data]="dropdownItems"
[(ngModel)]="selectedItems"
[settings]="dropdownSettings"
(onSelect)="onSelectItem($event)"
(onDeSelect)="onDeselectItem($event)"
(onSelectAll)="onSelectAll($event)"
(onDeSelectAll)="onDeselectAll($event)">
</ng-multiselect-dropdown>
</div>
</div>
Then in parent.component.html:
<app-multi-dropdown
[loading]="filterPropertiesMap.get(filterEntry).loading"
[dropdownItems]="filterPropertiesMap.get(filterEntry).items"
[(selectedItems)]="filterPropertiesMap.get(filterEntry).selectedItems"
[dropdownSettings]="filterPropertiesMap.get(filterEntry).setting"
[placeholder]="filterPropertiesMap.get(filterEntry).name"
[header]="filterPropertiesMap.get(filterEntry).name"
(itemSelectEvent)="onItemSelect($event)"
(itemDeselectEvent)="onItemDeselect($event)"
(selectAllEvent)="onSelectAll($event)"
(deselectAllEvent)="onDeselectAll($event)"
></app-multi-dropdown>
in parent.component.ts I didn't do anything but log:
onItemSelect($event) {
console.log("onItemSelect");
}
onItemDeselect($event) {
console.log("onItemDeselect");
}
onSelectAll($event) {
console.log("onSelectAll");
}
onDeselectAll($event) {
console.log("onDeselectAll");
}
in above code filterPropertiesMap defines settings.
You may see that what I am doing is in child component, I created eventemitters for select, deselect, and in the function I emitt this.selectedItems.
But I don't think this is a good way to implement this, and actually, it doesn't work well.
sometimes, when I select or deselect, it doesn't changed immediately.
So how to implement this? when I select deselect, selectAll, deselectAll. my parent component can react immediately and correctly.
Also the weird thing is: when I load the page, I will have some default values chose, for example 6,7,8,9. Then I select all and it still 6,7,8,9. and then after that I deselect all agin select all, the field will change to all(for example 1,2,3,4,5,6,7,8,9). Does event emitter has delay or will ignore some choices??
Edit:
I tried to extract all the necessary snippets of code to build a project here:
https://drive.google.com/file/d/1BlV2EtdwZhqqpkdiC0_mlaw_r3w6Bder/view?usp=sharing
I hope when I all the event(select, deselect, selectAll, deselectAll) can be emitted and received by parent component
sorry one mistake: the app-multi-dropdown tag in parent component should be app-child
You can use it as part of an Angular reactive form, and just emit every time it value changes.
The HTML of the child component could be something like this:
<form [formGroup]="myForm">
<ng-multiselect-dropdown
name="dropdownItems"
[settings]="dropdownSettings"
[placeholder]="placeholder"
[data]="dropdownItems"
formControlName="dropdownItems">
</ng-multiselect-dropdown>
</form>
And the .ts file:
import { IDropdownSettings } from 'ng-multiselect-dropdown';
import { FormGroup, FormBuilder } from '#angular/forms';
import { Component, OnInit, Input, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.scss']
})
export class ChildComponent implements OnInit {
#Input() placeholder: string;
#Input() defaultValues: any[];
#Input() dropdownItems: any[];
#Input() dropdownSettings: IDropdownSettings;
#Output() onDropdownChange = new EventEmitter();
myForm: FormGroup;
constructor(
private _fb: FormBuilder
) { }
ngOnInit(): void {
this.myForm = this._fb.group({
dropdownItems: ['']
});
// Set default values as real values
this.onDropdownChange.emit(this.defaultValues);
this.myForm.valueChanges.subscribe(val => {
this.onDropdownChange.emit(val.dropdownItems)
})
}
}
EDIT: Based on your last update:
With the code you updated and my peace of code, I build this functional project running on stackblitz: https://stackblitz.com/edit/angular-ivy-g7w3dz
Hope I got the logic properly
you haven't set up your emitter properly. do this:
#Output()selectAllEvent = new EventEmitter<any>();
onSelectAll(event) {
this.selectAllEvent.emit(this.selectedItems);
}
then on the parent:
<app-multi-dropdown
(selectAllEvent)="onSelectAll($event)"
></app-multi-dropdown>
In my code, I have a component that holds all the data retrieved from firebase. Including a variable called "currentTurnName" which gets passed from the parent to the child component. That variable is used to render a twitch channel inside the child component but doesn't update when the data changes.. I followed a few stack overflow guide that talk about using the *ngIf directive like <childComponent *ngIf"currentTurnName"> to stop the child component from loading until the data is retrieved- it works on page load, except it doesn't work when that variable is passed in new data asynchronously.
Child Component
export class TwitchVideoComponent implements OnInit {
constructor() {
}
player: any;
#Input() currentTurnName: any;
ngOnInit(): void {
var options = {
width: 1080,
height: 720,
channel: this.currentTurnName,
};
this.player = new Twitch.Player("<player div ID>", options)
this.player.setVolume(0.5);
console.log(this.currentTurnName);
}
// ngOnChanges(changes: SimpleChanges) {
// if (this.currentTurnName) {
// this.player.setChannel(this.currentTurnName);
// }
// }
}
Child Template
<div id="<player div ID>" class="twitch"></div>
Parent component
export class GameRoomComponent implements OnInit {
public users$: Observable<User[]>
currentUser: Observable<CurrentUser[]>;
constructor(private usersService: UsersService, private afs: AngularFirestore) {
this.currentTurn = afs.collection<CurrentUser>('currentPlayerDB').valueChanges().pipe(
tap(res => {
this.currentTurnName = res[0].user;
console.log(this.currentTurnName);
})
).subscribe();
}
currentTurn: any;
items: any;
currentTurnName : any;
Parent Template
<app-twitch-video *ngIf="currentTurnName"
[currentTurnName]="currentTurnName" >
This somewhat talks about my issue, but mine is more complex considering the data is changed
Angular 2 child component loads before data passed with #input()
You could use Angular's property setters. The documentation can be found here in this section (Intercept input property changes with a setter): https://angular.io/guide/component-interaction
Here is a snippet using your code:
export class TwitchVideoComponent implements OnInit {
constructor() {}
player: any;
#Input() set currentTurnName(name) {
this._currentTurnName = name;
console.log('This should fire every time this Input updates', this._currentTurnName);
if (this._currentTurnName) {
this.player.setChannel(this._currentTurnName);
}
}
public _currentTurnName: any;
ngOnInit(): void {
var options = {
width: 1080,
height: 720,
channel: this._currentTurnName,
};
this.player = new Twitch.Player("<player div ID>", options)
this.player.setVolume(0.5);
console.log(this._currentTurnName);
}
}
I know are several answers to questions related to this error on SO. I've tried several of them to no avail, so I figured if I provide some context-specific detail, someone may have insight to this problem in specific.
I have a parent and child component which need to both be communicating to/from one another. This components are both fairly robust, so I'll try to keep code examples as concise as possible.
User form component - Parent Component
#Component({
selector: 'hl2-user-form',
templateUrl: './user-form.component.html',
styleUrls: ['./user-form.component.scss']
})
export class UserFormComponent extends BaseComponentService implements OnInit, OnDestroy {
#Input() model: PanelModel;
// use to track changes in assigned user companies in grid
userCompanies: UserCompanyModel[] = [];
companies: CompanyModel[] = [];
userForm: FormGroup;
confirmation: IConfirmationModel;
statusEnum = StatusEnum;
systemParameterMessage: string = '';
rowData: Array<any> = [];
selectedDepartmentsAssigned: IUserFormDepartmentsAssignedGridViewModel[] = [];
selectedUserRolesAssigned: IUserRoleAssignedViewModel[] = [];
gridOptions: GridOptions = <GridOptions>{};
gridData: BehaviorSubject<any> = new BehaviorSubject(null);
isEdit: BehaviorSubject<boolean> = new BehaviorSubject(null);
defaultCompanyOptions: SelectItem[] = [];
userTypeOptions: SelectItem[] = [];
defaultApplicationOptions: SelectItem[] = [];
settingsForOptions: SelectItem[] = [];
settingsForDropdownSelectedCompanyId: number = null;
emergencyRoleIdSelected: number = null;
isChildComponentReadyForLoad: boolean = false;
constructor(route: ActivatedRoute,
public userService: UserService,
public cdRef: ChangeDetectorRef,
private formBuilder: FormBuilder,
private helpUrlService: HelpUrlService,
private adminPanelService: AdminPanelService,
private panelService: PanelService,
public adminConfirmationService: AdminConfirmationService,
private messageTranslatorService: MessageTranslatorService,
private companyService: CompanyService,
private gridStateService: GridStateService,
private applicationService: ApplicationService,
private systemParameterService: SystemParameterService) {
super(route);
}
User form component template/mark-up (relevance)-- Parent Component
<!--far much more parent-component (user-form) mark-up above this line-->
<button pButton class="ui-button-info" label="{{'Lang.Admin.Help' | translate}}"
(click)="onHelpClicked()"></button>
<button pButton label="test button" (click)="test()">TEST BUTTON</button>
</div>
</div>
<hr/>
<hl2-company-specific-security-settings *ngIf="isEdit.value && isChildComponentReadyForLoad" [companies]="companies" [confirmation]="confirmation"
[defaultCompanyOptions]="defaultCompanyOptions"
[isEdit]="isEdit" [userCompanies]="userCompanies"
[settingsForOptions]="settingsForOptions"
[settingsForDropdownSelectedCompanyId]="settingsForDropdownSelectedCompanyId"
(selectedDepartmentsAssigned)="onDepartmentsSelected($event)"
(selectedUserRolesAssigned)="onUserRolesSelected($event)"
(selectedEmergencyRoleId)="onEmergencyRoleIdSelected($event)">
</hl2-company-specific-security-settings>
Company specific security settings component -- Child Component
#Component({
selector: 'hl2-company-specific-security-settings',
templateUrl: './company-specific-security-settings.component.html',
styleUrls: ['./company-specific-security-settings.component.scss']
})
export class CompanySpecificSecuritySettingsComponent extends BaseComponentService implements OnInit, OnDestroy,
OnChanges {
#Input() userCompanies: UserCompanyModel[] = [];
#Input() confirmation: ConfirmationModel = null;
#Input() defaultCompanyOptions: SelectItem[] = [];
#Input() isEdit: BehaviorSubject<boolean> = new BehaviorSubject(null);
#Input() companies: CompanyModel[] = [];
#Input() settingsForOptions: SelectItem[] = [];
#Input() settingsForDropdownSelectedCompanyId: number = null;
#Output() selectedDepartmentsAssigned: EventEmitter<IUserFormDepartmentsAssignedGridViewModel[]>
= new EventEmitter<IUserFormDepartmentsAssignedGridViewModel[]>();
#Output() selectedUserRolesAssigned: EventEmitter<IUserRoleAssignedViewModel[]>
= new EventEmitter<IUserRoleAssignedViewModel[]>();
#Output() selectedEmergencyRoleId: EventEmitter<number> = new EventEmitter<number>();
emergencyRoleOptions: SelectItem[] = [];
emergencyRoleIdSelected: number = null;
departmentAssignedRowData: Array<IUserFormDepartmentsAssignedGridViewModel> = [];
userRoleAssignedRowData: Array<IUserRoleAssignedViewModel> = [];
initialDepartmentsAssignedRows: Array<IUserFormDepartmentsAssignedGridViewModel> = [];
initialUserRolesAssignedRows: Array<IUserRoleAssignedViewModel> = [];
departmentAssignedGridOptions = <GridOptions>{
onRowSelected: () => {
this.emitSelectedDepartments();
}
};
userRoleAssignedGridOptions = <GridOptions>{
onRowSelected: () => {
this.emitSelectedRoles();
}
};
constructor(route: ActivatedRoute,
public userService: UserService,
public cdRef: ChangeDetectorRef,
public adminConfirmationService: AdminConfirmationService,
private roleService: RoleService,
private departmentService: DepartmentService) {
super(route);
}
Company specific security settings template/mark-up (relevance) -- Child Component
<div class="flex-component">
<h4 class="center-header">{{'Lang.Admin.CompanySpecificSettings' | translate}}</h4>
<h5>{{'Lang.Admin.SecuritySettings' | translate}}</h5>
<div class="company-specific-dropdowns">
<label>{{'Lang.Admin.SettingsFor' | translate}}</label>
<p-dropdown [options]="settingsForOptions" [autoWidth]="false" (onChange)="onSettingsForSelectionChanged()"
[(ngModel)]="settingsForDropdownSelectedCompanyId" [ngModelOptions]="{standalone: true}"></p-dropdown>
<label>{{'Lang.Admin.EmergencyAccessRole' | translate}}</label>
<p-dropdown [options]="emergencyRoleOptions" [autoWidth]="false" [(ngModel)]="emergencyRoleIdSelected"
(onChange)="setEmergencyRoleId()"
[ngModelOptions]="{standalone: true}"></p-dropdown>
</div>
As you may see, I'm communicating back to the parent using #Output() props. The error occurs when I change the dropdown selection for settingsForDropdownSelectedCompanyId, but not all the time.
Certainly happy to provide more code if its relevant, but I hope its not too dense to read through to get enough context as is. TIA for anyone who may be able to help.
This error usually tells you which variable has changed. Can you provide what that is? Typically it says something like: "Expression has changed..... somevar..previous value: false new value: true. A work around would be:
<hl2-company-specific-security-settings
[hidden]="!isEdit.value || !isChildComponentReadyForLoad"
[companies]="companies" [confirmation]="confirmation"
I have an app.component.ts component which template contains several tab components, each one containing a particular link:
app.component.html:
<h1>Tabs container</h1>
<div>
<nav>
<tab *ngFor="let tab of tabList" [name]="tab.name" [link]="tab.link" (eventEmitter)="closeTabEvent($event)"></tab>
</nav>
</div>
<div>
<router-outlet></router-outlet>
</div>
The HTML template of the tab component is tab.component.html:
<a [routerLink]='link' routerLinkActive="router-link-active">{{name}}</a>
<button *ngIf="tabIndex > 0" (click)="closeTab('tabToCloseKey')">Close tab</button>
I'm looking for a way to get a boolean value inside my TabComponent class, indicating which route is active.
Here is my TabComponent class:
export class TabComponent implements OnInit {
#Input() name: string;
#Input() link: string;
#Input() param: string;
targetArray: Array<any>;
#Output() eventEmitter = new EventEmitter<[number, boolean]>();
// #Output() isActiveEmitter = new EventEmitter<boolean>();
static lastTabIndex: number = 0;
tabIndex: number = 0;
isActive: boolean = true;
// #ViewChild('routerLinkActive') a;
// el: HTMLElement;
// className: string;
constructor(private router: Router, private sharedService: SharedService, private _elRef: ElementRef) {}
ngOnInit() {
// this.className = this._elRef.nativeElement.find('a').className;
// this.className = this.el.className;
this.tabIndex = TabComponent.lastTabIndex;
// console.log(this.className);
TabComponent.lastTabIndex++;
}
closeTab(){
console.log('tab closed at index: ' + this.tabIndex);
let tuple: [number, boolean];
tuple = [this.tabIndex, this.isActive];
this.eventEmitter.emit(tuple);
this.hideTabComponent();
}
hideTabComponent(){
console.log('Hiding component');
}
}
I would to store the information indicating if the route corresponding to the tab is active or not in the isActive boolean attribute of this typescript class.
You can use route param in the routerLink in the tab link so that you can catch that value in the Class with ActivatedRoute as mentioned in the Angular doc: