Mirror.js

A set of utilities to simplify the creation of interactive applications written in JavaScript

Mirror.js bundle now assembling

0b

Mount point

Point mirror at an object to populate, functions in the above bundle are added to that object. The name globalThis is equal to window when running in the browser

Code

globalThis.mirror = globalThis

cell

The cell is an abstraction that unifies the structure of an entire codebase. It takes inspiration from biology, there are many types of cells in a body, but they each descended from a common form. So too, when writing sofware, we may unify the format of the code we write in order to focus one's attention on the goal rather than style.

Thesis

The more predictable the structure of all code in a codebase, the easier on the mind it is to assemble complex structures from the various elements

Purpose

Reduce cognitive overhead of writing code

Code

mirror.cell = init => (me={}) =>
 Object.assign(me, init?.(me))
  .boot?.() ?? me

Example

const Counter = cell(me => ({
 value: 0,
 increment() {
  me.value++
 }
}))

const hits = Counter({ value: 20 })

hits.increment()
// hits.value is now 21

Cell gallery

Browse a gallery of example cells

AttributeMemory

The AttributeMemory cell is remembering the open attribute on all the <details> elements across page reloads by storing the state in local storage (try reloading, you won't have to expand this container again)

Code

mirror.AttributeMemory = cell(me => ({
 attribute: 'open',
 key: me.key ?? '#attributeMemory',
 get() {
  const data = localStorage.getItem(me.key)
  if (data) {
   return JSON.parse(data)
  }
  return {}
 },
 setFrom(element) {
  const value = element.getAttribute(me.attribute)
  localStorage.setItem(
   me.key,
   JSON.stringify(
    value === null
     ? {}
     : { [me.attribute]: value }
   )
  )
 },
 observeElement(element) {
  const observer =
   new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
     if (mutation.type === "attributes") {
      me.setFrom(element)
     }
    })
   })
  observer.observe(element, { attributes: true })
 },
 restoreTo(element) {
  for (const [k, v] of Object.entries(me.get())) {
   element.setAttribute(k, v)
  }
 }
}))

Example

const details = document.getElementsByTagName('details')
for (const d of Array.from(details)) {
 const summary =
  d.getElementsByTagName('summary')[0].innerText
 const attrMem = AttributeMemory({
  key: `openMemory#${summary}`
 })
 attrMem.observeElement(d)
 attrMem.restoreTo(d)
}
LineNumbers

The LineNumbers cell is what is creating the line numbers in all the code snippets on this page

Code

mirror.LineNumbers = cell(me => ({
 lines: 0,
 gutter: document.createElement('div'),
 async boot() {
  me.element.lineNumbers = me
  const lines = 1 +
   (me.element.innerText.match(/\n/g)?.length ?? 0)
  me.gutter.classList.add('gutter')
  me.element.parentElement.appendChild
   (me.gutter)
  await slowly(me.addLine)(Array(lines))
 },
 addLine() {
  const line = document.createElement('div')
  line.innerText = me.lines
  line.classList.add('line')
  me.gutter.appendChild(line)
  me.lines++
 }
}))
ScrollMemory

The ScrollMemory cell is remembering the scroll position across page reloads by saving the scroll offset in local storage (try reloading, you'll be right here again!)

Code

mirror.ScrollMemory = cell(me => ({
 key: '#scrollMemory.position',
 get() {
  const data = localStorage.getItem(me.key)
  if (data) {
   return JSON.parse(data)
  }
  return {}
 },
 setFrom(element) {
  return () => {
   const { scrollTop, scrollLeft } = element
   localStorage.setItem(
    me.key,
    JSON.stringify({ scrollTop, scrollLeft })
   )
  }
 },
 observeElement(element) {
  window.addEventListener(
   'scroll',
   me.setFrom(element)
  )
 },
 restorePosition(element) {
  Object.assign(element, me.get())
 }
}))

Example


const bodyScrollMem = ScrollMemory()
bodyScrollMem.observeElement(document.body)
bodyScrollMem.restorePosition(document.body)

css

Attach styles to HTML elements by automatically generating CSS class names

Purpose

Create CSS styles dynamically

Code

mirror.css = ([source]) => {
 const style = mirror.element('style')
 mirror.cssId = 1 + (mirror.cssId ?? 0)
 const className = `css${mirror.cssId}`
 style.innerText = source.includes('{')
  ? source.replace(/&/g, `.${className}`)
  : `.${className} { ${source} }`
 document.head.appendChild(style)
 return className
}

Example

const redBackground = css`
 background-color: red;
`

document.body.classList.add(redBackground)
// body element now has red background

Example with hover

const buttonStyle = css`
 & { background-color: red; }
 &:hover { background-color: yellow; }
 &:active { background-color: blue; }
`

button.classList.add(buttonStyle)
// button element is red by default,
// yellow when hovered, and blue when clicked

element

Create a HTML element, optionally include attributes

Code

mirror.element = (tagName='article', attrs) => {
 const elem = document.createElement(tagName)
 if (attrs) {
  mirror.mapEntries(attrs)
   (elem.setAttribute.bind(elem))
 }
 return elem 
}

Example

const input = mirror.element('input', {
 placeholder: 'First name'
})

// input is created with placeholder 'First name'

events

events makes attaching event listeners easier on the eyes

Purpose

Add event listeners to DOM elements

Code

mirror.events = fns => element =>
 mirror.mapEntries(fns)
  (element.addEventListener.bind(element))

Example

events({
 click() {
  // do something on click
 },
 keydown(e) {
  // do something on key down
 }
})(element)

mapEntries

Map over all entries in an object, calling a function for each key-value pair

Purpose

Perform repeated actions with less code

Code

mirror.mapEntries = source => fn =>
 Object.entries(source).map(
  ([k, v]) => fn(k, v)
 )

slowly

Purpose

Perform an action over time

Code

mirror.slowly = action =>
 async (source, t=20) => {
  const clone = Array.from(source)
  for (const i in Array(clone.length).fill()) {
   await new Promise(r => setTimeout(r, t))
   await action(clone[i], i)
  }
 }

times

Purpose

To repeat an action a certain number of times

Code

mirror.times = n => fn =>
 Array(n).fill().map((_, i) => fn(i))

Example

const list = times(7)(i => i * 10)
// list is now [0, 10, 20, 30, 40, 50, 60]