An Idiosyncratic Blog

πŸ›  JavaScript Utils: Part One

Published on
β€’ 20 minutes read

Part One of the JavaScript Utils series where I share code snippets which can be directly used in your projects. All the snippets on this page are licensed under CC0-1.0, unless mentioned otherwise.

Get unique values in an array

const myArray = ['🍺', '🍺', '🍷', 'πŸ₯›', '🍷', 'πŸ₯€']

const unique = (array) => array.filter((val, index, arr) => arr.indexOf(val) === index)
// OR
const unique = (array) => Array.from(new Set(array))
// OR
const unique = (array) => [...new Set(numbers)]

console.log(unique(myArray)) // => [ "🍺", "🍷", "πŸ₯›", "πŸ₯€" ]

Remove Empty elements from an Array

const input = [0, 1, null, 2, '', 3, undefined, 3, , , , , , 4, , 4, , 5, , 6, , , ,]

const clean = (array) => array.filter(Boolean)

console.log(clean(input))

The typical pattern that I see often used is to remove elements that are falsy, which include an empty string "", 0, NaN, null, undefined, and false. Using the Boolean constructor function, we can simply return values which are truthy.

Copy Text to Clipboard 1

function fallbackCopyTextToClipboard(text) {
  const textArea = document.createElement('textarea')
  textArea.value = text

  // Avoid scrolling to bottom
  textArea.style.top = '0'
  textArea.style.left = '0'
  textArea.style.position = 'fixed'

  document.body.appendChild(textArea)
  textArea.focus()
  textArea.select()

  try {
    var successful = document.execCommand('copy')
    var msg = successful ? 'successful' : 'unsuccessful'
    console.log('Fallback: Copying text command was ' + msg)
  } catch (err) {
    console.error('Fallback: Oops, unable to copy', err)
  }

  document.body.removeChild(textArea)
}

function copyTextToClipboard(text) {
  if (!navigator.clipboard) {
    fallbackCopyTextToClipboard(text)
    return
  }
  navigator.clipboard.writeText(text).then(
    function () {
      console.log('Async: Copying to clipboard was successful!')
    },
    function (err) {
      console.error('Async: Could not copy text: ', err)
    }
  )
}

Bonus: Did you know you can use the copy method in the Developer tools console to quickly copy results of operations? Try it out!

Default Function Parameters

const getPage = (pageName = 'πŸ›  JavaScipt Utils: Part One', url = '/snippets/') => {
  return `${pageName} ${url}`
}

console.log(getPage()) // => πŸ›  JavaScipt Utils: Part One /snippets/

Required Function Parameters

const isRequired = () => {
  throw new Error('Missing a mandatory parameter.')
}

const getPage = (pageName = 'πŸ›  JavaScipt Utils: Part One', url = isRequired()) => {
  return `${pageName} ${url}`
}

console.log(getPage()) // => Missing a mandatory parameter.

In the above code, if we do not pass any value for url, it assumes a default value of the result of isRequired() method. Since isRequired throws and error as a return value, that's what we get. This is a simple extension of the Default Function Parameters trick.

Convert timestamp in MS to human-readable format

const formatMS = (ms) => {
  if (ms < 0) ms = -ms
  const time = {
    day: Math.floor(ms / 86400000),
    hour: Math.floor(ms / 3600000) % 24,
    minute: Math.floor(ms / 60000) % 60,
    second: Math.floor(ms / 1000) % 60,
    millisecond: Math.floor(ms) % 1000,
  }
  return (
    Object.entries(time)
      // keep only non-zero values.
      .filter((val) => val[1] !== 0)
      // create the string for each value
      .map(([key, val]) => `${val} ${key}${val !== 1 ? 's' : ''}`)
      // combine the values into a string.
      .join(', ')
  )
}

formatMS(1001) // => '1 second, 1 millisecond'
formatMS(Date.now()) // => '18785 days, 14 hours, 29 minutes, 36 seconds, 104 milliseconds'

Convert String to Slug

