Re-inventing the wheel with a small DOM helper script

This is an itch I find that needs scratching every now and then — a distraction — and a good exercise to re-familiarise myself with the some of vanilla js’s dom methods. classList’s toggle force parameter would be case in point

I have basically carried on from the small classList script I wrote the other day on here.

A script to handle a few of the more common tasks e.g. toggling classes and adding events in a jquery like fashion — if that’s your thing?

Javascript

// helper functions
import { forEachable, flattenAll } from './helpers/array-helpers.js'

/**
 * array factory function
 * @param {HTMLElements, nodeList, element} elements
 * @returns {Object} array methods forEach etc
 */
const arrayMethods = (elements) => {
  const nodes = forEachable(elements)

  return {
    forEach: function (fn, context) {
      nodes.forEach(fn, context)
      return this
    }
  }
}

/**
 * classlist transform factory function
 * @param {HTMLElements, nodeList, element} elements
 * @returns {Object} classList methods add, remove, toggle
 */
const classTransforms = (elements) => {
  const nodes = forEachable(elements)

  // return a callback for nodes.forEach
  const classListTransform = (method, classNames) =>
    (elem) => elem.classList[method](...classNames)

  return {
    addClass: function (...classNames) {
      nodes.forEach(classListTransform('add', classNames))
      return this
    },

    removeClass: function (...classNames) {
      nodes.forEach(classListTransform('remove', classNames))
      return this
    },

    toggleClass: function (className, force) {
      nodes.forEach(
        (elem) => elem.classList.toggle(className, force)
      )
      return this
    }
  }
}

/**
 * listeners factory function
 * @param {HTMLElements, nodeList, element} elements
 * @returns {Object} eventListener methods add and remove
 */
const listeners = (elements) => {
  const nodes = forEachable(elements)

  return {
    addEvent: function (type, fn, optional) {
      nodes.forEach(
        element => element.addEventListener(type, fn, optional)
      )
      return this
    },

    removeEvent: function (type, fn, optional) {
      nodes.forEach(
        element => element.removeEventListener(type, fn, optional)
      )
      return this
    }
  }
}

/**
 * Selector factory function
 * @param {String, HTMLElements, nodeList or an element}
 * @returns {Object} with dom manipulation methods
 */
const QueryJS = (selection, root = document) => {

  // single out #id selectors for querySelector
  const isId = (selector) => /^#\S+$/.test(selector.trim())

  const nodes = flattenAll([
    (typeof selection === 'string')
      // a string selector
      ? (isId(selection))
          ? root.querySelector(selection)
          : root.querySelectorAll(selection)
      // or an HTMLCollection, nodeList, an element
      : selection
  ])

  // return mixin
  return {
    ...arrayMethods(nodes),
    ...classTransforms(nodes),
    ...listeners(nodes),

    get nodes () { return nodes }, // return array of all nodes
    get node () { return nodes[0] } // return first node
  }
}

export { QueryJS as default, classTransforms, listeners }

Chose not go down the prototype route, instead opting for mixins and modules which can be imported and used in their own right. Edit: Got a feeling that may backfire :slight_smile:

A test script, based on a couple of the posts/assignments on here.

import queryJS, { listeners } from './js/queryJS.js'

queryJS(window).addEvent('DOMContentLoaded', () => {

  const options = {
    root: queryJS('.wrapper').node,
    rootMargin: '300px',
    threshold: .5
  }

  const intersecting = (entries, observer) => {
    for (const { target, isIntersecting } of entries) {

      queryJS(`#nav-tab-${target.dataset.section}`)
        .toggleClass('active', isIntersecting)
    }
  }

  const observer = new IntersectionObserver(intersecting, options)
  const observe = elem => observer.observe(elem)

  queryJS('section').forEach(observe)

  // Scroll into View:
  //
  // A test here using the listeners module without QueryJS and passing in a nodeList.
  listeners(document.querySelectorAll('.tab-menu a'))
    .addEvent('click', (event) => {
      event.preventDefault()

      const { tab } = event.target.dataset
      const options = { behavior: 'smooth', block: 'center' }

      queryJS(`#section-${tab}`).node.scrollIntoView(options)
    })
})

Just a bit of fun : )

2 Likes

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.