Create Mapper Component Using SVG In Angular 7

Introduction 

 
Recently, while working on a project, I came across this unique requirement where I needed to show the mapping between two HTML table records, simulating which record from the 1st table is mapped/linked to a record of the 2nd table. The functionality should allow the user to easily drag & drop row from one table over the table row & accordingly, the mapping should be created. After doing some R &D, I made up my mind to make use of SVG (Scalable Vector Graphics) to implement the mapping functionality between two HTML table records. When we talk about SVG, the first thing that comes to our mind is an image, but it’s more than that. You can do programming in it by dynamically creating SVG tags, elements, etc. You could even add animations to your SVG images. For more information on SVG, please self-explore more on it.
 
Enough of the theory. Let’s get on to our project requirements. The technology stack we’ll use is Angular 7 & SVG. I’ll use Angular CLI to create a new project with the name demo-app.
 
For this demo, we will try to create a mapping between customers & products. We could have any data for that matter, since our main aim from this article is to understand the logic behind creating the mapping between 2 HTML tables using SVG. I’ll be using JSON files for both datasets, which we’ll try to fetch using HTTP requests in order to simulate the HTTP calls in the real world. Below is the snapshot of what we’ll build in this demo.
 
 
 
Here is the architecture of our application, we’ve one shared module wherein we’ll keep our mapper component since it needs to be generic & reusable across the application. We also have a “modules” module which will store our application feature modules.
 

Application architecture

 
 
We’ll use PrimeNG turbotable to showing our data in tabular format. Here are the libraries that I’m using in my application.
 
 
  
After installing the required packages, I’ve added the below entry in angular.json file
  1. "node_modules/primeng/resources/themes/nova-light/theme.css",  
  2. "node_modules/font-awesome/css/font-awesome.min.css",  
  3. "node_modules/primeng/resources/primeng.min.css",  
  4. "src/styles.css"  
For listing PrimeNG modules, I’ve added a new module named app-primeng.module.ts & added the reference of this to our application main module i.e. in shared.module.ts file.
 
Our application does have some constant which we are storing in app.constants.ts file. Here is the content of the app.constants.ts file.
  1. export class AppConstants {  
  2.     public static readonly FIELD_TXT = 'field';  
  3.     public static readonly HEADER_TXT = 'header';  
  4.     public static readonly HASHTAG_TXT = '#';  
  5.     public static readonly ID_ATTR = 'id';  
  6.     public static readonly STYLE_ATTR = 'style';  
  7.     public static readonly WIDTH_ATTR = 'width';  
  8.     public static readonly HEIGHT_ATTR = 'height';  
  9.     public static readonly TRANSPARENT_BG = '#ffffff00';  
  10.     public static readonly BLACK_COLOR = '#000000';  
  11.     public static readonly WHITE_COLOR = '#FFFFFF';  
  12.     static ColorCodes = class {  
  13.         public static readonly CODES: any[] = ['#00ffff''#802A2A''#4d4d00''#000000''#0000ff''#a52a2a''#4b0026''#00008b''#008b8b''#a9a9a9''#006400''#bdb76b''#8b008b''#556b2f''#ff8c00''#9932cc''#8b0000''#e9967a''#9400d3''#ff00ff''#ffd700''#008000''#4b0082''#939365''#99994d''#8B6969''#90ee90''#F3AE9A''#ffb6c1''#96C8A2''#00ff00''#800000''#000080''#808000''#ffa500''#862d2d''#800080''#ff0000''#2d5986''#ffff00''#354b83''#116805''#47887f''#79341d''#febc52''#7d7050''#c96b8f''#66dd38''#61535b''#512818''#a320d0''#2b583c''#f19057''#3c53a7''#b42c7a''#61a31b''#9b0c2f''#ec87aa''#5e1654''#b36807''#52143d''#7d4b61''#a62638''#a15d7b''#a72c0c''#6F4242''#271f35''#8B8878''#324F17''#46523C''#6B9C75''#7F9A65''#414F12''#859C27''#79341d''#98A148''#808000'];  
  14.     };  
  15.     static SVGConstants = class {  
  16.         public static readonly SVG_NS: string = 'http://www.w3.org/2000/svg';  
  17.         public static readonly SVG_XML_NS: string = 'http://www.w3.org/2000/xmlns/';  
  18.         public static readonly SVG_XLINK_NS: string = 'http://www.w3.org/1999/xlink';  
  19.         public static readonly XMLNS_XLINK_ATTR: string = 'xmlns:xlink';  
  20.         public static readonly PATH_TENSION: number = 0.2;  
  21.         public static readonly SVG_ID_ATTR = 'svg-canvas';  
  22.         public static readonly SVG_PARENT_ID_ATTR = 'svg-parent';  
  23.         public static readonly SVG_CONTAINER_ID_ATTR = 'svg-container';  
  24.         public static readonly SVG_STYLE_ATTR = 'position:absolute;';  
  25.         public static readonly SVG_TAG = 'svg';  
  26.         public static readonly VIEWBOX_ATTR = 'viewBox';  
  27.         public static readonly REF_X_ATTR = 'refX';  
  28.         public static readonly REF_Y_ATTR = 'refY';  
  29.         public static readonly PRESERVE_ASPECT_RATIO = 'preserveAspectRatio';  
  30.         public static readonly MARKER_UNITS_ATTR = 'markerUnits';  
  31.         public static readonly MARKER_HEIGHT_ATTR = 'markerHeight';  
  32.         public static readonly MARKER_WIDTH_ATTR = 'markerWidth';  
  33.         public static readonly ORIENT_ATTR = 'orient';  
  34.         public static readonly DEFS_TAG = 'defs';  
  35.         public static readonly MARKER_TAG = 'marker';  
  36.         public static readonly PATH_TAG = 'path';  
  37.         public static readonly TITLE_TAG = 'title';  
  38.         public static readonly GROUP_TAG = 'g';  
  39.         public static readonly CIRCLE_TAG = 'circle';  
  40.     };  
  41. }  
