- Pad a string with a token
- Formatting a timestamp
- Convert a String to Camelcase
- Convert a String to Pascalcase
- Convert a String to Kebabcase
- Convert Hex to RGB
- Convert RGB to Hex
- Random Number Within Range
- Random Color
- Generate an Array of Objects from a specific count
- Generate an Array of Numbers from a range
- Generate a String of a Duplicated Token with a Specific Length
- Pre-Loading an Image
- Lazy-Load an Image
- Sort an Object by keys
- Sort an Object by key values
- Sort an Array by Object Props
- Sort an Array of Objects by Object Prop Values
- Filter instances from an Array
- Convert non-safe characters into HTML entities for HTML attributes
- Check if a node is a child of a specific node type
- Prompt a User to save a file
- Prompt a User to load a file
- Get More Info About Unhandled Promise Rejection
- Parse Query Params to an Object
- Comparing characters in a String
- Download image with custom name
- Copy text from an element
This is supported in newer versions of ES via padStart or padEnd
The example below demonstrates how to remove some of the boilerplate by having a default token.
const pad = (num, token = '00') => `${num}`.padStart(token.length, token);
pad(4);
// returns '04'
pad(4, 'XXXX');
// returns 'XXX4'
Newer format with localized times
const [month, day, year] = (new Date()).toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: 'America/Los_Angeles' }).split('/');
const [time, meridiem] = (new Date()).toLocaleDateString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: 'America/Los_Angeles' }).split(', ')[1].split(' ');
console.log(`${year}-${month}-${day} ${time}${meridiem.toLowerCase()}`);
// logs '2021-05-31 01:29pm'
Couldn't find more info on the options
on MDN, but this article shows all the possible options for each type: https://medium.com/swlh/use-tolocaledatestring-to-format-javascript-dates-2959108ea020.
Function with formatting options
const timestamp = ({
date = new Date(),
format = '[y]-[mo]-[d] [h]:[mi]:[s][md]',
timeZone = 'America/Los_Angeles',
} = {}) => {
// NOTE: Since the format positioning changes per locale, sticking with this one.
// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString#using_locales
const langLocale = 'en-US';
// Format values: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
const [month, day, year] = date.toLocaleDateString(langLocale, { day: '2-digit', month: '2-digit', year: 'numeric', timeZone }).split('/');
const [time, meridiem] = date.toLocaleDateString(langLocale, { hour: '2-digit', minute: '2-digit', second: '2-digit', timeZone }).split(', ')[1].split(' ');
const [hour, minutes, seconds] = time.split(':');
const tokens = {
d: day,
h: hour,
mi: minutes,
mo: month,
md: meridiem.toLowerCase(),
raw: date,
s: seconds,
y: year,
};
return Object.keys(tokens).reduce((str, token) => {
return str.replace(new RegExp(`\\[${token}\\]`, 'g'), tokens[token]);
}, format);
};
console.log( timestamp() );
console.log( timestamp({ format: '[y]-[mo]-[d] [h]:[mi][md]' }) );
console.log( timestamp({ format: 'Month: "[mo]" Day: "[d]"' }) );
var d = new Date();
d.setDate(d.getDate()-1);
console.log( timestamp({ date: d }) );
Old format
const timestamp = '1525877727373'; // would be string from server
const d = new Date(+timestamp);
const hour = d.getHours();
const strMonth = pad(d.getMonth() + 1); // month is zero based, but everything else is 1 based
const strDate = pad(d.getDate());
const strHour = hour > 12 ? pad(hour-12): pad(hour);
const strMins = pad(d.getMinutes());
const meridiem = hour >= 12 ? 'pm' : 'am';
att.value = `${strMonth}/${strDate}/${d.getFullYear()} ${strHour}:${strMins}${meridiem}`;
// returns '04/09/2018 07:55am'
const camelCase = (str) => str
.toLocaleLowerCase()
// kill any non alpha-numeric chars (but leave spaces)
.replace(/[^a-zA-Z0-9 ]/g, '')
// capitalize any words with a leading space (and remove the space)
.replace(/\s+(\w)?/gi, (m, l) => l.toUpperCase());
console.log(camelCase('SOME (rAndom) wurdz!'));
// 'someRandomWurdz'
const pascalCase = (str, del = ' ') => str
.split(del)
.map((word) => word.replace(/^\w/, c => c.toUpperCase()))
.join('');
const kebabCase = (str) => str
.trim()
.toLocaleLowerCase()
// kill any non alpha-numeric chars (but leave spaces)
.replace(/[^a-zA-Z0-9 ]/g, '')
// capitalize any words with a leading space (and replace the space with a hyphen)
.replace(/\s+(\w)?/gi, (m, l) => `-${l.toLowerCase()}`);
console.log(kebabCase('SOME (rAndom) wurdz!'));
// 'some-random-wurdz'
const hexToRGB = (hex) => {
const hasAlpha = hex.length === 5 || hex.length === 9;
let r = 0, g = 0, b = 0, a = 1;
// 3 or 4 digits
if (hex.length === 4 || hex.length === 5) {
r = `0x${hex[1] + hex[1]}`;
g = `0x${hex[2] + hex[2]}`;
b = `0x${hex[3] + hex[3]}`;
if (hasAlpha) a = `0x${hex[4] + hex[4]}`;
}
// 6 or 8 digits
else if (hex.length === 7 || hex.length === 9) {
r = `0x${hex[1] + hex[2]}`;
g = `0x${hex[3] + hex[4]}`;
b = `0x${hex[5] + hex[6]}`;
if (hasAlpha) a = `0x${hex[7] + hex[8]}`;
}
if (hasAlpha) a = +(a / 255).toFixed(2);
return (hasAlpha) ? `rgba(${+r}, ${+g}, ${+b}, ${a})` : `rgb(${+r}, ${+g}, ${+b})`;
}
// usage
hexToRGB('#f1b71f'); // result: 'rgb(241, 183, 31)'
hexToRGB('#f1b71f80'); // result: 'rgba(241, 183, 31, 0.5)'
const rgbToHex = (r, g, b, a) => {
if (typeof r === 'string' && r.startsWith('rgb')) {
const arr = r.replace(/rgba?\(|\)/g, '').split(',').map(s => s.trim());
r = +arr[0];
g = +arr[1];
b = +arr[2];
if (arr.length === 4) a = +arr[3];
}
r = r.toString(16);
if (r.length === 1) r = `0${r}`;
g = g.toString(16);
if (g.length === 1) g = `0${g}`;
b = b.toString(16);
if (b.length === 1) b = `0${b}`;
if (a !== undefined) {
a = Math.round(a * 255).toString(16);
if (a.length === 1) a = `0${a}`;
}
return (a !== undefined) ? `#${r + g + b + a}` : `#${r + g + b}`;
}
// usage
rgbToHex('rgb(241, 183, 31)'); // result: '#f1b71f'
rgbToHex(241, 183, 31); // result: '#f1b71f'
rgbToHex('rgba(241, 183, 31, 0.5)'); // result: '#f1b71f80'
rgbToHex(241, 183, 31, 0.5); // result: '#f1b71f80'
const randomNumber = (min, max) => Math.floor(Math.random() * (max - min + 1) + min);
const randomColor = () => {
return `#${Math.floor(Math.random()*16777215).toString(16).padEnd(6, 'E')}`;
};
const arr = Array(100).fill().map(() => ({ fu: 'bar' }));
const genRange = (start, end) => Array(end - (start - 1)).fill(0).map((_, ndx) => start + ndx);
// usage
genRange(5, 8); // results in [5, 6, 7, 8]
const genString = (token='-', count=10) => Array(count).fill().reduce((str) => `${str}${token}`, '');
// usage
genString(); // results in '----------'
genString('=', 20); // results in '========================================'
Markup
<!-- NOTE: a transparent image is being used as a placeholder, you could use a gif of a spinner -->
<img
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
data-src="https://cdn.com/some/image1.jpg"
>
<img
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
data-src="https://cdn.com/some/image2.jpg"
>
Utils
/**
* Checks whether or not an image (that we're getting ready to load) has already
* been loaded & cached.
*
* @param {String} imgPath - The path to the image.
* @returns {Boolean}
*/
const checkIfImageCached = (imgPath) => {
const img = document.createElement('img');
img.src = imgPath;
return img.complete;
};
/**
* Allows for calling a callback once an image has been loaded into cache.
*
* @param {String} imgPath - The path to the image.
* @param {Function} cb - A callback to be executed once the image has loaded.
*/
const loadImage = (imgPath, cb) => {
const img = new Image();
img.addEventListener('load', cb);
img.src = imgPath;
};
App Code
const handleLoadedImage = (img) => {
img.src = img.dataset.src;
};
const imgs = document.querySelectorAll('img[data-src]');
for(let i=0; i<imgs.length; i++){
const img = imgs[i];
const src = img.dataset.src;
(checkIfImageCached(src))
? handleLoadedImage(img)
: loadImage(src, handleLoadedImage.bind(null, img));
}
This isn't JS, but since I've been using JS (IntersectionObserver
) to do handle it for so many years, adding it as a reminder. If you need to have the image load immediately change the attribute to eager
.
<!-- load only when in view -->
<img
src="https://cdn.com/some/image1.jpg""
loading="lazy"
/>
const obj = {
fu: 'bar',
bar: 'fu',
arr: ['str'],
obj: { fu: 'bar', bar: 'fu' },
};
const sortObjByKeys = (obj) => {
return Object.keys(obj).sort().reduce((sorted, prop) => {
const curr = obj[prop];
sorted[prop] = (!Array.isArray(curr) && curr !== null && typeof curr === 'object')
? sortObjByKeys(curr)
: curr;
return sorted;
}, {});
};
Imagine you have an Object that's sorted by it's keys, but you need an Object sorted by values for display purposes.
/**
* Sorts an Object by it's key values.
*
* @param {Object} obj - An Object that needs sorting.
* @return {Object}
*/
const sortByValues = (obj) => {
const sorted = {};
Object.keys(obj)
.sort((a, b) => {
const optA = obj[a].toLowerCase();
const optB = obj[b].toLowerCase()
if (optA < optB) return -1;
if (optA > optB) return 1;
return 0;
})
.forEach((key) => {
sorted[key] = obj[key];
});
return sorted;
};
// small snippet that demonstrates order
const countryOpts = {
ae: 'United Arab Emirates',
at: 'Austria',
au: 'Australia',
be: 'Belgium',
bg: 'Bulgaria',
ca: 'Canada',
ch: 'Switzerland',
};
console.log( sortByValues(countryOpts) );
const sortArrayByProp = (prop) => (a, b) => {
const _a = `${a[prop]}`.toLowerCase();
const _b = `${b[prop]}`.toLowerCase();
const subCheck = (_b > _a) ? -1 : 0;
return (_a > _b) ? 1 : subCheck;
};
const arr = [{name: 'Blah2'}, {name: 'Blah1'}];
arr.sort(sortArrayByProp('name'));
const sortArrayByPropVal = (arr, orderArr) => {
const _arr = [...arr];
const sortedArr = [];
orderArr.forEach(([ prop, value ]) => {
const tempArr = [];
for (let i=_arr.length - 1; i>=0; i--) {
if (_arr[i][prop] === value) {
tempArr.push(_arr[i]);
_arr.splice(i, 1);
}
}
sortedArr.push(...tempArr.reverse());
});
return [...sortedArr, ..._arr];
};
const arr = [
{ p: '1', prop: 'content' },
{ p: '2', prop: 'title' },
{ p: '3', prop: 'content' },
{ p: '4', prop: 'title' },
{ p: '5', prop: 'fu' },
];
console.log(sortArrayByPropVal(arr, [['prop', 'title'], ['prop', 'fu']]));
There are times (like when sharing WebPack configs) where you'll want to remove instance items from an Array. By
instance items I mean items that were created with the new
operator.
const plugins = origPlugins.filter((plugin) => {
switch (plugin.constructor.name) {
case 'ProgressPlugin': return false;
default: return true;
}
});
const sanitizeStringForAttr = (str) => str
.replace(/&/g, "&")
.replace(/>/g, ">")
.replace(/</g, "<")
.replace(/"/g, """);
/**
* Will loop over a child nodes parent's until it either hits the specified
* root element, or it's determined that the node is a child of a specific
* node type.
*
* @param {HTMLElement} childEl - The node that you're trying to see if it's contained within a specific node type
* @param {HTMLElement} rootEl - The top-most element where the loop should stop iterating
* @param {String} nodeName - The node name that the child should be within
* @return {Boolean}
*/
function childOf(childEl, rootEl, nodeName) {
let currEl = childEl;
while (currEl && currEl !== rootEl) {
if (currEl.nodeName.toLowerCase() === nodeName.toLowerCase()) return true;
currEl = currEl.parentNode;
}
return false;
}
// usage
childOf(ev.target, ev.currentTarget, 'a');
/**
* Allows you to prompt a User to save a file after they've clicked on something.
*
* @param {String} data - The text that'll be written to the file.
* @param {String} name - The name of the file.
* @param {String} type - The file type. Use one of `saveFile.FILE_TYPE__*`
* @example
* saveFile({
* data: JSON.stringify(someObj, null, 2),
* name: 'backup.json',
* type: saveFile.FILE_TYPE__JSON,
* });
*/
function saveFile({ data, name, type }) {
if (!data || !name || !type) throw Error(`You're missing a required param: data: "${data}" | name: "${name}" | type: "${type}"`);
const file = new Blob([data], { type });
const a = document.createElement('a');
a.href = URL.createObjectURL(file);
a.download = name;
a.click();
a.remove();
}
saveFile.FILE_TYPE__JSON = 'application/json';
saveFile.FILE_TYPE__TEXT = 'text/plain';
/**
* Prompts a User to pick a file from their filesystem. Once it's loaded, the
* file is read, and returned via a Promise.
*
* @returns {Promise}
* @example
* loadFile().then((data) => { console.log(data); });
*/
function loadFile() {
return new Promise((resolve) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.addEventListener('change', (ev) => {
const importedFile = ev.target.files[0];
const reader = new FileReader();
reader.addEventListener('load', (readEv) => {
const content = readEv.target.result;
resolve(content);
});
reader.readAsText(importedFile);
});
fileInput.click();
});
}
There may be times when you get random UnhandledRejection
errors. This usually stems from a Promise not having a catch
,
but in a large codebase this can be difficult to track down. Just add the below snippet to the root entry of your app, and
it should give you a more verbose error and point to the file causing the error.
process.on('unhandledRejection', (err) => {
console.error('unhandledRejection', error.stack);
});
function parseQueryParams(params) {
const ret = {};
params.replace(/^\?/, '').split('&').forEach((param) => {
const data = param.split('=');
ret[data[0]] = data[1];
});
return ret;
}
const params = parseQueryParams(window.location.search);
There may come a time where you're trying to compare two Strings that visually look the same, but you get a result that says they're not equal. The below examples show a couple troubleshooting options to display the characters in the String.
// Displays all newline and carriage return characters as '[n]' and '[r]'
console.log(`${string1.replace(/\n/g, '[n]').replace(/\r/g, '[r]')}\n${string2.replace(/\n/g, '[n]').replace(/\r/g, '[r]')}`);
// Displays character codes for each character in a String
const val1 = string1.split('').map((char, ndx) => `[${char.replace('\n', '\\n').replace('\r', '\\r')}: ${string1.charCodeAt(ndx)}]`).join('');
const val2 = string2.split('').map((char, ndx) => `[${char.replace('\n', '\\n').replace('\r', '\\r')}: ${string2.charCodeAt(ndx)}]`).join('');
console.log(`${val1}\n${val2}`);
In the below, the image has already loaded into the DOM. I load the image first so the User has something to look at since the downloading of the image may or may not happen.
<!--
The 'download' attribute will only work with local images. There's info that says it'll also work if a Server returns a specific disposition header but I think they mean if your local server returns that header.
-->
<a href="<IMG_SRC>" download="custom.jpg">
<!--
It's important that crossOrigin is 'anonymous', otherwise Chrome disallows creating blob URLs.
-->
<img src="<IMG_SRC>" crossOrigin="anonymous" />
</a>
// Top of script
window.blobURLs = [];
// Down in the script where you're processing things.
const a = document.querySelector('<ANCHOR_QUERY>');
const img = document.querySelector('<IMG_QUERY>');
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth; // `natural*` the actual dimensions of the image, not what it's currently sized to.
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob((blob) => {
const blobURL = URL.createObjectURL(blob);
window.blobURLs.push(blobURL);
a.href = blobURL;
}, 'image/jpeg', 1);
// For performance reasons you need to clean up the URLs once you're done with them, like if you have a SPA and clicking around loads different views, you'll need to remove these first.
if (window.blobURLs.length) {
while (window.blobURLs.length) {
const blobURL = window.blobURLs[window.blobURLs.length - 1];
URL.revokeObjectURL(blobURL);
window.blobURLs.pop();
console.log(`Revoked Blob URL: "${blobURL}"`);
}
}
The below is pretty flexible since the User can specify to the copyHandler
what should be copied. So if you want to copy text from a node you'd use textContent
, or value
if from an input/textarea. There's also some added code to aid in styling copy states.
const copyEl = document.querySelector('<SELECTOR>');
const copyStatusHandler = (el, state) => (err) => {
el.classList.add(state);
err && alert(err.stack);
setTimeout(() => { el.classList.remove(state); }, 2000);
};
const copyHandler = (dataFn) => (ev) => {
const el = ev.currentTarget;
const type = 'text/plain';
const blob = new Blob([dataFn()], { type });
const data = [new ClipboardItem({ [type]: blob })];
navigator.clipboard.write(data).then(
copyStatusHandler(el, 'success'),
copyStatusHandler(el, 'error')
);
}
navigator.permissions.query({ name: 'clipboard-write' }).then(({ state }) => {
if (state == 'granted' || state == 'prompt') {
copyEl.addEventListener('click', copyHandler(() => copyEl.textContent));
}
else {
alert('[WARN] Access to clipboard not enabled, disabling feature.');
}
});