function slugify(string) {
  return string
    .toString()
    .trim()
    .toLowerCase()
    .replace(/\s+/g, '-')
    .replace(/[^\w\-]+/g, '')
    .replace(/\-\-+/g, '-')
    .replace(/^-+/, '')
    .replace(/-+$/, '')
}

console.log(slugify('abra cadabra πŸ’£')) // =>  abra-cadabra
console.log(slugify('abra!@#$% cadabra+_)(*) πŸ’£')) // =>  abra-cadabra
console.log(slugify('abra 123 cadabra πŸ’£')) // =>  abra-123-cadabra
console.log(slugify('abra 123/456/789 cadabra πŸ’£')) // => abra-123456789-cadabra

Remove given items from Array (Only primitive type)

function removeItemOnce(arr, value) {
  var index = arr.indexOf(value)
  if (index > -1) {
    arr.splice(index, 1)
  }
  return arr
}

function removeItemAll(arr, value) {
  var i = 0
  while (i < arr.length) {
    if (arr[i] === value) {
      arr.splice(i, 1)
    } else {
      ++i
    }
  }
  return arr
}

console.log(removeItemOnce(['🍺', '🍺', '🍷', 'πŸ₯›', '🍷', 'πŸ₯€'], '🍺')) // => [ "🍺", "🍷", "πŸ₯›", "🍷", "πŸ₯€" ]
console.log(removeItemAll(['🍺', '🍺', '🍷', 'πŸ₯›', '🍷', 'πŸ₯€'], '🍺')) // => [ "🍷", "πŸ₯›", "🍷", "πŸ₯€" ]

Extract text from webpage

const extract = (tag) => {
  const result = []
  const headings = document.querySelectorAll(tag)
  headings.forEach((heading) => {
    result.push({
      id: heading.id,
      text: heading.innerText,
    })
  })
  return result
}

If you run this snippet on this page extract('p'), you'll get all the text which are wrapped in <p></p> tag:

[
  {
    "id": "",
    "text": "Part One of the JavaScript Utils series where I share code snippets which can be directly used in your projects. All of the snippets on this page are licensed under CC0-1.0, unless mentioned otherwise."
  },
  {
    "id": "",
    "text": "The typical pattern that I see often used is to remove elements that are falsy, which include an empty string \"\", 0, NaN, null, undefined, and false. Using the Boolean constructor function, we can simply return values which are truthy."
  },
  {
    "id": "",
    "text": "Bonus: Did you know you can use the copy method in the Developer tools console to quickly copy results of operations? Try it out!"
  },
  {
    "id": "",
    "text": "In the above code, if we do not pass any value for url, it assumes a default value of the result of isRequired() method. Since isRequired throws and error as a return value, that’s what we get. This is a simple extension of the Default Function Parameters trick."
  },
  {
    "id": "",
    "text": "If you run this snippet on this page `extract('p')`, you'll get all the text which are wrapped in `<p></p>` tag:"
  },
  {
    "id": "",
    "text": "I find this pretty useful to quicky scrap a page without relying on external services or plugins."
  },
  {
    "id": "",
    "text": "This is kind of a long one, but a really useful utility. It will add additional properties to the Object using Proxy object. In this case, we are adding the array methods like map, reduce, forEach, filter, slice, find, findKey, includes , keyOf, lastKeyOf."
  },
  {
    "id": "",
    "text": "A Proxy object wraps another object and intercepts operations, like reading/writing properties and others, optionally handling them on its own, or transparently allowing the object to handle them."
  }
]

I find this pretty useful to quick scrape a page without relying on external services or plugins.

Array Methods on Objects2

This is kind of a long one, but a really useful utility. It will add additional properties to the Object using Proxy object. In this case, we are adding the array methods like map, reduce, forEach, filter, slice, find, findKey, includes , keyOf, lastKeyOf.

A Proxy object wraps another object and intercepts operations, like reading/writing properties and others, optionally handling them on its own, or transparently allowing the object to handle them.