As you could see from the above file, we have some SVG constants created for use in the application. The advantage of doing so is that some of the SVG attributes are case sensitive. By following the above approach, it will be less error prone since we have all our attributes defined at one place. If any change is needed, we can simply update it over here. Inside our Shared module, we’ll now create a mapper component which will help the user create a mapping. I’m keeping the table datasource type as Object (since I need it to be generic in nature).
 
MapperComponent.ts
  1. import {  
  2.     Component,  
  3.     OnInit,  
  4.     OnDestroy,  
  5.     ElementRef,  
  6.     ViewChild,  
  7.     Input,  
  8.     AfterViewInit,  
  9.     ViewChildren,  
  10.     QueryList,  
  11.     HostListener,  
  12. } from '@angular/core';  
  13. import {  
  14.     Table  
  15. } from 'primeng/table';  
  16. import {  
  17.     MessageService,  
  18.     SortEvent  
  19. } from 'primeng/api';  
  20. import {  
  21.     fromEvent,  
  22.     Subject,  
  23.     zip  
  24. } from 'rxjs';  
  25. import {  
  26.     debounceTime,  
  27.     takeUntil  
  28. } from 'rxjs/operators';  
  29. import {  
  30.     AppConstants  
  31. } from 'src/app/app-constant';  
  32. import {  
  33.     IColumn  
  34. } from '../../models/table-config.model';  
  35. import {  
  36.     Utility  
  37. } from 'src/app/app-utility';  
  38. @Component({  
  39.     selector: 'app-mapper',  
  40.     templateUrl: './mapper.component.html'  
  41. })  
  42. export class MapperComponent implements OnInit, AfterViewInit, OnDestroy {  
  43.     @ViewChild('masterTable') masterTable: Table;  
  44.     @ViewChild('referenceTable') referenceTable: Table;  
  45.     @Input() mstDataKey: string = null// unique identifier from Master Table  
  46.     @Input() refDataKey: string = null// unique identifier from Reference Table  
  47.     @Input() mstTableName: string = null;  
  48.     @Input() refTableName: string = null;  
  49.     @Input() mstEmptyMessage = 'No Records Found';  
  50.     @Input() refEmptyMessage = 'No Records Found';  
  51.     @Input() mstDataSource: object[] = null// master DataSource  
  52.     @Input() refDataSource: object[] = null// reference DataSource  
  53.     isFirstLoad = true;  
  54.     loading = false;  
  55.     enableBgColorMapping = false;  
  56.     mstSelectedCols: IColumn[] = [];  
  57.     refSelectedCols: IColumn[] = [];  
  58.     draggedObj: Object = null;  
  59.     droppedOnToObj: Object = null;  
  60.     reRenderMapping = false;  
  61.     randomColor: string = AppConstants.TRANSPARENT_BG;  
  62.     private readonly MST_TXT = 'mst';  
  63.     private readonly REF_TXT = 'ref';  
  64.     private rowsBgPainted = false;  
  65.     private componentDestroyed$: Subject < boolean > = new Subject < false > ();  
  66.     // SVG Caching Related fields  
  67.     private _svgParentPosition: any = null;  
  68.     private _mstParentDiv: HTMLDivElement = null;  
  69.     private _mstParentDivLeft: number = null;  
  70.     private _refParentDiv: HTMLDivElement = null;  
  71.     private _refParentDivLeft: number = null;  
  72.     constructor(private elRef: ElementRef, private messageService: MessageService) {}  
  73.     ngOnInit(): void {  
  74.         this.getDefaultTableCols();  
  75.     }  
  76.     ngAfterViewInit(): void {  
  77.         // Call to DrawMapping Method on initial Load;  
  78.         if (this.mstSelectedCols.length > 0 && this.refSelectedCols.length > 0) {  
  79.             this.drawMapping();  
  80.         }  
  81.         const rowsRendered$ = zip(this.mstRows.changes, this.refRows.changes);  
  82.         rowsRendered$.pipe(takeUntil(this.componentDestroyed$), debounceTime(1000)).subscribe(([]) => {  
  83.             if (this.reRenderMapping && this.mstSelectedCols.length > 0 && this.refSelectedCols.length > 0) {  
  84.                 this.drawMapping();  
  85.                 this.reRenderMapping = false;  
  86.             } else if (this.reRenderMapping) {  
  87.                 this.removeSVG();  
  88.                 this.reRenderMapping = false;  
  89.             }  
  90.         });  
  91.         this.windowResizeEventBinding();  
  92.     }  
  93.     toggleMappingDisplayMode(): void {  
  94.         this.drawMapping();  
  95.     }  
  96.     /** 
  97.  
  98.     * Sorts the Table Data 
  99.  
  100.     * @param event SortEvent Object 
  101.  
  102.     */  
  103.     customSort(event: SortEvent): void {  
  104.         event.data.sort((data1, data2) => {  
  105.             const value1 = data1[event.field];  
  106.             const value2 = data2[event.field];  
  107.             let result = null;  
  108.             if (value1 == null && value2 != null) {  
  109.                 result = -1;  
  110.             } else if (value1 != null && value2 == null) {  
  111.                 result = 1;  
  112.             } else if (value1 == null && value2 == null) {  
  113.                 result = 0;  
  114.             } else if (typeof value1 === 'string' && typeof value2 === 'string') {  
  115.                 result = value1.localeCompare(value2);  
  116.             } else {  
  117.                 result = (value1 < value2) ? -1 : (value1 > value2) ? 1 : 0;  
  118.             }  
  119.             return (event.order * result);  
  120.         });  
  121.     }  
  122.     //#region Drag & Drop Events  
  123.     dragStart(event, rowData: Object) {  
  124.         event.dataTransfer.effectAllowed = 'copy';  
  125.         event.dataTransfer.dropEffect = 'move';  
  126.         this.draggedObj = rowData;  
  127.     }  
  128.     drop(event, refTableRowRef: HTMLTableRowElement, rowData: Object): void {  
  129.         event.preventDefault();  
  130.         event.dataTransfer.dropEffect = 'move';  
  131.         let referenceTableRow = refTableRowRef || event.path[1];  
  132.         this.droppedOnToObj = referenceTableRow ? rowData : null;  
  133.     }  
  134.     dragEnd(event, mstTableRowRef: HTMLTableRowElement): void {  
  135.         let masterTableRow = mstTableRowRef || event.path[0];  
  136.         if (masterTableRow && this.droppedOnToObj) {  
  137.             this.mapRelation(this.draggedObj, this.droppedOnToObj);  
  138.         }  
  139.         this.draggedObj = null;  
  140.         this.droppedOnToObj = null;  
  141.     }  
  142.     private getDefaultTableCols(): void {  
  143.         if (this.mstDataSource && this.mstDataSource.length > 0) {  
  144.             this.mstSelectedCols.push(...this.getDefaultMasterColsFromObj());  
  145.         }  
  146.         if (this.refDataSource && this.refDataSource.length > 0) {  
  147.             this.refSelectedCols.push(...this.getDefaultReferenceColsFromObj());  
  148.         }  
  149.     }  
  150.     private getDefaultMasterColsFromObj(): IColumn[] {  
  151.         const objMstColumns: IColumn[] = [];  
  152.         Object.keys(this.mstDataSource[0]).map(x => {  
  153.             objMstColumns.push(this.getColumn(x));  
  154.         });  
  155.         return objMstColumns;  
  156.     }  
  157.     private getDefaultReferenceColsFromObj(): IColumn[] {  
  158.         const objRefColumns: IColumn[] = [];  
  159.         Object.keys(this.refDataSource[0]).map(x => {  
  160.             objRefColumns.push(this.getColumn(x, false));  
  161.         });  
  162.         return objRefColumns;  
  163.     }  
  164.     private getColumn(fieldName, isMasterField = true, isVisible = true, width = '120px'): IColumn {  
  165.         const objCol: IColumn = {  
  166.             field: fieldName,  
  167.             displayName: Utility.insertSpace(Utility.initCapitalize(fieldName)),  
  168.             width: width,  
  169.             textAlign: isMasterField ? Utility.getTextAlignFromValueType(this.mstDataSource[0][fieldName]) : Utility.getTextAlignFromValueType(this.refDataSource[0][fieldName]),  
  170.             allowFiltering: true,  
  171.             allowSorting: true,  
  172.             allowReordering: true,  
  173.             allowResizing: true,  
  174.             isVisible: isVisible,  
  175.             cssClass: 'ui-resizable-column',  
  176.         };  
  177.         return objCol;  
  178.     }  
  179.     /** 
  180.  
  181.     * GET's the HTML Table Row Reference 
  182.  
  183.     * @param objData Object Data 
  184.  
  185.     * @param searchTable Search in Master Or Reference Table 
  186.  
  187.     */  
  188.     private getHTMLTableRowRef(objData: Object, searchTable: string): HTMLTableRowElement {  
  189.         let elementId: string = null;  
  190.         if (searchTable === this.MST_TXT) {  
  191.             let dataKey = objData[this.mstDataKey];  
  192.             if (this.masterTable.hasFilter() && this.masterTable.filteredValue) {  
  193.                 elementId = this.MST_TXT + (dataKey === null ? this.masterTable.filteredValue.findIndex(x => x === objData).toString() : this.masterTable.filteredValue.findIndex(x => x[this.mstDataKey] === objData[this.mstDataKey]).toString());  
  194.             } else {  
  195.                 elementId = this.MST_TXT + (dataKey === null ? this.masterTable.value.findIndex(x => x === objData).toString() : this.masterTable.value.findIndex(x => x[this.mstDataKey] === objData[this.mstDataKey]).toString());  
  196.             }  
  197.         } else {  
  198.             let dataKey = objData[this.refDataKey];  
  199.             if (this.referenceTable.hasFilter() && this.referenceTable.filteredValue) {  
  200.                 elementId = this.REF_TXT + (dataKey === null ? this.referenceTable.filteredValue.findIndex(x => x === objData).toString() : this.referenceTable.filteredValue.findIndex(x => x[this.refDataKey] === objData[this.refDataKey]).toString());  
  201.             } else {  
  202.                 elementId = this.REF_TXT + (dataKey === null ? this.referenceTable.value.findIndex(x => x === objData).toString() : this.referenceTable.value.findIndex(x => x[this.refDataKey] === objData[this.refDataKey]).toString());  
  203.             }  
  204.         }  
  205.         return (elementId && document.getElementById(elementId)) as HTMLTableRowElement;  
  206.     }  
  207.     /** 
  208.  
  209.     * Maps the newly dragged Row from Master Table to Reference Table 
  210.  
  211.     */  
  212.     private mapRelation(draggedObject: Object, droppedOnToObject: Object): void {  
  213.         this.paintBgOrDrawArrow(draggedObject, droppedOnToObject);  
  214.     }  
  215.     private drawMapping(): void {  
  216.         this.redrawSVG();  
  217.     }  
  218.     /** 
  219.  
  220.     * Paints the Bg color the MasterTable & Reference Table Mapped Rows 
  221.  
  222.     */  
  223.     private paintBgOrDrawArrow(draggedObject: Object, droppedOnToObject: Object): void {  
  224.         let masterTableRow = this.getHTMLTableRowRef(draggedObject, this.MST_TXT);  
  225.         let referenceTableRow = this.getHTMLTableRowRef(droppedOnToObject, this.REF_TXT);  
  226.         if (masterTableRow && referenceTableRow) {  
  227.             this.randomColor = Utility.getRandomHexColor();  
  228.             if (this.enableBgColorMapping) {  
  229.                 this.paintBackground(this.randomColor, masterTableRow, referenceTableRow);  
  230.             } else {  
  231.                 this.drawArrowRelations(this.randomColor, AppConstants.SVGConstants.PATH_TENSION, masterTableRow, referenceTableRow, draggedObject, droppedOnToObject);  
  232.             }  
  233.         }  
  234.     }  
  235.     private paintBackground(bgColor: string, masterTableRow: HTMLTableRowElement, referenceTableRow: HTMLTableRowElement): void {  
  236.         const textColor = Utility.hexInverseBw(bgColor);  
  237.         const tdClass = textColor === AppConstants.WHITE_COLOR ? 'tdWhite' : 'tdBlack';  
  238.         masterTableRow.style.background = bgColor;  
  239.         masterTableRow.classList.add(tdClass);  
  240.         referenceTableRow.style.background = bgColor;  
  241.         referenceTableRow.classList.add(tdClass);  
  242.     }  
  243.     /** 
  244.  
  245.     * WindowResize Event 
  246.  
  247.     */  
  248.     private windowResizeEventBinding(): void {  
  249.         fromEvent(window, 'resize').pipe(takeUntil(this.componentDestroyed$), debounceTime(1000)).subscribe(() => {  
  250.             if (!this.enableBgColorMapping) {  
  251.                 this.redrawSVG();  
  252.             }  
  253.         });  
  254.     }  
  255.     // #region 'SVG Region'  
  256.     /** 
  257.  
  258.     * Removs the SVG element from the page 
  259.  
  260.     */  
  261.     removeSVG(): void {  
  262.         const htmlSVGElement = this.elRef.nativeElement.querySelector(`#${AppConstants.SVGConstants.SVG_ID_ATTR}`);  
  263.         if (htmlSVGElement) {  
  264.             const svgContainer = this.elRef.nativeElement.querySelector(`#${AppConstants.SVGConstants.SVG_CONTAINER_ID_ATTR}`);  
  265.             svgContainer.removeChild(htmlSVGElement);  
  266.         }  
  267.     }  
  268.     /** 
  269.  
  270.     * Redraws the SVG 
  271.  
  272.     */  
  273.     redrawSVG(): void {  
  274.         this.removeSVG();  
  275.         this.clearSVGCache();  
  276.     }  
  277.     private getSVGGroup(mstRowRef: HTMLTableRowElement, rowData: Object): SVGGElement {  
  278.         const groupId = this.mstDataKey ? `group${rowData[this.mstDataKey]}` : `group${mstRowRef.id}`;  
  279.         return this.elRef.nativeElement.querySelector(`#${groupId}`);  
  280.     }  
  281.     /** 
  282.  
  283.     * Removes the SVG Group 
  284.  
  285.     * @param svgGroup SVGGroup to be removed 
  286.  
  287.     */  
  288.     private removeSVGGroup(svgGroup: SVGGElement): void {  
  289.         const svgElement = this.elRef.nativeElement.querySelector(`#${AppConstants.SVGConstants.SVG_ID_ATTR}`);  
  290.         if (svgElement && svgGroup) {  
  291.             svgElement.removeChild(svgGroup);  
  292.         }  
  293.     }  
  294.     /** 
  295.  
  296.     * GET's the element's Bounding Client Rect position. 
  297.  
  298.     * @param element HTML Element 
  299.  
  300.     */  
  301.     private getAbsolutePosition(element) {  
  302.         const rect = element.getBoundingClientRect();  
  303.         let xVal = rect.left;  
  304.         let yVal = rect.top;  
  305.         if (window.scrollX) {  
  306.             xVal += window.scrollX;  
  307.         }  
  308.         if (window.scrollY) {  
  309.             yVal += window.scrollY;  
  310.         }  
  311.         return {  
  312.             x: xVal,  
  313.             y: yVal  
  314.         };  
  315.     }  
  316.     /** 
  317.  
  318.     * Creates the SVG Element 
  319.  
  320.     */  
  321.     private createSVG(): any {  
  322.         const htmlSVGElement = this.elRef.nativeElement.querySelector(`#${AppConstants.SVGConstants.SVG_ID_ATTR}`);  
  323.         const svgParent = this.elRef.nativeElement.querySelector(`#${AppConstants.SVGConstants.SVG_PARENT_ID_ATTR}`);  
  324.         const svgParentPosition = this.getAbsolutePosition(svgParent);  
  325.         const svgContainer = this.elRef.nativeElement.querySelector(`#${AppConstants.SVGConstants.SVG_CONTAINER_ID_ATTR}`);  
  326.         const svgContainerPosition = this.getAbsolutePosition(svgContainer);  
  327.         if (null == htmlSVGElement) {  
  328.             const svg = document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.SVG_TAG);  
  329.             svg.setAttribute(AppConstants.ID_ATTR, AppConstants.SVGConstants.SVG_ID_ATTR);  
  330.             svg.setAttribute(AppConstants.STYLE_ATTR, AppConstants.SVGConstants.SVG_STYLE_ATTR);  
  331.             svg.setAttribute(AppConstants.WIDTH_ATTR, svgParent.clientWidth.toString());  
  332.             svg.setAttribute(AppConstants.HEIGHT_ATTR, svgParent.clientHeight.toString());  
  333.             svg.setAttribute(AppConstants.SVGConstants.VIEWBOX_ATTR, `${svgContainerPosition.x - svgParentPosition.x}  
  334.   
  335. ${svgContainerPosition.y - svgParentPosition.y} ${svgContainer.clientWidth} ${svgContainer.clientHeight}`);  
  336.             svg.setAttribute(AppConstants.SVGConstants.PRESERVE_ASPECT_RATIO, 'xMinYMin meet');  
  337.             svg.setAttributeNS(AppConstants.SVGConstants.SVG_XML_NS, AppConstants.SVGConstants.XMLNS_XLINK_ATTR, AppConstants.SVGConstants.SVG_XLINK_NS);  
  338.             svgContainer.appendChild(svg);  
  339.             return svg;  
  340.         }  
  341.         return htmlSVGElement;  
  342.     }  
  343.     /** 
  344.  
  345.     * Adds Title to the SVG Group Element 
  346.  
  347.     */  
  348.     private addSVGGroupTitle(svgGroupElement: SVGGElement, draggedObject: Object): void {  
  349.         const svgGroupTitle = < SVGTitleElement > document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.TITLE_TAG);  
  350.         svgGroupTitle.textContent = this.mstDataKey && this.mstDataKey !== '' ? draggedObject[this.mstDataKey] : '';  
  351.         svgGroupElement.appendChild(svgGroupTitle);  
  352.     }  
  353.     /** 
  354.  
  355.     * Creates a new SVG Group 
  356.  
  357.     */  
  358.     private createSVGGroupElement(masterTableRow: HTMLTableRowElement, draggedObject: Object): SVGGElement {  
  359.         // re-use existing group incase there are one to many mappings (corrupted data)  
  360.         const svgGroup = this.getSVGGroup(masterTableRow, draggedObject)  
  361.         if (svgGroup) {  
  362.             return svgGroup;  
  363.         }  
  364.         const id = this.mstDataKey && this.mstDataKey !== '' ? `group${draggedObject[this.mstDataKey]}` : `group${masterTableRow.id}`;  
  365.         const svgGroupElement = < SVGGElement > document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.GROUP_TAG);  
  366.         svgGroupElement.setAttribute('id', id);  
  367.         svgGroupElement.setAttribute('shape-rendering''inherit');  
  368.         svgGroupElement.setAttribute('pointer-events''all');  
  369.         const svg = this.createSVG();  
  370.         svg.appendChild(svgGroupElement);  
  371.         return svgGroupElement;  
  372.     }  
  373.     /** 
  374.  
  375.     * Draws a circle on the given co-ordinates 
  376.  
  377.     * @param x Element xPosition 
  378.  
  379.     * @param y Element yPosition 
  380.  
  381.     * @param radius Radius of the Circle 
  382.  
  383.     * @param color Color of the Circle 
  384.  
  385.     */  
  386.     private drawCircle(x, y, radius, color, svgGroupElement: SVGGElement): void {  
  387.         const shape = document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.CIRCLE_TAG);  
  388.         shape.setAttributeNS(null'cx', x);  
  389.         shape.setAttributeNS(null'cy', y);  
  390.         shape.setAttributeNS(null'r', radius);  
  391.         shape.setAttributeNS(null'fill', color);  
  392.         svgGroupElement.appendChild(shape);  
  393.     }  
  394.     /** 
  395.  
  396.     * Creates the Arrow marker 
  397.  
  398.     */  
  399.     private createArrowMarker(color: string, svgGroupElement: SVGGElement, referenceTableRow: HTMLTableRowElement): void {  
  400.         const defs = document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.DEFS_TAG);  
  401.         svgGroupElement.appendChild(defs);  
  402.         const id = 'triangle' + referenceTableRow.id;  
  403.         const marker = document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.MARKER_TAG);  
  404.         marker.setAttribute(AppConstants.ID_ATTR, id);  
  405.         marker.setAttribute(AppConstants.SVGConstants.VIEWBOX_ATTR, '0 0 10 10');  
  406.         marker.setAttribute(AppConstants.SVGConstants.REF_X_ATTR, '0');  
  407.         marker.setAttribute(AppConstants.SVGConstants.REF_Y_ATTR, '5');  
  408.         marker.setAttribute(AppConstants.SVGConstants.MARKER_UNITS_ATTR, 'strokeWidth');  
  409.         marker.setAttribute(AppConstants.SVGConstants.MARKER_WIDTH_ATTR, '10');  
  410.         marker.setAttribute(AppConstants.SVGConstants.MARKER_HEIGHT_ATTR, '8');  
  411.         marker.setAttribute(AppConstants.SVGConstants.ORIENT_ATTR, 'auto');  
  412.         marker.setAttribute('fill', color);  
  413.         const path = document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.PATH_TAG);  
  414.         marker.appendChild(path);  
  415.         path.setAttribute('d''M 0 0 L 10 5 L 0 10 z');  
  416.         defs.appendChild(marker);  
  417.     }  
  418.     /** 
  419.  
  420.     * Draws a curved line based on the parameters passed & with given color & path tension 
  421.  
  422.     * @param x1 x1 Position 
  423.  
  424.     * @param y1 y1 Position 
  425.  
  426.     * @param x2 x2 Position 
  427.  
  428.     * @param y2 y2 Position 
  429.  
  430.     * @param color Stroke Color 
  431.  
  432.     * @param tension Path Tension 
  433.  
  434.     */  
  435.     private drawCurvedLine(x1, y1, x2, y2, color, tension, svgGroupElement: SVGGElement, referenceTableRow: HTMLTableRowElement, draggedObject: Object, droppedOnToObject: Object): void {  
  436.         const shape = document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.PATH_TAG); {  
  437.             const delta = (x2 - x1) * tension;  
  438.             const hx1 = x1 + delta;  
  439.             const hy1 = y1;  
  440.             const hx2 = x2 - delta;  
  441.             const hy2 = y2;  
  442.             const path = 'M ' + x1 + ' ' + y1 + ' C ' + hx1 + ' ' + hy1 + ' ' + hx2 + ' ' + hy2 + ' ' + x2 + ' ' + y2;  
  443.             shape.setAttributeNS(null'd', path);  
  444.             shape.setAttributeNS(null'fill''none');  
  445.             shape.setAttributeNS(null'stroke', color);  
  446.             shape.setAttributeNS(null'stroke-width''1.1');  
  447.             shape.setAttributeNS(null'marker-end', `url(#triangle${referenceTableRow.id})`);  
  448.             svgGroupElement.appendChild(shape);  
  449.         }  
  450.     }  
  451.     /** 
  452.  
  453.     * Draws the relation between the rows 
  454.  
  455.     * @param color Color value 
  456.  
  457.     * @param tension Path Tension 
  458.  
  459.     */  
  460.     private drawArrowRelations(color: string, tension: number, masterTableRow: HTMLTableRowElement, referenceTableRow: HTMLTableRowElement, draggedObject: Object, droppedOnToObject: Object): void {  
  461.         let svgParentPosition = this._svgParentPosition;  
  462.         if (!svgParentPosition) {  
  463.             const svgParent = this.elRef.nativeElement.querySelector(`#${AppConstants.SVGConstants.SVG_PARENT_ID_ATTR}`);  
  464.             svgParentPosition = this.getAbsolutePosition(svgParent);  
  465.             this._svgParentPosition = svgParentPosition;  
  466.         }  
  467.         if (!this._mstParentDiv) {  
  468.             this._mstParentDiv = masterTableRow.closest('div.col-12') as HTMLDivElement;  
  469.         }  
  470.         const mstParentDiv = this._mstParentDiv;  
  471.         let leftPos = this.getAbsolutePosition(masterTableRow);  
  472.         if (!this._mstParentDivLeft) {  
  473.             this._mstParentDivLeft = mstParentDiv.getBoundingClientRect().left;  
  474.         }  
  475.         leftPos.x = this._mstParentDivLeft;  
  476.         let x1 = leftPos.x - svgParentPosition.x;  
  477.         let y1 = leftPos.y - svgParentPosition.y;  
  478.         x1 += mstParentDiv.offsetWidth;  
  479.         y1 += (masterTableRow.offsetHeight / 2);  
  480.         if (!this._refParentDiv) {  
  481.             this._refParentDiv = referenceTableRow.closest('div.col-12') as HTMLDivElement;  
  482.         }  
  483.         const refParentDiv = this._refParentDiv;  
  484.         const rightPos = this.getAbsolutePosition(referenceTableRow);  
  485.         if (!this._refParentDivLeft) {  
  486.             this._refParentDivLeft = refParentDiv.getBoundingClientRect().left;  
  487.         }  
  488.         rightPos.x = this._refParentDivLeft;  
  489.         const x2 = rightPos.x - svgParentPosition.x - 8;  
  490.         let y2 = rightPos.y - svgParentPosition.y;  
  491.         y2 += (referenceTableRow.offsetHeight / 2);  
  492.         let svgGroupElement = this.createSVGGroupElement(masterTableRow, draggedObject);  
  493.         this.addSVGGroupTitle(svgGroupElement, draggedObject);  
  494.         this.drawCircle(x1, y1, 4, color, svgGroupElement);  
  495.         this.createArrowMarker(color, svgGroupElement, referenceTableRow);  
  496.         this.drawCurvedLine(x1, y1, x2, y2, color, tension, svgGroupElement, referenceTableRow, draggedObject, droppedOnToObject);  
  497.     }  
  498.     // #endregion  
  499.     private clearSVGCache(): void {  
  500.         this._svgParentPosition = null;  
  501.         this._mstParentDiv = null;  
  502.         this._mstParentDivLeft = null;  
  503.         this._refParentDiv = null;  
  504.         this._refParentDivLeft = null;  
  505.     }  
  506.     @HostListener('window:beforeunload')  
  507.     ngOnDestroy(): void {  
  508.         this.componentDestroyed$.next(true);  
  509.         this.componentDestroyed$.complete();  
  510.     }  
  511. }  
