Let’s say that you receive a date like “14/01/2022”, “14 January 2022”, “Jan, 14 2022”etc, and you need to convert this date to an ISO string in JavaScript while ensuring that the date that is inputted is for UTC (Universal Time Coordinated) timezone – no matter where you are in the world. It would seem easy right?
Your first reaction might be to simply do something like this:
1
2
3
constdateString="14 Jan 2022";// U.S. peeps <em>"Jan, 14 2022"</em>
Recently, I thought it would be a cool idea to add a date-time stamp to the end of a Google Doc checklist item with Google Apps Script. So I knew when I completed a task.
I often share a project Google Doc with clients and then add my tasks to the document list. With Google’s new check box list item, I wanted to add the date and time that I completed the task when I checked the box.
The bad news is that there is no onEdit() trigger (like in Google Sheets) for the DocumentApp class that would listen for an edit of the document and see a change of the checked box from unchecked to checked and then apply the date-time stamp. 😢
All good, I settled for the next best thing! A menu item.
Recently, I needed a way to ensure that my JavaScript array of objects does not contain any duplicate objects based on an ‘id’ key. While I do enjoy more common approaches to this solution with the use of a simple ‘for’ loop it is always a bit of fun to see how folks have come up with modern solutions.
It wasn’t too long before I stumbled across this ES6 one-liner:
1
let uniqueObjArray=[...new Map(objArray.map((item)=>[item["id"],item])).values()];
But as many code oneliners are, they are a little tricky to understand. So I wanted to spend some time understanding how this worked and thought, you might find this a little interesting too.
Let’s take a look at an example and then work through each bit as we go.
Table of Contents
The Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
constmyObjArray=[
{
name:"Eva Devore",
character:"Evandra",
episodes:15,
},
{
name:"Alessia Medina",
character:"Nixie",
episodes:15,
},
{
name:"Kendall Drury",
character:"DM",
episodes:15,
},
{
name:"Thomas Taufan",
character:"Antrius",
episodes:14,
},
{
name:"Alessia Medina",
character:"Nixie",
episodes:15,
},
];
// Creates an array of objects with unique "name" property values.
In the example above, we want to create a unique array of objects based on the ‘name’ property of each object set.
You can see that there is a duplicate property in positions 1 and 4 with key ‘name’ and value, ‘Alessia Medina’ that we need to remove.
You can also change the key to either the ‘character’ or ‘episodes’ property.
When the distinct array of objects is created it will set the last duplicate object as the object value. Why? Because the script will essentially reassign the ‘name’ property each time it loops through the array when creating the new map.
Let’s start breaking down the script so we can see how each one operates.
Code Breakdown
myObjArray.map
1
2
3
4
5
6
7
8
9
10
11
12
13
// Creates a new 2d array with the selected key in position 0 and the object in position 1 of each iteration.
let test_uniqueObjArray_map=myObjArray.map((item)=>{
The first task of this script remaps the array of objects using the JavaScript map() method. This method takes a function, which in our is an arrow function.
Map method arrow functions generally look like this:
In our example above, we have our callback arguments on a new line so we will also need to include curly braces {}.
With the map method, the function will act on each array and return the result to generate a new array of the same length.
For us, our call back condition rebuilds each array to make a sub-array containing the value of each name key in the array as the zeroeth element and the object at the first element.
1
2
3
4
5
...
[item["name"],item];
...
So the first element in the new array will look like this:
Before we continue, let’s take a quick look at a basic Map process on a 2d array:
1
2
3
4
5
6
7
8
9
10
11
12
13
// Creates a key-value map of a 2d array
let valuesObject=newMap([
["key_one","val_one"],
["key_two","val_two"],
["key_three","val_three"],
]);
console.log("valuesObject",valuesObject);
// LOGS: valuesObject Map {
// 'key_one' => 'val_one',
// 'key_two' => 'val_two',
// 'key_three' => 'val_three'
// }
To be frank, I didn’t really understand the Map object too well until I explored this script.
Map object stores key-value pairs similar to an Object. However, the Map maintains the insertion order of the properties. You’ll see Map objects often displayed like this when logged out in the console.
1
'key'=>'value'
Map can be iterated through in a similar way to a 2d array with the zeroeth element as a key and the next element as a value for each property of the map – ['key', 'value'].
Alternatively, we can also generate a Map from a 2d array as we did in the example above – turning each sub-array into a key-value pair.
We are using the data we retrieved from our previous example here to remove some of the clutter from the process. I have added those results at the top of the code block above.
In this example, we simply apply new Map to this array of data. By doing this Map turns into a type of Object with a key-value pair. Now keep in mind that Object keys are the highlander of data types – there can be only one.
What does this mean beyond a bad joke that really shows my age?
It means that each key must be unique. All of our keys are now the names of our users. The new Map constructor process will then iterate through each name and store it and then assign its value. If a key already exists it will overwrite it with this next value with the same key name.
This means that the last duplicate key will always be displayed. Effectively only storing unique values.
Displaying the keys of each property in the Map
We can generate iterators to go through each key or value with the keys() and values() methods respectively.
We will have a look at the keys() method first quickly.
Let’s apply keys() to our test_uniqueObjArray_NewMap Map we generated above.
As you can see this produces an iterator of all the (unique) keys in our data as a Map Iterator. It’s not quite an array of objects, but it allows us to iterate over each key to do something with it.
This is similar to using the Array.from() static method that would look like this:
1
2
3
let test_uniqueObjArray_NewMap_values_asArray=Array.from(
test_uniqueObjArray_NewMap_values
);
Performance
So how does this one-liner stack up against a more traditional for-loop like this?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
vararray=[
{
name:"Eva Devore",
character:"Evandra",
episodes:15,
},
{
name:"Alessia Medina",
character:"Nixie",
episodes:15,
},
{
name:"Kendall Drury",
character:"DM",
episodes:15,
},
{
name:"Thomas Taufan",
character:"Antrius",
episodes:14,
},
{
name:"Alessia Medina",
character:"Nixie",
episodes:15,
},
];;
varunique=[];
vardistinct=[];
for(leti=0;i<array.length;i++){
if(!unique[array[i].name]){
distinct.push(array[i].name);
unique[array[i].name]=1;
}
}
console.log(distinct)
Surprisingly better than I thought it would, to be honest.
Running a benchmark test with jsbench.me, the one-liner ran only 13.74% slower. Which is pretty good compared to some of the other options I found out there.
Conclusion
So should you be using this oneliner over the for loop? Is an impressive one-liner better than something more clear? To be honest, I am on the fence.
I do like the way this script operates. It is clean and once I got my head around the Map object, it did make a lot of sense. I think if I saw something like this in the wild I could pretty easily identify what it was for and see that it was a nice short solution to a problem.
I don’t think I would use this approach when I need to iterate over objects in the many thousands. Then speed becomes important. But if I need something in my toolkit to solve a problem like this, then I am definitely going to use it.
I have an example of how I used the code to check for duplicate selections of files in Google Drive here:
I am currently working on a larger project at the moment that requires a lot of front-end wrangling. As a part of this project, I needed to create Button Items that are generated by the user from both an HTML select element for one section and, a comma-separated text input in another section.
When a user selects an item from a select menu, a button appears in a desired area with the name of the selection and a small “X” that can be clicked to remove the item. Likewise, if a user wishes to create a bunch of items separated by commas in an input element and either hit the “Enter” or the “Add” button, then those items will be transformed as a bunch of individual buttons that the user can remove and change.
The buttons essentially become the user’s selection of items.
I also needed to be able to get a list of those item buttons for when I submit a form server-side or for some other task.
As a result, I created a small library called itemButton().
Note: If you have a bit of experience with front-end, then all you may need is to grab the CSS code and input.js file. If you need some further explanation you can find the information below.
What it does
The itemButton() library:
Creates a button named by the selection or the user’s input, This is removable should the user wish to change their choices.
Allows you to select the max number of items you want your users to be able to choose.
Easily extracts a list of buttons by ID and value.
An example
Take a look at an example:
My inability to consistently fail to place an ‘e’ at the end of giraffe notwithstanding, you can see that the user can select items from a select element and they will be displayed in a chosen area below. Further, when the user types some words and separates them by a comma they are displayed as buttons in a div below it.
Limit the number of displayed Item Buttons
You might have also noted that the select element will only display two-item buttons, while the comma-separated list will display up to 10. You are able to create a maximum limit of any of your text input or select elements you are running itemButton() on.
Validation
When the user submits their items, itemButton() will:
Check for duplicates. If it already exists, it won’t be displayed again.
Remove any empty comma-separated elements.
Remove any non-alphanumeric characters.
Cut any text input items between commas to less than or equal to 25 characters (You can change this if you want).
Exchange any spaces between words with a dash for the button ID.
Any item starting with a number gets and “a” at the start of the button ID so it can be used in the HTML.
Getting a list if item buttons
As you can see in the video above, when the user clicks the Log Itemsbutton, an object of ids and values for each item is listed in the browser console using the list() method in itemButton(). This is most useful for when you are submitting data to the server.
Let’s have a gander at the code:
The Code
I have chosen to put the Javascript itemButton() library in an input.js file. In my project, I have other small classes or libraries in that file too. It is up to you how you want to add the code.
The CSS for the buttons is separate, and I have added it to my universal style.css file. Again you can put it anywhere you think works for you.
input.js
input.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
export{itemButton};
/**
* Add, remove and list buttons from a coma separated text input or
* from a select box.
*/
functionitemButton(){
let publicAPI={};
/**
* Adds a deletable button from either an input or select tag.
* @param {string} items - single item or string of comma separated items.
* @param {string} selectionsLoc - id of the location the item buttons will be placed.
* @param {number} numSelections - Total number of items permitted.
* Get all the items in a selected tag location or false if none exist.
* @param {string} selectionsLoc id of the location the item buttons will be placed.
* @return {object|boolean} object of all current item buttons in selected tag
* containing {id: , val: }. If none then false.
*/
publicAPI.list=(selectionsLoc)=>{
let items=document.getElementById(selectionsLoc).querySelectorAll("span");
//If no items exists return false
if(items.length==0){
returnfalse;
}
returnObject.keys(items).map((item)=>{
return{
id:items[item].id,
val:(()=>{
let val=items[item].innerText;
returnval.substr(0,val.length-2);
})(),
};
});
};
returnpublicAPI;
}
stlye.css
Add this to your main CSS file.
style.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* Buttons style for item selections such as categories and tags. */
.item-btn{
background-color:#cfcdcd;
color:rgb(24,24,24);
border-radius:5px;
border:none;
padding:2px;
text-align:center;
display:inline-block;
margin:2px;
}
.item-x-btn{
display:inline-block;
border:none;
background-color:#cfcdcd;
box-shadow:none;
margin-left:1px;
border-radius:50%;
color:rgb(24,24,24);
font-size:small;
line-height:1.28;
transition:0.3s;
}
.item-x-btn:hover{
background-color:firebrick;
color:whitesmoke;
cursor:pointer;
}
.item-x-btn:focus{
outline:none;
}
Quick use guide
Create the select button or input button
First, you will need to create your select or text input elements. Make sure that they have an appropriate ID that we can reference later. You can have as many select or text input elements as you want to reference the itemButton() class.
<h1 align="center">Create removable button item lists from text input andselect tags</h1>
<!--Selection menu used tocreate item buttons-->
<div>
<div>
<label for="items">Add your selection</label>
<select id="items"name="items"></select>
</div>
</div>
<!--Comma separated list that creates item buttons.-->
<div>
<div>
<label for="tags"
>Add your tags<em>(separate by comma):</em></label
></label>
<input type="text"name="tags"id="tags"/>
</div>
<div>
<button id="addTag"type="button">Add</button>
</div>
</div>
</form>
</div>
Here we have added a select element with an id of “items”. We also have a text input with an ID of “tags”. The “tags” element also has an “addTag” button that the user can use to add their tags.
Create a div or span for your item buttons to go
Next, we need to create a location to display your item buttons. In the example below, I have used a div directly below the select or input elements.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!--Selection menu used tocreate item buttons-->
<div>
<div>
<label for="items">Add your selection</label>
<select id="items"name="items"></select>
</div>
<!--Buttons foritems selected appear here-->
<div id="items-selections"></div>
</div>
<!--Comma separated list that creates item buttons.-->
<div>
<div>
<label for="tags"
>Add your tags<em>(separate by comma):</em></label
></label>
<input type="text"name="tags"id="tags"/>
</div>
<div>
<button id="addTag"type="button">Add</button>
</div>
<!--Buttons fortag appear here.-->
<div id="tags-selection"></div>
Importing the itemButton() code
Our next step will be inside the script tags of our HTML file. If you are importing just the input.js file into your HTML file you will need to invoke you script tags as a module :
1
2
3
<script type="module"src="input.js">
...
</script>
If you are importing more than one file, like I am in the example, I recommend you import your Javascript files like this:
1
2
3
4
5
<script type="module">
import{items}from"./resources/items.js";
import{itemButton}from"./resources/input.js";
...
</script>
In the example above, my two files are in the resourcesfolder. The items.js file is just the file I have stored my list of select items.
Add event listeners
Your next task is to add event listeners for each of your elements. For me, I will add an “input” event for my select element.
For my text input, I will add both a “keypress” (Enter) listener if the user hits enter after typing in their items in the “tags” text input or a “click” if they hit the “Add” button. We will also clear out the tags after each use.
Back to our example for our select element, our item is the value of the current selection. The location that we want to display our item buttons is theitems-selection div and the maximum number of items that our user can add is 2.
For our text input, the value is the comma-separated string of values that the user enters. The location the button items will be displayed in will be the tags-selection div and we will allow the user to add up to 10 items.
To get a list of item buttons from any of your assigned areas that display them, you can use the list() method.
This method takes one argument, the element id that the item buttons are contained in. The list method will return an object of key-value pairs containing:
And the resources > items.js file for the select element.
items.js
1
2
3
4
5
6
7
8
9
10
11
12
13
export constitems=[
"Cheese",
"Cake",
"Bananas",
"Pizza",
"Donuts",
"Steak",
"Cucumber",
"Meat Pies",
"Peaches",
"Hamburgers",
"Asparagus",
];
The Wrap Up
I hope you found this small library useful for creating your own buttons. You may wish to make style changes to your buttons to match your own colour theme.
You may also wish to extend or reduce the length of characters for each item. You can do this in the input.js file on line 37.
I really like hearing how people apply these components to their own projects. Feel free to share in the comments below.
If you found this tool useful, please click the like button so I know that I am making good content. Or if you want to get updates on my latest posts, please subscribe (below the comments).
I just had a recent email from a reader who asked how to tidy up a user’s inputted name from say, a Google Form so that all the first letters of each work in the name are capitalised in the same way that the Google Sheets Proper function does.
I thought it would be a good idea to provide a quick reference for the reader and myself for future projects.