Complex Tables in Angular (How to create dynamic) - arrays

In my current project, we have to show complex tables like you see here: https://medium.muz.li/complex-tables-356826d11861 (or see image).
The cells are very dynamic, and all information coming from API. We have about 25 different tables, with the same features and behavior. The solution right now is a component for each table. I found this to be no got approach, as this is not very dynamic and chances are, the codebase of the component could differentiate and so we will have difficulties adding new features, changes, and solving bugs overall tables.
As I understand to solve this in terms of UI I was thinking about the best approach, how the basic building should look like. Like we could create an Array for each table which holds the row, which holds the cells as objects and these objects hold all information about each cell (editable, style, content type, tooltip...).
Advantages: Dynamic, one codebase for all (upcoming) tables, faster implementation of features and changes, ease of manipulating the table (filter, search, sorting, remove/add rows).
What would you suggest?

Your approach is totally fine and surely the way to go.
If you want to go a bit further, you can create components for each type of cells :
TextCell, TextfieldCell, SelectCell, etc ...
and even further have your components loaded dynamically
have a look at ComponentFactoryResolver
then in your data cell Object, add a field with the name of the component.
It's not the easiest to do though but the benefits will be here in term of maintainability

Expanding on Jeremy's answer, you could implement
import { Component } from "#angular/core";
export class Table {
constructor(
public title: string,
public columns: string[],
public rows: Row[]
) {}
}
export class Row {
constructor(public cells: Cell[]) {}
}
export type CellValue = string | number | boolean;
export class Cell {
constructor(public value: CellValue) {}
isText(): boolean {
return typeof this.value === "string";
}
isNumber(): boolean {
return typeof this.value === "number";
}
isBoolean(): boolean {
return typeof this.value === "boolean";
}
}
#Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
title = "CodeSandbox";
table: Table;
constructor() {
this.table = new Table(
"Table name",
["Country", "State", "Population", "Northern"],
[
new Row([new Cell("AR"), new Cell("BSAS"), new Cell(44490000), new Cell(false)]),
new Row([new Cell("US"), new Cell("CA"), new Cell(328000000), new Cell(true)])
]
);
}
}
and have the template be something like the following:
<table>
<tr>
<th *ngFor="let col of table.columns">{{col}}</th>
</tr>
<tr *ngFor="let row of table.rows">
<td *ngFor="let cell of row.cells">
<input type="text" *ngIf="cell.isText()" [(ngModel)]="cell.value" />
<input type="number" *ngIf="cell.isNumber()" [(ngModel)]="cell.value" />
<input
type="checkbox"
*ngIf="cell.isBoolean()"
[(ngModel)]="cell.value"
/>
</td>
</tr>
</table>
These values will be automatically filled as you write the input because of the [(ngModel)]. Hope it helps you. I created a sample here in case you want this code.

Related

How to exclude certain keys/values when iterating through array and displaying on table in Angular