Most of the code is self-explanatory, & I’ve also added comments to help you understand if there's any doubt. Just one thing I would like to highlight is for each row among the master Table (i.e. the first Table) & Reference table (i.e. the second table), I’m appending mst & ref with index respectively. So for instance, for our 1st record from master table will have the ID as “mst0”. Similarly, for our 3rd record from reference table will have the ID as “ref2”. That’s why in our getHTMLTableRowRef () we’re manually appending mst & ref with index no to find the HTML table row.
 
MapperComponent.html
 
I’ve divided our screen into 3 columns, i.e.
 
The 1st column will store the Master Table data
 
The 2nd column will be showing our mapping of what we’ll create. I’ve used the viewBox feature of SVG to show SVG content on a particular section of our screen.
 
The 3rd column will store the ReferenceTable data
  1. <div id="svg-parent" class="row no-gutters" style="width:100%;">  
  2.     <div class="col-12 col-md-5 border border-secondary" style="overflow: auto;">  
  3.         <p-table id="masterTable" #masterTable [value]="mstDataSource" [columns]="mstSelectedCols" [dataKey]="mstDataKey"  
  4.   
  5. [metaKeySelection]="true" [customSort]="true" columnResizeMode="expand" [resizableColumns]="true" [responsive]="true"  
  6.   
  7. [loading]="loading">  
  8.             <ng-template pTemplate="header" let-columns>  
  9.                 <tr>  
  10.                     <ng-container *ngFor="let col of columns">  
  11.                         <th *ngIf="col.isVisible" [title]="col.field" pResizableColumn [pSortableColumn]="col.field" [style.width]="col.width">  
  12.   
  13. {{col.displayName}}  
  14.   
  15. </th>  
  16.                     </ng-container>  
  17.                 </tr>  
  18.             </ng-template>  
  19.             <ng-template pTemplate="body" let-rowData let-rowIndex="rowIndex" let-columns="columns">  
  20.                 <tr *ngIf="mstSelectedCols.length > 0" #mst id="mst{{rowIndex}}" class="drag-row" pDraggable="rowobject"  
  21.   
  22. (onDragStart)="dragStart($event,rowData)" (onDragEnd)="dragEnd($event,mst,rowData)">  
  23.                     <ng-container *ngFor="let col of columns">  
  24.                         <td *ngIf="col.isVisible" [title]="rowData[col.field]" [ngClass]="col.cssClass" [style.text-align]="col.textAlign">  
  25.   
  26. {{rowData[col.field]}}  
  27.   
  28. </td>  
  29.                     </ng-container>  
  30.                 </tr>  
  31.             </ng-template>  
  32.             <ng-template pTemplate="emptymessage">  
  33.                 <tr>  
  34.                     <td *ngIf="mstDataSource.length===0 || masterTable.filteredValue.length===0" [attr.colspan]="mstSelectedCols.length"  
  35.   
  36. style="text-align: left;">  
  37.                         <h6 class="text-danger">{{mstEmptyMessage}}</h6>  
  38.                     </td>  
  39.                 </tr>  
  40.             </ng-template>  
  41.         </p-table>  
  42.     </div>  
  43.     <div class="col-1" id="svg-container"></div>  
  44.     <div class="col-12 col-md-6 border border-secondary" style="overflow: auto;">  
  45.         <p-table id="referenceTable" #referenceTable [value]="refDataSource" [columns]="refSelectedCols" [dataKey]="refDataKey"  
  46.   
  47. [loading]="loading" columnResizeMode="expand" [resizableColumns]="true" [responsive]="true">  
  48.             <ng-template pTemplate="header" let-columns>  
  49.                 <tr>  
  50.                     <ng-container *ngFor="let col of columns">  
  51.                         <th *ngIf="col.isVisible" [title]="col.field" pResizableColumn [pSortableColumn]="col.field" [style.width]="col.width">  
  52.   
  53. {{col.displayName}}  
  54.   
  55. </th>  
  56.                     </ng-container>  
  57.                 </tr>  
  58.             </ng-template>  
  59.             <ng-template pTemplate="body" let-rowData let-rowIndex="rowIndex" let-columns="columns">  
  60.                 <tr *ngIf="refSelectedCols.length>0" #ref id="ref{{rowIndex}}" class="drop-row" [pDroppableDisabled]="isReadOnly"  
  61.   
  62. pDroppable="rowobject" (onDrop)="drop($event,ref,rowData)">  
  63.                     <ng-container *ngFor="let col of columns">  
  64.                         <td *ngIf="col.isVisible" [title]="rowData[col.field]" [class]="col.cssClass" [style.text-align]="col.textAlign">  
  65.   
  66. {{rowData[col.field]}}  
  67.   
  68. </td>  
  69.                     </ng-container>  
  70.                 </tr>  
  71.             </ng-template>  
  72.             <ng-template pTemplate="emptymessage">  
  73.                 <tr>  
  74.                     <td *ngIf="refDataSource.length===0 || referenceTable.filteredValue.length===0" [attr.colspan]="refSelectedCols.length"  
  75.   
  76. style="text-align: left;">  
  77.                         <h6 class="text-danger">{{refEmptyMessage}}</h6>  
  78.                     </td>  
  79.                 </tr>  
  80.             </ng-template>  
  81.         </p-table>  
  82.     </div>  
  83. </div>  