const withArrayMethods = (obj) => {
  const methods = {
    map(target) {
      return (callback) => Object.keys(target).map((key) => callback(target[key], key, target))
    },
    reduce(target) {
      return (callback, accumulator) =>
        Object.keys(target).reduce(
          (acc, key) => callback(acc, target[key], key, target),
          accumulator
        )
    },
    forEach(target) {
      return (callback) => Object.keys(target).forEach((key) => callback(target[key], key, target))
    },
    filter(target) {
      return (callback) =>
        Object.keys(target).reduce((acc, key) => {
          if (callback(target[key], key, target)) acc[key] = target[key]
          return acc
        }, {})
    },
    slice(target) {
      return (start, end) => Object.values(target).slice(start, end)
    },
    find(target) {
      return (callback) => {
        return (Object.entries(target).find(([key, value]) => callback(value, key, target)) ||
          [])[0]
      }
    },
    findKey(target) {
      return (callback) => Object.keys(target).find((key) => callback(target[key], key, target))
    },
    includes(target) {
      return (val) => Object.values(target).includes(val)
    },
    keyOf(target) {
      return (value) => Object.keys(target).find((key) => target[key] === value) || null
    },
    lastKeyOf(target) {
      return (value) =>
        Object.keys(target)
          .reverse()
          .find((key) => target[key] === value) || null
    },
  }
  const methodKeys = Object.keys(methods)

  const handler = {
    get(target, prop, receiver) {
      if (methodKeys.includes(prop)) return methods[prop](...arguments)
      const [keys, values] = [Object.keys(target), Object.values(target)]
      if (prop === 'length') return keys.length
      if (prop === 'keys') return keys
      if (prop === 'values') return values
      if (prop === Symbol.iterator)
        return function* () {
          for (value of values) yield value
          return
        }
      else return Reflect.get(...arguments)
    },
  }

  return new Proxy(obj, handler)
}

Usage

import withArrayMethods from 'utils';

const object = withArrayMethods({
    'beer': '🍺',
    'whiskey': 'πŸ₯ƒ',
    'wine': '🍷',
    'soda': 'πŸ₯€'
  });

console.log(object['beer']); // => 🍺
console.log(object.keys); // =>  Array(4) [ "beer", "whiskey", "wine", "soda" ]
console.log(object.values); // => Array(4) [ "🍺", "πŸ₯ƒ", "🍷", "πŸ₯€" ]
console.log(...object); // => 🍺 πŸ₯ƒ 🍷 πŸ₯€

console.log(object.slice(0, 2)); // => Array [ "🍺", "πŸ₯ƒ" ]
console.log(object.slice(-1)); // => Array [ "πŸ₯€" ]

console.log(object.find((v, i) => v === 'πŸ₯€')); // => soda
console.log(object.findKey((v, i) => v === 'πŸ₯ƒ')); // => whiskey

console.log(object.includes('πŸ₯€')); // => true
console.log(object.includes('coke')); // => false

console.log(object.keyOf('🍷')); // => wine
console.log(object.keyOf('wine')); // => null
console.log(object.lastKeyOf('🍷')); // => wine

// Array methods
console.log(object.forEach((v, i) => console.log(`${i}: ${v}`));
// => 'beer': '🍺', 'whiskey': 'πŸ₯ƒ', 'wine': '🍷', 'soda': 'πŸ₯€'

console.log(object.map((v, i) => i + v));
// => Array(4) [ "beer🍺", "whiskeyπŸ₯ƒ", "wine🍷", "sodaπŸ₯€" ]

console.log(object.filter((v, i) => v !== 'πŸ₯€'));
// => Object { beer: "🍺", whiskey: "πŸ₯ƒ", wine: "🍷" }

console.log(object.reduce((a, v, i) => ({ ...a, [v]: i }), {}));
// => Object { "🍺": "beer", "πŸ₯ƒ": "whiskey", "🍷": "wine", "πŸ₯€": "soda" }

And that's it for the Part One of JavaScript Utils. Let me know which one did you found to be most useful.

Footnotes

  1. [https://stackoverflow.com/a/30810322] ↩

  2. https://github.com/30-seconds/30-seconds-blog/blob/master/blog_posts/javascript-object-array-proxy.md ↩