Have just run into a bit of a roadblock and any advice would be much appreciated
I am currently building out an application in angular to fetch data from an API and display it on the page
I have a service called "Api Service using the HTTPClient to call the API
apiservice.service.ts
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class ApiserviceService {
constructor(private _http:HttpClient) { }
getData(){
return this._http.get('APIURL');
}
}
Then I have another component to observe this call and map the data to an array
data.component.ts
import { Component, OnInit } from '#angular/core';
import { map, toArray } from 'rxjs';
import { ApiserviceService } from '../apiservice.service';
#Component({
selector: 'app-data',
templateUrl: './data.component.html',
styleUrls: ['./data.component.scss']
})
export class VenueComponent implements OnInit {
data: any = [];
constructor(private service:ApiserviceService) {}
MapData() {
this.service.getData().subscribe(result=>{
this.data=result;
console.log(this.data);
})
}
ngOnInit() {
this.MapData()
}
}
I am then displaying the data in a table
data.component.html
<table>
<tr *ngFor= "let item of data | keyvalue" >
<td>{{item.key}} : {{item.value}}</td>
</table>
Now at the moment I am iterating thrugh the array using *ngFor but there are keys/values that I want to exclude from being displayed
For example I want to display key2 and value 2 but not key 1 and value 1
[Key1 : value1], [Key2 :value2]
How would I go about doing this? Any advice would be much appreciated
in my opinion you have at least 2 options, filter the observable:
this.service.getData().pipe(
filter(o => {
return // your condition here
})
).subscribe(result=>{
the second option is to write a custom pipe. You can create a pipe that extend the keyvalue pipe and filters the data as you need. You will find numerous tutorial where they explain how to create a pipe.
I think you can use Angular Slice Pipe[1]
<table>
<tr *ngFor= "let item of data | keyvalue | slice: 1" >
<td>{{item.key}} : {{item.value}}</td>
</table>
Hope this help, Thanks!
[1]: https://angular.io/api/common/SlicePipe
First of all: use types for your variables. It gives you the benefit of knowing exactly which kind of data you are handling and it helps us to understand what exactly you want to achieve.
Here is my take on this: I suggest you to put this kind of logic into your service, so that this logic can be reused somewhere else.
I also suggest you to use some kind of reactive approach, so that you don't have to manually unsubscribe your requests.
The following approach is based on the assumption that your API returns an array, not an object.
apiservice.service.ts
...
export class ApiserviceService {
data$ = this._http.get('APIURL').pipe(
map(data => data.filter(item => item.key !== 'Key1'))
);
constructor(private _http:HttpClient) { }
...
data.component.ts
...
public data$ = this.service.data$;
...
data.component.html
<table>
<tr *ngFor= "let item of data$ | async" >
<td>{{item.key}}</td>...
</table>
I suggest you to not use pipes in this case, since pipes should be generic and your filtering logic seems to be too specific for a pipe.

Angular 12 FormGroup dynamically Array checkboxes custom validator does not work

I am creating an Angular 12 app, with Material.
I have a form with an checkbox array loaded dynamically from database.
I need to validate that at least one checkbox is selected
I defined like this in my OnInit():
ngOnInit(): void {
this.form = this.fb.group({
Id: new FormControl(null),
Name: new FormControl('',Validators.required),
Recipents: new FormControl('',[Validators.required, matchingEmailValidator()]),
IsActive: new FormControl(true),
ProcessorName: new FormControl('',Validators.required),
Channel: new FormArray([]),
}, { validators: [customValidateArrayGroup()] }
);
}
I need a custom validation for channel form array. If I added it in the definition of the channel, it does not fire when I check it. So, I decided to do it at the form level..
I added:
{ validators: [customValidateArrayGroup()] }
Every time an object changes, it fires this validator.
This is my custom validator:
export function customValidateArrayGroup(): ValidatorFn {
return function validate(formGroup: FormGroup) {
let checked = 0
Object.keys(formGroup.controls).forEach(key => {
const control = formGroup.controls[key]
if (control.value) {
checked++
}
})
if (checked < 1) {
return {
requireCheckboxToBeChecked: true,
}
}
return null
}
}
Here is my Html where I defined the Checkbox Array
<mat-label><strong>Channel</strong></mat-label>
<li *ngFor="let chanel of notification.NotificationChannelLogLevels">
<mat-checkbox id= {{chanel.NotificationLogLevel.Id}} formArrayName="Channel"
[checked]="chanel.IsActive"
(change)="changeEventFunc($event)">
{{chanel.NotificationLogLevel.Name}}
</mat-checkbox>
</li>
The problem I have is that the custom validator does not fire when a checkbox is clicked. Maybe is becouse they are loaded dinamically and are not recognized by formGroup.controls
How can I validate this?
You have an odd mix of using formarray and your js array in the template. Currently your formarray is completely empty, so it would be expected that it does not run when checkboxes are checked. You can choose to iterate your JS array and push / remove to formarray, or then you push the values to the formarray when you receive the data and then just iterate that one in the template. The below solution does the latter:
Shortened code....
Build form:
this.form = this.fb.group({
Channel: this.fb.array([], [customValidateArrayGroup()]),
});
I attached the custom validator to the formarray itself. When you have the dynamic data ready, then iterate it and push form controls to your formarray. I like to use a getter as well. Push whatever properties you need, here I choose IsActive and Name only:
get channels() {
return this.form.get('Channel') as FormArray;
}
// when you have data accessible:
this.notificationChannelLogLevels.forEach(value => {
this.channels.push(this.fb.group({
isActive: value.IsActive,
name: value.Name
}))
})
Now iterate this formarray in the template:
<div formArrayName="Channel">
<li *ngFor="let chanel of channels.controls; let i = index">
<ng-container [formGroupName]="i">
<mat-checkbox formControlName="isActive">
{{ chanel.get('name').value}}
</mat-checkbox>
</ng-container>
</li>
<small *ngIf="channels.hasError('hasError') && channels.touched">
Choose at least one
</small>
</div>
The custom validator checks that at least one checkbox field has isActive as true:
export function customValidateArrayGroup() {
return function validate(formArr: AbstractControl): ValidationErrors | null {
const filtered = (formArr as FormArray).value.filter(chk => chk.isActive);
return filtered.length ? null : { hasError: true }
};
}
A STACKBLITZ for your reference.
I think you have your FormArray setup incorrectly in your template.
You are applying the formArrayName attribute the each checkbox when it needs to be applied to a parent container,
<div formArrayName="myFormArray">
<div *ngFor="*ngFor="let chanel of notification.NotificationChannelLogLevels; let i = index">
//Use the index here to dynamically tie each mat-checkbox to a FormControl
<mat-checkbox [FormControl]="myCheckboxes[i]"></mat-checkbox>
</div>
</div>
And then in your .ts file you'll have to define myCheckboxes as a FormArray with instances of form control inside it. Otherwise myCheckboxes[i] will be either null or an index out of bounds. You can use the form array you added to your form group, but the indexes you reference in the template have to be defined.
Here is a good blog post going over how to handle adding/removing instances from the form array,
https://netbasal.com/angular-reactive-forms-the-ultimate-guide-to-formarray-3adbe6b0b61a
And another,
https://blog.angular-university.io/angular-form-array/
As a side note, if your logging levels are static, it may just be easier or more intuitive to define the list of checkbox controls as a FormGroup and apply your validator to the form group.

error: Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays how to solve

Service code which return json data
export class EmployeeServiceService {
constructor(private _http:Http) { }
GetEmployees() :Observable<IEmployee[]>{
debugger;
return this._http.get("http://localhost:2724/api/employee/1")
.map((response:Response)=> <IEmployee[]>response.json())
}
}
In this component class im not able to convert the json data please any one help me to solve my issue
export class EmployeeListComponent implements OnInit {
Employees:IEmployee[];
constructor( private _EmployeeService: EmployeeServiceService) {
}
ngOnInit() {
this._EmployeeService
.GetEmployees().subscribe((employeeData)=> this.Employees = employeeData);
}
}
html
<tbody>
<tr *ngFor="let employee of Employees">
<td>{{employee.Address}}</td>
<td>{{employee.City}}</td>
<td>{{employee.EmployeeID}}</td>
<td>{{employee.FirstName}}</td>
<td>{{employee.LastName}}</td>
</tr>
<tr *ngIf="!Employees|| Employees.length== 0">
<td>No employee to display!!!</td>
</tr>
</tbody>
enter code here
This error occurs whenever you are trying to bind an Object with ngFor. ngFor directive supports only arrays.
As per the request, it seems it returns only one Object,
return this._http.get("http://localhost:2724/api/employee/1")
if you need to bind only one Object no need to use ngFor.
Also you need to initialize employees with an empty array.
Employees:IEmployee[] = [];
if you still want to bind one Object to the table, push the Object to the array as follows,
this.Employees.push(employeeData);
Two options
(i) Change your request to return all employees
return this._http.get("http://localhost:2724/api/employee/all")
(ii) Push the Object to an array as follows,
ngOnInit() {
this._EmployeeService
.GetEmployees().subscribe((employeeData)=> this.Employees.push(employeeData);
}

Angular losing binding after array update

I have a situation where i need to have a dynamic array, think like a booking form, where people are adding flights to the list but it could be 1 flight or 10 flights. i did up a dummy example where i have been able to replicate the issue, i'm hoping it's me and not an issue with the Angluar. anyway, here it is.
I start with an empty array with a button to add items, those items are bound with a *ngFor (code below) each of the fields below are in that array, the "Name" values i have populated 1-5 by just typing
I then Decide to delete number 3 which is successful
I then decide to add a new one, here is where everything goes wrong. as you can see below, it successfully adds the 5th item again, but the one that should have #5 in it, is now blank.
I then press "Create Array" which just dumps the array to console, and i see the below, the values are still in there, but not bound to the Input for that 1 item.
Ok, Now for the code:
This is my HTML Template file:
<form name="form" #f="ngForm">
Name: <input class="input" type="text" name="Name" [(ngModel)]="model.Name"
#Name="ngModel" />
Description: <input class="input" type="text" name="Description"
[(ngModel)]="model.Description" #Description="ngModel" />
<br>
<button (click)="addThought()">New Thought</button>
<div class="Thought" *ngFor="let Thought of myObject.Thoughts;let i=index">
Thought Name:<input name="Name-{{i}}" [(ngModel)]=Thought.Name
#Thought.Name="ngModel" type="Text" /><br>
Thought Description:<input name="Description-{{i}}"
[(ngModel)]=Thought.Description #Thought.Description="ngModel" type="Text"
/>
<br>
<br>
<button (click)="removeThought(Thought)">Remove Thought</button>
</div>
<button (click)="CreateThought()">Create Arrays</button>
</form>
and this is my component TS file:
export class CreateThoughtComponent implements OnInit {
model: any = {};
myObject: customObject = new customObject;
constructor(private guid: Guid, private staticData: StaticDataService) {
}
ngOnInit() {
}
CreateThought() {
console.log(this.myObject);
}
addThought() {
let thought: Thought = new Thought;
this.myObject.Thoughts.push(thought);
}
removeThought(t: Thought) {
this.myObject.Thoughts = this.myObject.Thoughts.filter(item => item !==
t);
}
}
And here is the declaration of the array within an object
export class customObject {
Name: string;
Description: string;
Thoughts: Thought[];
constructor() {
this.Thoughts = new Array<Thought>();
}
}
export class Thought {
Name: string;
Description: string;
}
Any help or suggestions would be greatly appreciated.
This is a tricky thing about Angular's change detection mechanism. You can solve your problem easily by creating a clone of your object. e.g.
addThought() {
let thought: Thought = new Thought;
this.myObject.Thoughts.push(thought);
// clone the object in order to force Angular to apply changes
this.myObject = JSON.parse(JSON.stringify(this.myObject));
}
I solved it by removing the name="Name-{{i}}" from the input's, and adding [ngModelOptions]="{standalone: true}" instead. at it seemed to be an issue with the dynamic way i was assigning the Name to the input using the "index"
I was able to solve it by randomly generating a guid for each name but that created a mire of other issues as well.
That being said, i have also tested DiabolicWord's solution above and it works, since it's so simple going to mark his as the answer.

Select the text inside an input using Typescript in Angular 2

I'm trying to do exactly what is described in this post, but in Angular2.
Basically use the javascript function .setSelectionRange(start, end); in an input after a user clicks on a trigger. I can't find any way to replicate this behaviour using Typescript.
Thanks in advance.
I can't find any way to replicate this behaviour using Typescript.
TypeScript is just JavaScript. I suspect you mean to say Angular2 (that post is Angular1).
Angular2
You need to get a hold of the dom element (which is what you seem to be struggling with). In your controller you need to inject ElementRef. E.g. #Inject(ElementRef) elementRef: ElementRef,
Once you have the element you can traverse it and do whatever dom access / manual manipulation you need to do.
More
Docs : https://angular.io/docs/js/latest/api/core/ElementRef-class.html
Example
Sample : https://stackoverflow.com/a/32709672/390330
import {Component, ElementRef} from 'angular2/core';
#Component({
selector:'display',
template:`
<input #myname (input) = "updateName(myname.value)"/>
<p> My name : {{myName}}</p>
`
})
class DisplayComponent implements OnInit {
constructor(public element: ElementRef) {
this.element.nativeElement // <- your direct element reference
}
ngOnInit() {
}
}
This example line of code shows the essence of selecting the text (with .name being an ElementRef reference):
this.name.nativeElement.setSelectionRange(0, 999);
Here are all the necessary pieces put together (as well as putting focus on the input) for a "name" field:
View:
<input name="name" formControlName="name" type="text" [(ngModel)]="data.name">
Component:
export class MyComponent {
#ViewChild('name') name: ElementRef; // * see security comment below for ElementRef
#Input() data: {name: 'Foo Baz'};
myForm: FormGroup;
constructor() {
this.myForm = new FormGroup({
name: new FormControl()
});
}
// call this to give the field focus and select its text
focusAndSelectNameFieldText(){
if (!this.name) return;
this.name.nativeElement.focus();
setTimeout(() => {
this.name.nativeElement.setSelectionRange(0, 999);
});
}
}
*Please be sure your use of ElementRef does not pose a security risk:
https://stackoverflow.com/a/44509202/442665

Resources