1

Using Angular 6 here

I wanted some inputs regarding designing of one of my UI component. So basically the current functionally we had already designed in AngularJs but in this new app which is using Angular 6 I wanted to know if there are better ways to do so.

Below is the UI flow I am looking to design.

User fills up a form at the top basically some text boxes and dropdowns.

After the above selection I show up a HTML table having 3 static columns and few buttons for ex as below:

ID Type Env +Add Column -Delete Row +Add Row

Above Id, Type and Env are 3 static columns which are always there. Now if the user wants to add more column (dynamic) he can click the Add Column button, where user can enter their own specific name to the column. There should also be functionally of deleting the dynamic columns. Once user is done adding columns he can click on Add Row which would create a dynamic row and user can then enter data to the table. Once user adds the row, user may click on Delete Row button to delete that row.

After user is done adding columns and rows to the table there is a submit button at the end which would send the above in Json format to my API which would then save the entire form.

FYI I already have a working versions of this in angularjs where I am using contenteditable against each row something as below:

<tr ng-repeat="r in targetTable.rows">
 <td class="unique-id">{{r.id}}</td>
 <td contenteditable="true" ng-repeat="column in targetTable.columns" ng-model="r[column.id]" ng-blur="!r.id? addNewRow(r[column.id], r): undefined"></td>
 <td class="blank" colspan="2"></td>
</tr>

See the demo here:

https://codepen.io/anon/pen/QXwjwM

What I need inputs is to how to design this html table with all the functionally of adding/deleting dynamic rows and columns in Angular 6. Is there some open source available or if anyone can help me to some examples. Or if I need to create all this manually. in similar way as I did in AngularJs.

Thanks

Anyone with inputs?

Brenda
  • 510
  • 3
  • 13
  • 36
  • Can you post what have you tried so far in a stackblitz demo. – Munim Munna Jun 17 '19 at 16:41
  • @MunimMunna We are migrating from angularjs to angular6. I have done similar thing in angularjs as shared in link above. Now I want to do the same thing in angular 6 but wanted to know if there is a better way examples etc. As in my angularjs i used contenteditable on each row which made my table slow when large data was entered or uploaded. – Brenda Jun 17 '19 at 17:03
  • Obviously using input boxes will give you performance gain over it. I suggest you attempt to build it using angular 6, we can help if you get stuck anywhere. – Munim Munna Jun 17 '19 at 17:36
  • @MunimMunna does angular6 has contenteditable as I am using above in angularjs – Brenda Jun 18 '19 at 11:48
  • @karen, take a look this great article from Netanet Basal https://netbasal.com/keeping-it-simple-implementing-edit-in-place-in-angular-4fd92c4dfc70 – Eliseo Jun 18 '19 at 18:33
  • @karen if you only want show as input, take a look, e.g. https://stackoverflow.com/questions/56566003/array-of-formgroup-within-mat-table-for-each-row/56571113#56571113 using mat-table, else it's only using `` as show briefly at end of my answer. If you want to move between "inputs", you can use some like https://stackblitz.com/edit/angular-x5qn69?file=src%2Fapp%2Farrow-div.directive.ts – Eliseo Jun 19 '19 at 07:02

2 Answers2

7

Joining my two comments, I created this stackblitz

I used material table because I'm lazy to formated a table. As commented, the only thing we need to use a mat-table is put as dataSource the controls of a Form Array

dataSource = this.myformArray.controls;

The columns of the table becomes like

<ng-container matColumnDef="surname">
    <th mat-header-cell *matHeaderCellDef> Surname </th>
        <td mat-cell *matCellDef="let element">
       <input arrow-div [formControl]="element.get('surname')">
       </td>
  </ng-container>

Yes, simple using [formControl]=element.get('nameOfField')

The funny work is make that arrrows keys work to move between "cells". I use a directive. But as I hate create a directive with @Output() I use a auxiliar service.

If we not use a service, our .html looks like

<input arrow-div [formControl]="element.get('id')" (arrowEvent)="move($event)">
<input arrow-div [formControl]="element.get('name')" (arrowEvent)="move($event)">
<input arrow-div [formControl]="element.get('surname')" (arrowEvent)="move($event)">
  ...

If we used a service our html become more transparent

<input arrow-div [formControl]="element.get('id')" >
<input arrow-div [formControl]="element.get('name')" >
<input arrow-div [formControl]="element.get('surname')" >
...

And in the app we subscribe to the service.

The service is simple

export class KeyBoardService {
  keyBoard:Subject<any>=new Subject<any>();
  sendMessage(message:any)
  {
    this.keyBoard.next(message)
  }
}

just a Subject and a method to send the value to subject.

The directive only listen if a arrow key is down and send the key sender. Well, I send a object of type {element:...,acction:..} to send more information.

export class ArrowDivDirective {
  constructor( private keyboardService:KeyBoardService,public element:ElementRef){}

  //@Output() arrowEvent:EventEmitter<any>=new EventEmitter();
   

