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
- "node_modules/primeng/resources/themes/nova-light/theme.css",
- "node_modules/font-awesome/css/font-awesome.min.css",
- "node_modules/primeng/resources/primeng.min.css",
- "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.
- export class AppConstants {
- public static readonly FIELD_TXT = 'field';
- public static readonly HEADER_TXT = 'header';
- public static readonly HASHTAG_TXT = '#';
- public static readonly ID_ATTR = 'id';
- public static readonly STYLE_ATTR = 'style';
- public static readonly WIDTH_ATTR = 'width';
- public static readonly HEIGHT_ATTR = 'height';
- public static readonly TRANSPARENT_BG = '#ffffff00';
- public static readonly BLACK_COLOR = '#000000';
- public static readonly WHITE_COLOR = '#FFFFFF';
- static ColorCodes = class {
- 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'];
- };
- static SVGConstants = class {
- public static readonly SVG_NS: string = 'http://www.w3.org/2000/svg';
- public static readonly SVG_XML_NS: string = 'http://www.w3.org/2000/xmlns/';
- public static readonly SVG_XLINK_NS: string = 'http://www.w3.org/1999/xlink';
- public static readonly XMLNS_XLINK_ATTR: string = 'xmlns:xlink';
- public static readonly PATH_TENSION: number = 0.2;
- public static readonly SVG_ID_ATTR = 'svg-canvas';
- public static readonly SVG_PARENT_ID_ATTR = 'svg-parent';
- public static readonly SVG_CONTAINER_ID_ATTR = 'svg-container';
- public static readonly SVG_STYLE_ATTR = 'position:absolute;';
- public static readonly SVG_TAG = 'svg';
- public static readonly VIEWBOX_ATTR = 'viewBox';
- public static readonly REF_X_ATTR = 'refX';
- public static readonly REF_Y_ATTR = 'refY';
- public static readonly PRESERVE_ASPECT_RATIO = 'preserveAspectRatio';
- public static readonly MARKER_UNITS_ATTR = 'markerUnits';
- public static readonly MARKER_HEIGHT_ATTR = 'markerHeight';
- public static readonly MARKER_WIDTH_ATTR = 'markerWidth';
- public static readonly ORIENT_ATTR = 'orient';
- public static readonly DEFS_TAG = 'defs';
- public static readonly MARKER_TAG = 'marker';
- public static readonly PATH_TAG = 'path';
- public static readonly TITLE_TAG = 'title';
- public static readonly GROUP_TAG = 'g';
- public static readonly CIRCLE_TAG = 'circle';
- };
- }
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
- import {
- Component,
- OnInit,
- OnDestroy,
- ElementRef,
- ViewChild,
- Input,
- AfterViewInit,
- ViewChildren,
- QueryList,
- HostListener,
- } from '@angular/core';
- import {
- Table
- } from 'primeng/table';
- import {
- MessageService,
- SortEvent
- } from 'primeng/api';
- import {
- fromEvent,
- Subject,
- zip
- } from 'rxjs';
- import {
- debounceTime,
- takeUntil
- } from 'rxjs/operators';
- import {
- AppConstants
- } from 'src/app/app-constant';
- import {
- IColumn
- } from '../../models/table-config.model';
- import {
- Utility
- } from 'src/app/app-utility';
- @Component({
- selector: 'app-mapper',
- templateUrl: './mapper.component.html'
- })
- export class MapperComponent implements OnInit, AfterViewInit, OnDestroy {
- @ViewChild('masterTable') masterTable: Table;
- @ViewChild('referenceTable') referenceTable: Table;
- @Input() mstDataKey: string = null;
- @Input() refDataKey: string = null;
- @Input() mstTableName: string = null;
- @Input() refTableName: string = null;
- @Input() mstEmptyMessage = 'No Records Found';
- @Input() refEmptyMessage = 'No Records Found';
- @Input() mstDataSource: object[] = null;
- @Input() refDataSource: object[] = null;
- isFirstLoad = true;
- loading = false;
- enableBgColorMapping = false;
- mstSelectedCols: IColumn[] = [];
- refSelectedCols: IColumn[] = [];
- draggedObj: Object = null;
- droppedOnToObj: Object = null;
- reRenderMapping = false;
- randomColor: string = AppConstants.TRANSPARENT_BG;
- private readonly MST_TXT = 'mst';
- private readonly REF_TXT = 'ref';
- private rowsBgPainted = false;
- private componentDestroyed$: Subject < boolean > = new Subject < false > ();
-
- private _svgParentPosition: any = null;
- private _mstParentDiv: HTMLDivElement = null;
- private _mstParentDivLeft: number = null;
- private _refParentDiv: HTMLDivElement = null;
- private _refParentDivLeft: number = null;
- constructor(private elRef: ElementRef, private messageService: MessageService) {}
- ngOnInit(): void {
- this.getDefaultTableCols();
- }
- ngAfterViewInit(): void {
-
- if (this.mstSelectedCols.length > 0 && this.refSelectedCols.length > 0) {
- this.drawMapping();
- }
- const rowsRendered$ = zip(this.mstRows.changes, this.refRows.changes);
- rowsRendered$.pipe(takeUntil(this.componentDestroyed$), debounceTime(1000)).subscribe(([]) => {
- if (this.reRenderMapping && this.mstSelectedCols.length > 0 && this.refSelectedCols.length > 0) {
- this.drawMapping();
- this.reRenderMapping = false;
- } else if (this.reRenderMapping) {
- this.removeSVG();
- this.reRenderMapping = false;
- }
- });
- this.windowResizeEventBinding();
- }
- toggleMappingDisplayMode(): void {
- this.drawMapping();
- }
-
-
-
-
-
-
-
- customSort(event: SortEvent): void {
- event.data.sort((data1, data2) => {
- const value1 = data1[event.field];
- const value2 = data2[event.field];
- let result = null;
- if (value1 == null && value2 != null) {
- result = -1;
- } else if (value1 != null && value2 == null) {
- result = 1;
- } else if (value1 == null && value2 == null) {
- result = 0;
- } else if (typeof value1 === 'string' && typeof value2 === 'string') {
- result = value1.localeCompare(value2);
- } else {
- result = (value1 < value2) ? -1 : (value1 > value2) ? 1 : 0;
- }
- return (event.order * result);
- });
- }
-
- dragStart(event, rowData: Object) {
- event.dataTransfer.effectAllowed = 'copy';
- event.dataTransfer.dropEffect = 'move';
- this.draggedObj = rowData;
- }
- drop(event, refTableRowRef: HTMLTableRowElement, rowData: Object): void {
- event.preventDefault();
- event.dataTransfer.dropEffect = 'move';
- let referenceTableRow = refTableRowRef || event.path[1];
- this.droppedOnToObj = referenceTableRow ? rowData : null;
- }
- dragEnd(event, mstTableRowRef: HTMLTableRowElement): void {
- let masterTableRow = mstTableRowRef || event.path[0];
- if (masterTableRow && this.droppedOnToObj) {
- this.mapRelation(this.draggedObj, this.droppedOnToObj);
- }
- this.draggedObj = null;
- this.droppedOnToObj = null;
- }
- private getDefaultTableCols(): void {
- if (this.mstDataSource && this.mstDataSource.length > 0) {
- this.mstSelectedCols.push(...this.getDefaultMasterColsFromObj());
- }
- if (this.refDataSource && this.refDataSource.length > 0) {
- this.refSelectedCols.push(...this.getDefaultReferenceColsFromObj());
- }
- }
- private getDefaultMasterColsFromObj(): IColumn[] {
- const objMstColumns: IColumn[] = [];
- Object.keys(this.mstDataSource[0]).map(x => {
- objMstColumns.push(this.getColumn(x));
- });
- return objMstColumns;
- }
- private getDefaultReferenceColsFromObj(): IColumn[] {
- const objRefColumns: IColumn[] = [];
- Object.keys(this.refDataSource[0]).map(x => {
- objRefColumns.push(this.getColumn(x, false));
- });
- return objRefColumns;
- }
- private getColumn(fieldName, isMasterField = true, isVisible = true, width = '120px'): IColumn {
- const objCol: IColumn = {
- field: fieldName,
- displayName: Utility.insertSpace(Utility.initCapitalize(fieldName)),
- width: width,
- textAlign: isMasterField ? Utility.getTextAlignFromValueType(this.mstDataSource[0][fieldName]) : Utility.getTextAlignFromValueType(this.refDataSource[0][fieldName]),
- allowFiltering: true,
- allowSorting: true,
- allowReordering: true,
- allowResizing: true,
- isVisible: isVisible,
- cssClass: 'ui-resizable-column',
- };
- return objCol;
- }
-
-
-
-
-
-
-
-
-
- private getHTMLTableRowRef(objData: Object, searchTable: string): HTMLTableRowElement {
- let elementId: string = null;
- if (searchTable === this.MST_TXT) {
- let dataKey = objData[this.mstDataKey];
- if (this.masterTable.hasFilter() && this.masterTable.filteredValue) {
- 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());
- } else {
- 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());
- }
- } else {
- let dataKey = objData[this.refDataKey];
- if (this.referenceTable.hasFilter() && this.referenceTable.filteredValue) {
- 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());
- } else {
- 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());
- }
- }
- return (elementId && document.getElementById(elementId)) as HTMLTableRowElement;
- }
-
-
-
-
-
- private mapRelation(draggedObject: Object, droppedOnToObject: Object): void {
- this.paintBgOrDrawArrow(draggedObject, droppedOnToObject);
- }
- private drawMapping(): void {
- this.redrawSVG();
- }
-
-
-
-
-
- private paintBgOrDrawArrow(draggedObject: Object, droppedOnToObject: Object): void {
- let masterTableRow = this.getHTMLTableRowRef(draggedObject, this.MST_TXT);
- let referenceTableRow = this.getHTMLTableRowRef(droppedOnToObject, this.REF_TXT);
- if (masterTableRow && referenceTableRow) {
- this.randomColor = Utility.getRandomHexColor();
- if (this.enableBgColorMapping) {
- this.paintBackground(this.randomColor, masterTableRow, referenceTableRow);
- } else {
- this.drawArrowRelations(this.randomColor, AppConstants.SVGConstants.PATH_TENSION, masterTableRow, referenceTableRow, draggedObject, droppedOnToObject);
- }
- }
- }
- private paintBackground(bgColor: string, masterTableRow: HTMLTableRowElement, referenceTableRow: HTMLTableRowElement): void {
- const textColor = Utility.hexInverseBw(bgColor);
- const tdClass = textColor === AppConstants.WHITE_COLOR ? 'tdWhite' : 'tdBlack';
- masterTableRow.style.background = bgColor;
- masterTableRow.classList.add(tdClass);
- referenceTableRow.style.background = bgColor;
- referenceTableRow.classList.add(tdClass);
- }
-
-
-
-
-
- private windowResizeEventBinding(): void {
- fromEvent(window, 'resize').pipe(takeUntil(this.componentDestroyed$), debounceTime(1000)).subscribe(() => {
- if (!this.enableBgColorMapping) {
- this.redrawSVG();
- }
- });
- }
-
-
-
-
-
-
- removeSVG(): void {
- const htmlSVGElement = this.elRef.nativeElement.querySelector(`#${AppConstants.SVGConstants.SVG_ID_ATTR}`);
- if (htmlSVGElement) {
- const svgContainer = this.elRef.nativeElement.querySelector(`#${AppConstants.SVGConstants.SVG_CONTAINER_ID_ATTR}`);
- svgContainer.removeChild(htmlSVGElement);
- }
- }
-
-
-
-
-
- redrawSVG(): void {
- this.removeSVG();
- this.clearSVGCache();
- }
- private getSVGGroup(mstRowRef: HTMLTableRowElement, rowData: Object): SVGGElement {
- const groupId = this.mstDataKey ? `group${rowData[this.mstDataKey]}` : `group${mstRowRef.id}`;
- return this.elRef.nativeElement.querySelector(`#${groupId}`);
- }
-
-
-
-
-
-
-
- private removeSVGGroup(svgGroup: SVGGElement): void {
- const svgElement = this.elRef.nativeElement.querySelector(`#${AppConstants.SVGConstants.SVG_ID_ATTR}`);
- if (svgElement && svgGroup) {
- svgElement.removeChild(svgGroup);
- }
- }
-
-
-
-
-
-
-
- private getAbsolutePosition(element) {
- const rect = element.getBoundingClientRect();
- let xVal = rect.left;
- let yVal = rect.top;
- if (window.scrollX) {
- xVal += window.scrollX;
- }
- if (window.scrollY) {
- yVal += window.scrollY;
- }
- return {
- x: xVal,
- y: yVal
- };
- }
-
-
-
-
-
- private createSVG(): any {
- const htmlSVGElement = this.elRef.nativeElement.querySelector(`#${AppConstants.SVGConstants.SVG_ID_ATTR}`);
- const svgParent = this.elRef.nativeElement.querySelector(`#${AppConstants.SVGConstants.SVG_PARENT_ID_ATTR}`);
- const svgParentPosition = this.getAbsolutePosition(svgParent);
- const svgContainer = this.elRef.nativeElement.querySelector(`#${AppConstants.SVGConstants.SVG_CONTAINER_ID_ATTR}`);
- const svgContainerPosition = this.getAbsolutePosition(svgContainer);
- if (null == htmlSVGElement) {
- const svg = document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.SVG_TAG);
- svg.setAttribute(AppConstants.ID_ATTR, AppConstants.SVGConstants.SVG_ID_ATTR);
- svg.setAttribute(AppConstants.STYLE_ATTR, AppConstants.SVGConstants.SVG_STYLE_ATTR);
- svg.setAttribute(AppConstants.WIDTH_ATTR, svgParent.clientWidth.toString());
- svg.setAttribute(AppConstants.HEIGHT_ATTR, svgParent.clientHeight.toString());
- svg.setAttribute(AppConstants.SVGConstants.VIEWBOX_ATTR, `${svgContainerPosition.x - svgParentPosition.x}
-
- ${svgContainerPosition.y - svgParentPosition.y} ${svgContainer.clientWidth} ${svgContainer.clientHeight}`);
- svg.setAttribute(AppConstants.SVGConstants.PRESERVE_ASPECT_RATIO, 'xMinYMin meet');
- svg.setAttributeNS(AppConstants.SVGConstants.SVG_XML_NS, AppConstants.SVGConstants.XMLNS_XLINK_ATTR, AppConstants.SVGConstants.SVG_XLINK_NS);
- svgContainer.appendChild(svg);
- return svg;
- }
- return htmlSVGElement;
- }
-
-
-
-
-
- private addSVGGroupTitle(svgGroupElement: SVGGElement, draggedObject: Object): void {
- const svgGroupTitle = < SVGTitleElement > document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.TITLE_TAG);
- svgGroupTitle.textContent = this.mstDataKey && this.mstDataKey !== '' ? draggedObject[this.mstDataKey] : '';
- svgGroupElement.appendChild(svgGroupTitle);
- }
-
-
-
-
-
- private createSVGGroupElement(masterTableRow: HTMLTableRowElement, draggedObject: Object): SVGGElement {
-
- const svgGroup = this.getSVGGroup(masterTableRow, draggedObject)
- if (svgGroup) {
- return svgGroup;
- }
- const id = this.mstDataKey && this.mstDataKey !== '' ? `group${draggedObject[this.mstDataKey]}` : `group${masterTableRow.id}`;
- const svgGroupElement = < SVGGElement > document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.GROUP_TAG);
- svgGroupElement.setAttribute('id', id);
- svgGroupElement.setAttribute('shape-rendering', 'inherit');
- svgGroupElement.setAttribute('pointer-events', 'all');
- const svg = this.createSVG();
- svg.appendChild(svgGroupElement);
- return svgGroupElement;
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
- private drawCircle(x, y, radius, color, svgGroupElement: SVGGElement): void {
- const shape = document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.CIRCLE_TAG);
- shape.setAttributeNS(null, 'cx', x);
- shape.setAttributeNS(null, 'cy', y);
- shape.setAttributeNS(null, 'r', radius);
- shape.setAttributeNS(null, 'fill', color);
- svgGroupElement.appendChild(shape);
- }
-
-
-
-
-
- private createArrowMarker(color: string, svgGroupElement: SVGGElement, referenceTableRow: HTMLTableRowElement): void {
- const defs = document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.DEFS_TAG);
- svgGroupElement.appendChild(defs);
- const id = 'triangle' + referenceTableRow.id;
- const marker = document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.MARKER_TAG);
- marker.setAttribute(AppConstants.ID_ATTR, id);
- marker.setAttribute(AppConstants.SVGConstants.VIEWBOX_ATTR, '0 0 10 10');
- marker.setAttribute(AppConstants.SVGConstants.REF_X_ATTR, '0');
- marker.setAttribute(AppConstants.SVGConstants.REF_Y_ATTR, '5');
- marker.setAttribute(AppConstants.SVGConstants.MARKER_UNITS_ATTR, 'strokeWidth');
- marker.setAttribute(AppConstants.SVGConstants.MARKER_WIDTH_ATTR, '10');
- marker.setAttribute(AppConstants.SVGConstants.MARKER_HEIGHT_ATTR, '8');
- marker.setAttribute(AppConstants.SVGConstants.ORIENT_ATTR, 'auto');
- marker.setAttribute('fill', color);
- const path = document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.PATH_TAG);
- marker.appendChild(path);
- path.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
- defs.appendChild(marker);
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- private drawCurvedLine(x1, y1, x2, y2, color, tension, svgGroupElement: SVGGElement, referenceTableRow: HTMLTableRowElement, draggedObject: Object, droppedOnToObject: Object): void {
- const shape = document.createElementNS(AppConstants.SVGConstants.SVG_NS, AppConstants.SVGConstants.PATH_TAG); {
- const delta = (x2 - x1) * tension;
- const hx1 = x1 + delta;
- const hy1 = y1;
- const hx2 = x2 - delta;
- const hy2 = y2;
- const path = 'M ' + x1 + ' ' + y1 + ' C ' + hx1 + ' ' + hy1 + ' ' + hx2 + ' ' + hy2 + ' ' + x2 + ' ' + y2;
- shape.setAttributeNS(null, 'd', path);
- shape.setAttributeNS(null, 'fill', 'none');
- shape.setAttributeNS(null, 'stroke', color);
- shape.setAttributeNS(null, 'stroke-width', '1.1');
- shape.setAttributeNS(null, 'marker-end', `url(#triangle${referenceTableRow.id})`);
- svgGroupElement.appendChild(shape);
- }
- }
-
-
-
-
-
-
-
-
-
- private drawArrowRelations(color: string, tension: number, masterTableRow: HTMLTableRowElement, referenceTableRow: HTMLTableRowElement, draggedObject: Object, droppedOnToObject: Object): void {
- let svgParentPosition = this._svgParentPosition;
- if (!svgParentPosition) {
- const svgParent = this.elRef.nativeElement.querySelector(`#${AppConstants.SVGConstants.SVG_PARENT_ID_ATTR}`);
- svgParentPosition = this.getAbsolutePosition(svgParent);
- this._svgParentPosition = svgParentPosition;
- }
- if (!this._mstParentDiv) {
- this._mstParentDiv = masterTableRow.closest('div.col-12') as HTMLDivElement;
- }
- const mstParentDiv = this._mstParentDiv;
- let leftPos = this.getAbsolutePosition(masterTableRow);
- if (!this._mstParentDivLeft) {
- this._mstParentDivLeft = mstParentDiv.getBoundingClientRect().left;
- }
- leftPos.x = this._mstParentDivLeft;
- let x1 = leftPos.x - svgParentPosition.x;
- let y1 = leftPos.y - svgParentPosition.y;
- x1 += mstParentDiv.offsetWidth;
- y1 += (masterTableRow.offsetHeight / 2);
- if (!this._refParentDiv) {
- this._refParentDiv = referenceTableRow.closest('div.col-12') as HTMLDivElement;
- }
- const refParentDiv = this._refParentDiv;
- const rightPos = this.getAbsolutePosition(referenceTableRow);
- if (!this._refParentDivLeft) {
- this._refParentDivLeft = refParentDiv.getBoundingClientRect().left;
- }
- rightPos.x = this._refParentDivLeft;
- const x2 = rightPos.x - svgParentPosition.x - 8;
- let y2 = rightPos.y - svgParentPosition.y;
- y2 += (referenceTableRow.offsetHeight / 2);
- let svgGroupElement = this.createSVGGroupElement(masterTableRow, draggedObject);
- this.addSVGGroupTitle(svgGroupElement, draggedObject);
- this.drawCircle(x1, y1, 4, color, svgGroupElement);
- this.createArrowMarker(color, svgGroupElement, referenceTableRow);
- this.drawCurvedLine(x1, y1, x2, y2, color, tension, svgGroupElement, referenceTableRow, draggedObject, droppedOnToObject);
- }
-
- private clearSVGCache(): void {
- this._svgParentPosition = null;
- this._mstParentDiv = null;
- this._mstParentDivLeft = null;
- this._refParentDiv = null;
- this._refParentDivLeft = null;
- }
- @HostListener('window:beforeunload')
- ngOnDestroy(): void {
- this.componentDestroyed$.next(true);
- this.componentDestroyed$.complete();
- }
- }
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
- <div id="svg-parent" class="row no-gutters" style="width:100%;">
- <div class="col-12 col-md-5 border border-secondary" style="overflow: auto;">
- <p-table id="masterTable" #masterTable [value]="mstDataSource" [columns]="mstSelectedCols" [dataKey]="mstDataKey"
-
- [metaKeySelection]="true" [customSort]="true" columnResizeMode="expand" [resizableColumns]="true" [responsive]="true"
-
- [loading]="loading">
- <ng-template pTemplate="header" let-columns>
- <tr>
- <ng-container *ngFor="let col of columns">
- <th *ngIf="col.isVisible" [title]="col.field" pResizableColumn [pSortableColumn]="col.field" [style.width]="col.width">
-
- {{col.displayName}}
-
- </th>
- </ng-container>
- </tr>
- </ng-template>
- <ng-template pTemplate="body" let-rowData let-rowIndex="rowIndex" let-columns="columns">
- <tr *ngIf="mstSelectedCols.length > 0" #mst id="mst{{rowIndex}}" class="drag-row" pDraggable="rowobject"
-
- (onDragStart)="dragStart($event,rowData)" (onDragEnd)="dragEnd($event,mst,rowData)">
- <ng-container *ngFor="let col of columns">
- <td *ngIf="col.isVisible" [title]="rowData[col.field]" [ngClass]="col.cssClass" [style.text-align]="col.textAlign">
-
- {{rowData[col.field]}}
-
- </td>
- </ng-container>
- </tr>
- </ng-template>
- <ng-template pTemplate="emptymessage">
- <tr>
- <td *ngIf="mstDataSource.length===0 || masterTable.filteredValue.length===0" [attr.colspan]="mstSelectedCols.length"
-
- style="text-align: left;">
- <h6 class="text-danger">{{mstEmptyMessage}}</h6>
- </td>
- </tr>
- </ng-template>
- </p-table>
- </div>
- <div class="col-1" id="svg-container"></div>
- <div class="col-12 col-md-6 border border-secondary" style="overflow: auto;">
- <p-table id="referenceTable" #referenceTable [value]="refDataSource" [columns]="refSelectedCols" [dataKey]="refDataKey"
-
- [loading]="loading" columnResizeMode="expand" [resizableColumns]="true" [responsive]="true">
- <ng-template pTemplate="header" let-columns>
- <tr>
- <ng-container *ngFor="let col of columns">
- <th *ngIf="col.isVisible" [title]="col.field" pResizableColumn [pSortableColumn]="col.field" [style.width]="col.width">
-
- {{col.displayName}}
-
- </th>
- </ng-container>
- </tr>
- </ng-template>
- <ng-template pTemplate="body" let-rowData let-rowIndex="rowIndex" let-columns="columns">
- <tr *ngIf="refSelectedCols.length>0" #ref id="ref{{rowIndex}}" class="drop-row" [pDroppableDisabled]="isReadOnly"
-
- pDroppable="rowobject" (onDrop)="drop($event,ref,rowData)">
- <ng-container *ngFor="let col of columns">
- <td *ngIf="col.isVisible" [title]="rowData[col.field]" [class]="col.cssClass" [style.text-align]="col.textAlign">
-
- {{rowData[col.field]}}
-
- </td>
- </ng-container>
- </tr>
- </ng-template>
- <ng-template pTemplate="emptymessage">
- <tr>
- <td *ngIf="refDataSource.length===0 || referenceTable.filteredValue.length===0" [attr.colspan]="refSelectedCols.length"
-
- style="text-align: left;">
- <h6 class="text-danger">{{refEmptyMessage}}</h6>
- </td>
- </tr>
- </ng-template>
- </p-table>
- </div>
- </div>
Now, we will define our Featured module component i.e. HomeComponent, in which we’ll call our MapperComponent.
HomeComponent.ts
- import {
- Component,
- OnInit
- } from '@angular/core';
- import {
- HomeService
- } from './home.service';
- import {
- Customer
- } from './models/customer.model';
- import {
- Product
- } from './models/product.model';
- import {
- forkJoin,
- Subject
- } from 'rxjs';
- import {
- takeUntil
- } from 'rxjs/operators';
- @Component({
- selector: 'app-home',
- templateUrl: './home.component.html'
- })
- export class HomeComponent implements OnInit {
- customers: Customer[];
- products: Product[];
- isDataLoaded: boolean;
- private componentDestroyed$: Subject < boolean > ;
- constructor(private homeService: HomeService) {
- this.isDataLoaded = false;
- this.componentDestroyed$ = new Subject < false > ();
- }
- ngOnInit() {
- this.getData();
- }
- private getData() {
- forkJoin(this.homeService.getCustomers(), this.homeService.getProducts()).pipe(takeUntil(this.componentDestroyed$)).subscribe(([custResponse, prodResponse]) => {
- console.log('Customer Response', custResponse);
- console.log('Prod Response', prodResponse);
- this.customers = custResponse;
- this.products = prodResponse;
- this.isDataLoaded = true;
- });
- }
- ngOnDestroy(): void {
- this.componentDestroyed$.next(true);
- this.componentDestroyed$.complete();
- }
- }
HomeComponent.html
- <div class="row">
- <div class="col-12">
- <h2>Mapping Demo using SVG</h2>
- </div>
- </div>
- <div class="w-100" *ngIf="isDataLoaded">
- <app-mapper [mstDataSource]="customers" [refDataSource]="products" [mstDataKey]="'eid'" [refDataKey]="'_id'"
-
- [mstTableName]="'Customer'" [refTableName]="'Products'"></app-mapper>
- </div>
HomeService.ts
- import {
- Injectable
- } from '@angular/core';
- import {
- HttpClient
- } from '@angular/common/http';
- import {
- Observable
- } from 'rxjs';
- import {
- Customer
- } from './models/customer.model';
- import {
- Product
- } from './models/product.model';
- @Injectable()
- export class HomeService {
- constructor(private httpClient: HttpClient) {}
- public getCustomers(): Observable < Customer[] > {
- return this.httpClient.get < Customer[] > ("assets/data.repository.json");
- }
- public getProducts(): Observable < Product[] > {
- return this.httpClient.get < Product[] > ("assets/products.repository.json");
- }
- }
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!