ES2015 Source Code: Class — JavaScript implementation of Delphi VCL TList Class including flexible multi-property List-Sort algorithm and more
Just for fun, I created this
ECMAScript 2015 (i.e., ES2015 or modern object-oriented JavaScript) version of a Borland / Embarcadero
Delphi TList VCL Class equivalent (or, at least partial equivalent) to demonstrate one approach to Object Oriented Programming (OOP) in JavaScript, and
I call the class "DList". I originally implemented this years ago in straight JavaScript, and have updated it to take advantage of ES2015 syntax and encapsulation. I have implemented some of the core TList methods like Add(), Insert(), InsertRange(), Delete(), IndexOf(), Delete, and Sort() in hopes of demonstrating the power and speed of JavaScript. This DList class wraps up the native JS array object and adds some nice extra functionality.
My DList JS/ES Class also
implements my proprietary multi-column / multi-property sort algorithm that is very adaptable and flexible and should be easy to understand (see comments in source code) where
I use a powers-of-two column-precedence algorithm within the sort-comparison closure callback method (in particular, see the Sorter: function(a, b) {} closure code in the source comments).
When I first wrote the original JS code, ES2015 Classes were not even an agreed standard, so I was quite excited to move on to the much simpler and much better real object oriented web-development language and framework:
Google's Dart programming language. And,
even though this JS/ES class makes life in JavaScript easier, simply put, Google Dart makes this type of functionality ultra-simple since their core API / framework / library includes a rich set of List / Collections classes. But, just in case you want to stick with JavaScript / ECMAScript, this should be an interesting example for you.
Live Example
In addition to the DList Object-List Class source-code, I also included the source-code I use for running various tests to confirm sorting and list-object(s) manipulation. Just call the test routine in your HTML body tag by including: onload="DListTest();"
And, to make demonstrating this easier, I have included an embedded JSFiddle (see below my source code).
This Fiddle (link to full fiddle) ES / JavaScript TList Class (Delphi-like) Example also shows it in action. Presuming you are a developer, you will find it very easy to examine the page-source code and see the ECMAScript / JavaScript programming (.js files) used by the example and so forth.
NOTE: you definitely need a modern browser (e.g., Chrome or FireFox) with ES2015 Class support. Either one has rather fantastic built in developer tools for stepping, tracing, object-interrogation, and other features you may also want for seeing how this all works.
ECMAScript ES2015 Source Code
/********************************************************************************
This source code is Copyright (c) 2017
Author: Mike Eberhart
I hereby release this code under the terms of the MIT License (for freeware).
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
********************************************************************************/
'use strict';
/*
███████████████████████████████████████████████████████████████████████████████████████████
DList Class
NOTE: this code uses the modern syntactic-sugar of ECMAScript 2015 CLASSES, and thus
requires a modern browser which supports their usage (e.g., Chrome, Firefox)
DESCRIPTION
This Class defines a List designed to hold objects of any type.
For proper encapsulation, the ONLY outside access to the internally maintained list is
through the Items[] array property (read only).
▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
PROPERTIES
PROPERTIES
- Items[] ............ an Array of any objects you wish to store in a List.
Because it is an Array, all methods available to arrays can be used in
addition to the custom DList methods defined herein; though, not all Array
methods/properties will make sense depending on what you store in the List.
- Count .............. the number of elements in the list.
- InstanceName ....... String value assigned during constructor; nice for debugging
TODO (PERHAPS):
- EnforceUnique (true/false)... in case non-unique list requirements exist
- MaxListElements (int)........ if this becomes an issue
- Sorted (true/false).......... maintain list as sorted at all times?
And, binary-search (IndexOf) if sorted and unique)
▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
METHODS
Add(item) ............ Add one item to list (by reference) and return index to that item
AddRange(arrayObj) ... Add list of Items at end of list
Clear() .............. Completely empty the list; i.e., Count = 0; Items=[]
Delete(index) ........ Remove item at [index] position
Equals(a,b) .......... [CLOSURE, via parm @ create] : Compare two objects
and return true (=) or false (!=)
Insert(index, objToInsert) ... Insert item at specified [index]
IndexOf(item) ........ Find first position of item, if not found return -1
Sort() ............... Sorts Items array based on array object's property
value(s) according to Sorter() method logic.
Sorter(a,b) .......... [CLOSURE, via parm @ create] : used by Sort() method
to determine sort-order of Items. See DETAILS below.
Update(index, objNew) Update the object at specified Index with new one.
TODO (PERHAPS): Move(), Exchange(), First(), Last(), Next(), Prev()
***********************
METHOD DETAILS / NOTES:
***********************
................................................................................
Equals() method
List creator can implement, via constructor parameter, a closure for this.
See example code.
This method is used to determine whether two objects in our Items array
are to be considered as Equals for the purposes of IndexOf() operations, which
can be used for:
1) enforcing a UNIQUE CONSTRAINT on array contents;
2) locating an array item to Delete;
3) locate a position in the array for an Insert.
NOTE: Equals() is NOT for testing true array-element-objects equivalence.
................................................................................
Sorter() method, used by Sort()
List creator can implement, via constructor parameter, a closure for this, which
the Sort() method will call internally. The Sorter method must compare two
Item-array objects (let's call them "a" and "b") and return a value that
is either:
Zero: indicating "a" and "b" are considered equal and no sorting required
for these two objects (i.e., their order relative to each other in
the Items array is already as we desire, since they are the "same"
for our sorting purposes).
<0 : less than 0, indicating that we want to Sort object "a" into a lower
Items-array position (index) than object "b"
>0 : greater than 0, indicating the opposite of our less-than-zero condition.
................................................................................
Insertion and Deletion methods act on an Index value that is simply the Items
array's Index position at which to perform the operation. If you wish to
Insert or Delete at a position determined by object-property value(s) stored
within the Items array, acquire the target-index first by calling the IndexOf()
method with object-search-criteria; the result of that call will be your index
for Insert/InsertRange or Delete.
e.g., mylist.Delete(mylist.IndexOf({id:123, name:"number123"}));
▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
EXAMPLES:
This is a how to create a new DList object and pass the Closures (functions)
for the Equals() and Sorter() methods the object should use.
--------------------------------------------------------------------------------
var mylist = new DList(
'MyInstanceNameHere',
//Equals() closure:
//E.g., we may want to ensure uniqueness on the combination of two properties
//present on each of our array Items, like id and lastname properties; if so,
//our code is as follows (NOTE: REMEMBER CASE-SENSITIVITY ISSUES):
function(a,b)
{
return (a.id == b.id && a.lastname == b.lastname);
},
//Sorter() closure:
Sorter: function(a, b) {
{
//Example algorithm for sorting on multiple columns; adapts with ease!
//Sort ascending by id, name
//<variable> = <expression> ? <true clause=""> : <false clause="">
//Use Powers-of-Two multiplier to set column-sort-order-precedence
//IMPORTANT NOTE: HIGHER power of two implies higher precedence
//
//REMEMBER THIS TOO: JS array sort is CASE-SENSITIVE by default.
//Use .toUpperCase() or such to level this, e.g. before our return-algorithm:
return (
1 * ((a.id == b.id) ? 0 : (a.id < b.id) ? -1 : 1) +
2 * ((a.firstname == b.firstname) ? 0 : (a.firstname < b.firstname) ? -1 : 1) +
4 * ((a.lastname.toUpperCase() == b.lastname.toUpperCase()) ? 0
: (a.lastname.toUpperCase() < b.lastname.toUpperCase()) ? -1 : 1)
)
}
}
);
███████████████████████████████████████████████████████████████████████████████████████████
*/
class DList {
constructor(instancename = '[InstanceName not specified in List constructor]',
//default closure for Sorter: default to no sort-ordering
equals = function(a, b) {return 0},
//default closure for Equals: default to no match
sorter = function(a, b) {return 0}) {
this._InstanceName = instancename;
this.Sorter = sorter;
this.Equals = equals;
//create the array that will contain our list Items
this._items = [];
} //constructor
//Read-only InstanceName property accessor (set during construction)
get InstanceName() {
return this._InstanceName;
}
//Provide read-only access to the internal items-array
get Items() {
return this._items;
}
//Read-only Count property
get Count() {
return this._items.length;
}
//Add single object reference to our list's Items[] array, and return its index position
Add(objToAdd) {
let newItemIndex = this._items.length;
this._items[newItemIndex] = objToAdd;
return newItemIndex;
}
//Use AddRange when merging an array of objects to our Items[].
//This adds Items to the end of our list's Items[] array;
//use Add() when adding only one item (more efficient).
//AddRange is just an InsertRange at end-of-Items-array.
AddRange(objArrayToAdd) {
this.InsertRange(this._items.length, objArrayToAdd);
}
Clear() {
this._items = [];
}
//TODO: Delete/DeleteRange with bad (i.e. -1 or non-existent) index must fail!
// Set to EOList?
Delete(index) {
this._items.splice(index, 1);
}
//Remove several entries at index (RemoveCount = how many to delete)
DeleteRange(index, RemoveCount) {
this._items.splice(index, RemoveCount);
}
IndexOf(obj) {
let i = this._items.length;
while (i--) {
if (this.Equals(this._items[i], obj)) {
return i;
}
}
return -1;
}
Insert(index, objToInsert) {
//make sure insertion-index is valid; TODO: real error check/range-warn.
index = Math.max(0, Math.min(index, this._items.length));
//splice obj into array at index, remove zero elements in the process.
this._items.splice(index, 0, objToInsert);
}
//Insert an array of objects, at index position, into Items[]
//Use Insert() method for single object insertion.
InsertRange(index, objArrayToInsert) {
//make sure insertion-index is valid; TODO: real error check/range-warn.
index = Math.max(0, Math.min(index, this._items.length));
let tmpArray1 = [];
//get any portion of existing Items[] array before insertion point
//Note: slice uses Zero-based begin/end indexes for extraction and extracts up to,
//but NOT including end index value (i.e., stops at ending index -1).
if (index > 0) {
tmpArray1 = this._items.slice(0, index);
}
//add our new values at array insertion point
tmpArray1 = tmpArray1.concat(objArrayToInsert);
//use slice() to get portion after insertion point; join that with beginning + added
this._items = tmpArray1.concat(this._items.slice(index, this._items.length));
}
//If sorting is desired, when creating a new() List, the creator must implement, via an
//optional parameter, a closure which will be assigned to the method named "Sorter".
Sort() {
this._items.sort(this.Sorter);
}
//Place a new object into Items array where old object was.
//Caller must pass valid index (TODO: test index)
Update(index, objNew) {
if ((this.IndexOf(objNew) == -1) || //OK if objNew's key-field-values are new (unique)
(this.Equals(objNew, this._items[index]))) //OK to update same unique "key"
{
this._items[index] = objNew;
} else {
//TODO: REPLACE THIS with raise error if unique constraint will be broken on update...
console.log(this.InstanceName + '.List.Update() violated unique constraint');
}
}
} //DList
/*
███████████████████████████████████████████████████████████████████████████████████████████
Various test-conditions and web / browser-console logging routines...
███████████████████████████████████████████████████████████████████████████████████████████
*/
function DListTest(){
const sSeparatorLine1 = '███████████████████████████████████████████████████████████████████████████████████████████';
const sSeparatorLine2 = '■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■';
const sSeparatorLine3 = '▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪';
let traceDiv = document.getElementById('trace');
function LogToScreen(stringToLog) {
if (stringToLog == '<hr />') {
console.log(sSeparatorLine1);
} else {
console.log(stringToLog);
}
if (typeof(stringToLog) == 'object') {
traceDiv.innerHTML += '
[' + stringToLog.InstanceName + '] SEE CONSOLE FOR FULL OBJECT INSPECTION CAPABILITY; Inspect the Items (array) objects';
} else {
traceDiv.innerHTML += '
' + stringToLog;
}
}
function LogItemsToScreen() {
for (let i = 0; i < mylist.Count; i++) {
LogToScreen('List Item[' + i + ']: ID=' + mylist.Items[i].id + '; firstname=' + mylist.Items[i].firstname + '; lastname=' + mylist.Items[i].lastname );
}
}
/*
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
BEGIN: create instance of the DList Class and get these tests rolling...
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
*/
let mylist = new DList(
'mylist',
//Equals() closure:
// E.g., we may want to ensure uniqueness on the combination of two properties
// present on each of our array Items, like id and lastname properties; if so,
// our code is as follows:
function(a, b) {
return (a.id == b.id && a.lastname == b.lastname);
},
//Sorter() closure:
function(a, b) {
{
//Example algorithm for sorting on multiple columns; adapts with ease!
//Sort ascending by id, name
//<variable> = <expression> ? <true clause=""> : <false clause="">
//Use Powers-of-Two multiplier to set column-sort-order-precedence
//IMPORTANT NOTE: HIGHER power of two implies higher precedence
//REMEMBER THIS TOO: JS array sort is CASE-SENSITIVE by default.
// Use .toUpperCase() or such to level this, e.g. we make lastname case-insensitive:
return (
1 * ((a.id == b.id) ? 0 : (a.id < b.id) ? -1 : 1) +
2 * ((a.firstname == b.firstname) ? 0 : (a.firstname < b.firstname) ? -1 : 1) +
4 * ((a.lastname.toUpperCase() == b.lastname.toUpperCase()) ? 0 : (a.lastname.toUpperCase() < b.lastname.toUpperCase()) ? -1 : 1)
)
}
}
);
//------------------------------------------------------------------------------
mylist.Add({id:4, firstname:'first-4', lastname:'last-4'});
mylist.Add({id:5, firstname:'first-5', lastname:'last-5'});
mylist.Add({id:13, firstname:'first-2', lastname:'last-1'});
mylist.Add({id:1, firstname:'first-2', lastname:'last-1'});
mylist.Add({id:1, firstname:'first-1', lastname:'last-1'});
mylist.Add({id:2, firstname:'FIRST-2', lastname:'last-2'});
mylist.Add({id:3, firstname:'first-3', lastname:'last-3'});
//List was build unsorted, now SORT it (on last, first, id) USING CLOSURE provided in constructor
//The items will then be ordered as: id:1-5, firstname:"first-1...first-5", lastname:"last-1...last-5"
mylist.Sort();
//Create another list loaded from mylist (so each console.log object-ref remains valid),
//otherwise, if we just log "mylist" over and over, in the end, all logged mylist refs will
//point to the final state (vs. each stepped-state of these tests)
let mylistInitialLoad = new DList('mylistInitialLoad');
mylistInitialLoad.AddRange(mylist.Items);
LogToScreen('Sorted (by lastname, firstname, id) initial 5-item list "mylistInitialLoad" follows:');
LogToScreen('Count: ' + mylistInitialLoad.Count);
LogToScreen('Items.length: ' + mylistInitialLoad.Items.length);
LogToScreen(mylistInitialLoad);
LogItemsToScreen();
LogToScreen('<hr />');
//------------------------------------------------------------------------------
mylist.Add({id:1, firstname:'dup-id-1', lastname:'dup-id-1'});
mylist.Add({id:2, firstname:'dup-id-2', lastname:'dup-id-2'});
mylist.Add({id:3, firstname:'dup-id-3', lastname:'dup-id-3'});
let mylistAdds = new DList('mylistAdds2');
mylistAdds.AddRange(mylist.Items);
LogToScreen('Sorted List with 3 items added (duplicate id values for id 1-3) to initial sorted 5-item list;
' +
'List not re-sorted after additions; "mylistAdds" follows:');
LogToScreen('Count: ' + mylistAdds.Count);
LogToScreen(mylistAdds);
LogItemsToScreen();
LogToScreen('<hr />');
//------------------------------------------------------------------------------
mylist.Sort();
let mylistSorted2 = new DList('mylistSorted2');
mylistSorted2.AddRange(mylist.Items);
LogToScreen('Same 8-item list, but RE-SORTED NOW (by lastname, firstname, id)... new instance dump follows;');
LogToScreen(mylistSorted2);
LogItemsToScreen();
LogToScreen('<hr />');
//------------------------------------------------------------------------------
mylist.Insert(mylist.IndexOf({id:1, firstname:'dup-id-1', lastname:'dup-id-1'}),
{id:111, firstname:'insert-before-id-1, dup-id-1', lastname:'inserted'});
let mylistAfterIns1 = new DList('mylistAfterIns1');
mylistAfterIns1.AddRange(mylist.Items);
LogToScreen('Insertion test (1 row, before dup-id-1)...;
' +
'List not re-sorted after inserts; "mylistAfterIns1" follows:');
LogToScreen('Count: ' + mylistAfterIns1.Count);
LogToScreen(mylistAfterIns1);
LogItemsToScreen();
LogToScreen('<hr />');
//------------------------------------------------------------------------------
mylist.Delete(mylist.IndexOf({id:4, firstname:'first-4', lastname:'last-4'}));
let mylistAfterDel1 = new DList('mylistAfterDel1');
mylistAfterDel1.AddRange(mylist.Items);
LogToScreen('Deletion test (id:4 removed)...;
' +
'List not re-sorted after delete; "mylistAfterDel1" follows:');
LogToScreen('Count: ' + mylistAfterDel1.Count);
LogToScreen(mylistAfterDel1);
LogItemsToScreen();
LogToScreen('<hr />');
//------------------------------------------------------------------------------
//attempt to insert into invalid position
mylist.Insert(999, {id:999, firstname:'WayOutFirst', lastname:'WayOutLast'});
let mylistAfterBogusInsPos = new DList('mylistAfterBogusInsPos');
mylistAfterBogusInsPos.AddRange(mylist.Items);
LogToScreen('Attempt to INSERT OUTSIDE Items[] Bounds (id:999)... should simply place new item at end of list;
' +
'"mylistAfterBogusInsPos" follows:');
LogToScreen('Count: ' + mylistAfterBogusInsPos.Count);
LogToScreen(mylistAfterBogusInsPos);
LogItemsToScreen();
LogToScreen('<hr />');
//------------------------------------------------------------------------------
mylist.Sort();
LogToScreen('RE-SORTED our list (by lastname, firstname, id)...');
LogItemsToScreen();
LogToScreen('<hr />');
//------------------------------------------------------------------------------
//attempt to update a value in the Items array with new object
mylist.Update(mylist.IndexOf({id:2, firstname:'dup-id-2', lastname:'dup-id-2'}),
{id:222, firstname:'updated-id-2(first)', lastname:'updated-id-2(last)'});
let mylistAfterUpdate1 = new DList('mylistAfterUpdate1');
mylistAfterUpdate1.AddRange(mylist.Items);
LogToScreen('List should now have dup-id-2 values changed...
' +
'"mylistAfterUpdate1" follows:');
LogToScreen('Count: ' + mylistAfterUpdate1.Count);
LogToScreen(mylistAfterUpdate1);
LogItemsToScreen();
LogToScreen('<hr />');
//------------------------------------------------------------------------------
//attempt update with SAME object (same, per Equals CLOSURE, which is: same id/last-name)
mylist.Update(mylist.IndexOf({id:222, firstname:'updated-id-2(first)', lastname:'updated-id-2(last)'}),
{id:222, firstname:'updated-id-2(first-fixed)', lastname:'updated-id-2(last)'});
let mylistAfterUpdate2 = new DList('mylistAfterUpdate2');
mylistAfterUpdate2.AddRange(mylist.Items);
LogToScreen('List should now have id=222 values changed again...
' +
'"mylistAfterUpdate2" follows:');
LogToScreen('Count: ' + mylistAfterUpdate2.Count);
LogToScreen(mylistAfterUpdate2);
LogItemsToScreen();
LogToScreen('<hr />');
//------------------------------------------------------------------------------
LogToScreen('UNIQUE CONSTRAINT VIOLATION ERROR should follow in Console:');
//attempt update with and violate our UNIQUE Constraint (Equals)
mylist.Update(mylist.IndexOf({id:222, firstname:'updated-id-2(first-fixed)', lastname:'updated-id-2(last)'}),
{id:1, firstname:'first-1', lastname:'last-1'});
LogToScreen('<hr />');
//------------------------------------------------------------------------------
let traceDiv2 = document.getElementById('trace2');
let myObjList = new DList(
'myObjectList',
function(a, b) {
return (a.id == b.id );
}
);
//------------------------------------------------------------------------------
// Test storing some references to external objects now...
let ElementRef1 = document.getElementById('trace-mod-1');
myObjList.Add({id:1, ElementName:'testdiv1', Element: ElementRef1, toString : function() {
return ('toString() closure fired: ' + this.id + ', ' + this.ElementName );
}});
let ElementRef2 = document.getElementById('trace-mod-2');
myObjList.Add({id:2, ElementName:'testdiv2', Element: ElementRef2});
let ElementRef3 = document.getElementById('trace-mod-3');
myObjList.Add({id:3, ElementName:'testdiv3', Element: ElementRef3});
myObjList.Items[myObjList.IndexOf({id:1})].Element.innerHTML =
'testdiv1 : REPLACEMENT via CODE; div referenced via List.Element (stored reference); stored closure results: ' +
myObjList.Items[myObjList.IndexOf({id:1})].toString();
myObjList.Items[myObjList.IndexOf({id:2})].Element.innerHTML = 'testdiv2 : REPLACEMENT via CODE; div referenced via List.Element (stored reference)';
myObjList.Items[myObjList.IndexOf({id:3})].Element.innerHTML = 'testdiv3 : REPLACEMENT via CODE; div referenced via List.Element (stored reference)';
} //end function DListTest
ES2015 DList Class embedded JSFiddle...
Continue to read this
Software Development and Technology Blog for computer programming, software development, and technology Techniques, How-To's, Fixes, Reviews, and News — focused on Dart Language, SQL Server, Delphi, Nvidia CUDA, VMware, TypeScript, SVG, other technology tips and how-to's, and my varied political and economic opinions.