  @HostListener('keydown', ['$event']) onKeyUp(e) {
    switch (e.keyCode)
    {
      case 38:
        this.keyboardService.sendMessage({element:this.element,action:'UP'})
        break;
      case 37:
        if (this.element.nativeElement.selectionStart<=0)
        {
        this.keyboardService.sendMessage({element:this.element,action:'LEFT'})
        e.preventDefault();
        }
        break;
      case 40:
        this.keyboardService.sendMessage({element:this.element,action:'DOWN'})
        break;
      case 39:
        if (this.element.nativeElement.selectionStart>=this.element.nativeElement.value.length)
        {
        this.keyboardService.sendMessage({element:this.element,action:'RIGTH'})
        e.preventDefault();
        }
        break;
    }
  }
}

Well, I take account when you're at first or at init of the input to send or not the key when we click lfet and right arrow.

The app.component only has to subscribe to the service and use ViewChildren to store all the inputs. be carefully! the order of the viewchildren in a mat-table goes from top to down and to left to rigth

@ViewChildren(ArrowDivDirective) inputs:QueryList<ArrowDivDirective>

  constructor(private keyboardService:KeyBoardService){}
  ngOnInit()
  {
    this.keyboardService.keyBoard.subscribe(res=>{
      this.move(res)
    })
  }
  move(object)
  {
    const inputToArray=this.inputs.toArray()
    const rows=this.dataSource.length
    const cols=this.displayedColumns.length
    let index=inputToArray.findIndex(x=>x.element===object.element)
    switch (object.action)
    {
      case "UP":
        index--;
        break;
      case "DOWN":
        index++;
        break;
      case "LEFT":
        if (index-rows>=0)
          index-=rows;
        else
        {
          let rowActual=index%rows;
          if (rowActual>0)
            index=(rowActual-1)+(cols-1)*rows;
        }
        break;
      case "RIGTH":
      console.log(index+rows,inputToArray.length)
        if (index+rows<inputToArray.length)
          index+=rows;
        else
        {
          let rowActual=index%rows;
          if (rowActual<rows-1)
            index=(rowActual+1);

        }
        break;
    }
    if (index>=0 && index<this.inputs.length)
    {
      inputToArray[index].element.nativeElement.focus();
    }
  }

*UPDATE If we want to add dinamically columns add new two variables (plus the "displayedColumns"

displayedColumns: string[] = ['name','surname','delete'];
displayedHead:string[]=['Name','Surname']
displayedFields:string[] = ['name','surname'];

And our table becomes like

<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
  <!-- All columns -->
   <ng-container *ngFor="let col of displayedFields;let i=index" [matColumnDef]="col">
      <th mat-header-cell *matHeaderCellDef> {{displayedHead[i]}} </th>
      <td mat-cell *matCellDef="let element">
        <input arrow-div [formControl]="element.get(col)">
      </td>
    </ng-container>
    <!---column delete-->
  <ng-container matColumnDef="delete">
    <th mat-header-cell *matHeaderCellDef></th>
    <td mat-cell *matCellDef="let element;let i=index;">
        <button arrow-div mat-button (click)="delete(i)">delete</button>
    </td>
  </ng-container>

  
  <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
  <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

A new function to add a column must add a FormControl to each FormGroup of the array, actualize the variables displayedColumns,displayedHead and displayedFields

addColumn()
  {
    let newField="Column"+(this.displayedFields.length+1)

    this.myformArray.controls.forEach((group:FormGroup)=>{
      group.addControl(newField,new FormControl())
    })
    this.displayedHead.push(newField)
    this.dataSource = [...this.myformArray.controls];
    this.displayedFields.push(newField);
    this.displayedColumns=[...this.displayedFields,"delete"];
  }

In this another stackblitz I add this functionality (also how delete a row and how create a new row)

Update 2 answer to how not "hardcode" the formArray really it's all inside. If we imagine that we has an array like

ELEMENT_DATA: any[] = [ { name: '1', surname: 'one' }, { name: '2', surname: 'two' }, { name: '3', surname: 'three' }, ]; )

We need use the array to give values to displayedHead, displayedFields and displayedColumns:

displayedHead:string[]=Object.keys(this.ELEMENT_DATA[0]).map(x=>x.substring(0,1).toUpperCase()+x.substring(1))
  displayedFields:string[] = Object.keys(this.ELEMENT_DATA[0]);
  displayedColumns:string[]=[...this.displayedFields,'delete']

To initialize the FormArray we are going to improve the function "add" to allow pass as argument an object to give value to the form

  add(data:any=null)
  {
    const newGroup=new FormGroup({});
    this.displayedFields.forEach(x=>{
      //see that if data!=null we create the FormControl with the value
      //of data[x]
      newGroup.addControl(x,new FormControl(data?data[x]:null))
    })
    this.myformArray.push(newGroup)

    this.dataSource = [...this.myformArray.controls];
  }

At least create a function initArray

  initArray(elements:any[]){
    elements.forEach(x=>{
      this.add(x);
    })
  }

And call it in ngOnInit

this.init(this.ELEMENT_DATA)

Well, if we don't has the array in a variable -usually we get the value form a service-, we need put all this in the subscribe function to the service