Now, we will define our Featured module component i.e. HomeComponent, in which we’ll call our MapperComponent.
 
HomeComponent.ts
  1. import {  
  2.     Component,  
  3.     OnInit  
  4. } from '@angular/core';  
  5. import {  
  6.     HomeService  
  7. } from './home.service';  
  8. import {  
  9.     Customer  
  10. } from './models/customer.model';  
  11. import {  
  12.     Product  
  13. } from './models/product.model';  
  14. import {  
  15.     forkJoin,  
  16.     Subject  
  17. } from 'rxjs';  
  18. import {  
  19.     takeUntil  
  20. } from 'rxjs/operators';  
  21. @Component({  
  22.     selector: 'app-home',  
  23.     templateUrl: './home.component.html'  
  24. })  
  25. export class HomeComponent implements OnInit {  
  26.     customers: Customer[];  
  27.     products: Product[];  
  28.     isDataLoaded: boolean;  
  29.     private componentDestroyed$: Subject < boolean > ;  
  30.     constructor(private homeService: HomeService) {  
  31.         this.isDataLoaded = false;  
  32.         this.componentDestroyed$ = new Subject < false > ();  
  33.     }  
  34.     ngOnInit() {  
  35.         this.getData();  
  36.     }  
  37.     private getData() {  
  38.         forkJoin(this.homeService.getCustomers(), this.homeService.getProducts()).pipe(takeUntil(this.componentDestroyed$)).subscribe(([custResponse, prodResponse]) => {  
  39.             console.log('Customer Response', custResponse);  
  40.             console.log('Prod Response', prodResponse);  
  41.             this.customers = custResponse;  
  42.             this.products = prodResponse;  
  43.             this.isDataLoaded = true;  
  44.         });  
  45.     }  
  46.     ngOnDestroy(): void {  
  47.         this.componentDestroyed$.next(true);  
  48.         this.componentDestroyed$.complete();  
  49.     }  
  50. }  