this.myserviceData.getData().subscribe(res=>{
displayedHead:string[]=Object.keys(res[0]).map(x=>x.substring(0,1).toUpperCase()+x.substring(1))
      displayedFields:string[] = Object.keys(res);
      displayedColumns:string[]=[...this.displayedFields,'delete']
      this.initArray(res)
})

Update allow user to change name of the columns

In a table, the "head" of each columns can be anything. So we can defined one variable

  columnSelect = -1;

And we are create a more complex header

  <th mat-header-cell *matHeaderCellDef>
     <span [style.display]="columnSelect!=i?'inline':'none'" (click)="selectColumn(i,columnName)">{{displayedHead[i]}} </span>
     <input #columnName [style.display]="columnSelect==i?'inline':'none'" [ngModel]="displayedHead[i]" (blur)="changeColumnName(i,columnName.value)"/>
     </th>

See that the header or is an input or is an span (if the "selectColumn" is equal to the column. remember that the columns are numerated from 0 -this is the reason because if selectColumn=-1 there're no column selected.

We use a template reference variable "#columnName" to pass the value to the function (blur) and when (click) the span. this allow us create two functions

  selectColumn(index: number, inputField: any) {
    this.columnSelect = index; //give value to columnSelect
    setTimeout(() => {          //in a setTimeout we make a "focus" to the input
      inputField.focus();
    });
  }

It's neccesary make the focus inside a setTimeout to allow Angular repaint the header and then make the focus. This is the reason also we use [style.display] and not *ngIf. (if we use *ngIf, the value "inputField" was null)

The function to change the name is a bit more complex

  changeColumnName(index, columnTitle) {
    const oldName = this.displayedFields[index];
    const columnName = columnTitle.replace(/ /g, "").toLowerCase();
    if (columnName != oldName) {
      this.myformArray.controls.forEach((group:FormGroup)=>{
          group.addControl(columnName,new FormControl(group.value[oldName]))
          group.removeControl(oldName)
      })
      this.displayedHead.splice(index, 1, columnTitle);
      this.displayedColumns.splice(index, 1, columnName);
      this.displayedFields.splice(index, 1, columnName);
    }
    this.columnSelect = -1;
  }

Basicaly we add a new formControl to the array and remove the older. I choose that the name of the "field" was the Title to lower case after remove all the spaces.

The new stackblitz

Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • Thanks eliseo for this. While I have just checked the demo and not read all the code. There are two things. I need to add dynamic columns as well as per my post together with 2 static columns .So if we go by your example Name and Surname would always be there and there should be a button to add more columns as well. Could you design a way for that. Thanks. Another thing if the Angular material that you have used, I hope that is open source? – Brenda Jun 19 '19 at 12:50
  • @Karen, I updated my answer and put a new stackblitz. Really I use material because really I'm lazy to formated a table. But it's not necesary (and if you are not planning using Angular-material, perhafs it's better not include in your aplication), you can do the same using simple tables in HTML. The idea is create a formArray and iterate oveer formArray.controls and use [formControl] -see my answer and the end of https://stackoverflow.com/questions/56566003/array-of-formgroup-within-mat-table-for-each-row/56571113#56571113 – Eliseo Jun 19 '19 at 13:57
  • @karen, without material table, https://stackblitz.com/edit/angular-wmfjhh-vkeegy?file=app%2Ftable-basic-example.ts – Eliseo Jun 19 '19 at 14:36
  • Thanks for the detailed Eliseo. Though I have not integrated it as yet into my project but this was what I was looking for. I will reach out to you on this post or post new query in future if I have issues. – Brenda Jun 21 '19 at 13:00
  • @Eliseo, Instead of adding the FormControl hardcoded, how can I add from a dynamic array (Current way is using hard code properties --> myformArray = new FormArray([ new FormGroup({ name: new FormControl("uno"), surname: new FormControl("one") }),..... ) ( From an dynamic array--> const ELEMENT_DATA: any[] = [ { name: '1', surname: 'one' }, { name: '2', surname: 'two' }, { name: '3', surname: 'three' }, ]; ) – SRAMPI Mar 16 '21 at 00:05
  • @SA., I update the answer indicate how you can not hard code the formArray, I hope this help you – Eliseo Mar 16 '21 at 09:31
  • @Eliseo, it works great. Thank you. What will be the best way to make some of the initial columns not editable? – SRAMPI Mar 16 '21 at 11:25
  • @Eliseo I am able to disable it ---> newGroup.addControl(x, new FormControl({ value: data ? data[x] : null, disabled: true }, null)); – SRAMPI Mar 16 '21 at 12:15
  • @Eliseo, what is the best way to add an icon for each column and allow the user to rename (dynamically added column too) in the table header itself – SRAMPI Mar 25 '21 at 13:22
  • @SA. I update again the answer to allow rename the columns. About add an icon, remember that the header can be anything, e.g. `displayedHead[i]+icono` – Eliseo Mar 25 '21 at 20:29
  • @Eliseo, Looks great. Thank you – SRAMPI Mar 29 '21 at 12:15
0

There is an ui-grid third party library which gives lot of features. You can refer the link

If you can upgrade as mentioned here demo

shikhar
  • 110
  • 10