HomeComponent.html
  1. <div class="row">  
  2.     <div class="col-12">  
  3.         <h2>Mapping Demo using SVG</h2>  
  4.     </div>  
  5. </div>  
  6. <div class="w-100" *ngIf="isDataLoaded">  
  7.     <app-mapper [mstDataSource]="customers" [refDataSource]="products" [mstDataKey]="'eid'" [refDataKey]="'_id'"  
  8.   
  9. [mstTableName]="'Customer'" [refTableName]="'Products'"></app-mapper>  
  10. </div>  
HomeService.ts
  1. import {  
  2.     Injectable  
  3. } from '@angular/core';  
  4. import {  
  5.     HttpClient  
  6. } from '@angular/common/http';  
  7. import {  
  8.     Observable  
  9. } from 'rxjs';  
  10. import {  
  11.     Customer  
  12. } from './models/customer.model';  
  13. import {  
  14.     Product  
  15. } from './models/product.model';  
  16. @Injectable()  
  17. export class HomeService {  
  18.     constructor(private httpClient: HttpClient) {}  
  19.     public getCustomers(): Observable < Customer[] > {  
  20.         return this.httpClient.get < Customer[] > ("assets/data.repository.json");  
  21.     }  
  22.     public getProducts(): Observable < Product[] > {  
  23.         return this.httpClient.get < Product[] > ("assets/products.repository.json");  
  24.     }  
  25. }  
Finally, try to run the application & drag one row from the first table & drop it onto another table. You can see the mapping is drawn between the two table records.
 
 
 
I hope you liked this article, stay tuned in for my next one!


Similar Articles