Skip to content
/ surreal Public

πŸ—Ώ Mini jQuery alternative. Dependency-free animations. Locality of Behavior. Use one element or arrays transparently. Pairs with htmx. Vanilla querySelector() but better!

License

Notifications You must be signed in to change notification settings

gnat/surreal

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ—Ώ Surreal

Tiny jQuery alternative for plain Javascript with inline Locality of Behavior!

cover (Art by shahabalizadeh)

Why does this exist?

For devs who love ergonomics! You may appreciate Surreal if:

  • You want to stay as close as possible to Vanilla JS.
  • Hate typing document.querySelector over.. and over..
  • Hate typing addEventListener over.. and over..
  • Really wish document.querySelectorAll had Array functions..
  • Really wish this would work in any inline <script> tag
  • Enjoyed using jQuery selector syntax.
  • Animations, timelines, tweens with no extra libraries.
  • Only 320 lines. No build step. No dependencies.
  • Pairs well with htmx
  • Want fewer layers, less complexity. Are aware of the cargo cult. ✈️

✨ What does it add to Javascript?

  • ⚑️ Locality of Behavior (LoB) Use me() inside <script>
    • No .class or #id needed! Get an element without creating a unique name.
    • this but much more flexible!
    • Want me in your CSS <style> tags, too? See our companion script
  • πŸ”— Call chaining, jQuery style.
  • ♻️ Functions work seamlessly on 1 element or arrays of elements!
    • All functions can use: me(), any(), NodeList, HTMLElement (..or arrays of these!)
    • Get 1 element: me()
    • ..or many elements: any()
    • me() or any() can chain with any Surreal function.
      • me() can be used directly as a single element (like querySelector() or $())
      • any() can use: for / forEach / filter / map (like querySelectorAll() or $())
  • πŸŒ— No forced style. Use: classAdd or class_add or addClass or add_class
    • Use camelCase (Javascript) or snake_case (Python, Rust, PHP, Ruby, SQL, CSS).

πŸ€” Why use me() / any() instead of $()

  • πŸ’‘ Solves the classic jQuery bloat problem: Am I getting 1 element or an array of elements?
    • me() is guaranteed to return 1 element (or first found, or null).
    • any() is guaranteed to return an array (or empty array).
    • No more checks = write less code. Bonus: Reads more like self-documenting english.

πŸ‘οΈ How does it look?

Do surreal things with Locality of Behavior like:

<label for="file-input" >
  <div class="uploader"></div>
  <script>
    me().on("dragover", ev => { halt(ev); me(ev).classAdd('.hover'); console.log("Files in drop zone.") })
    me().on("dragleave", ev => { halt(ev); me(ev).classRemove('.hover'); console.log("Files left drop zone.") })
    me().on("drop", ev => { halt(ev); me(ev).classRemove('.hover').classAdd('.loading'); me('#file-input').attribute('files', ev.dataTransfer.files); me('#form').send('change') })
  </script>
</label>

See the Live Example! Then view source.

🎁 Install

Surreal is only 320 lines. No build step. No dependencies.

πŸ“₯ Download into your project, and add <script src="/surreal.js"></script> in your <head>

Or, 🌐 via CDN: <script src="https://cdn.jsdelivr.net/gh/gnat/surreal@main/surreal.js"></script>

⚑ Usage

πŸ”οΈ DOM Selection

  • Select one element: me(...)
    • Can be any of:
      • CSS selector: ".button", "#header", "h1", "body > .block"
      • Variables: body, e, some_element
      • Events: event.currentTarget will be used.
      • Surreal selectors: me(),any()
      • Choose the start location in the DOM with the 2nd arg. (Default: document)
        • πŸ”₯ any('button', me('#header')).classAdd('red')
          • Add .red to any <button> inside of #header
    • me() ⭐ Get parent element of <script> without a .class or #id !
    • me("body") Gets <body>
    • me(".button") Gets the first <div class="button">...</div>. To get all of them use any()
  • Select one or more elements as an array: any(...)
    • Like me() but guaranteed to return an array (or empty array).
    • any(".foo") ⭐ Get all matching elements.
    • Convert between arrays of elements and single elements: any(me()), me(any(".something"))

πŸ”₯ DOM Functions

  • ♻️ All functions work on single elements or arrays of elements.
  • πŸ”— Start a chain using me() and any()
    • 🟒 Style A me().classAdd('red') ⭐ Chain style. Recommended!
    • 🟠 Style B: classAdd(me(), 'red')
  • 🌐 Global conveniences help you write less code.
    • globalsAdd() will automatically warn you of any clobbering issues!
    • πŸ’€πŸ©Έ If you want no conveniences, or are a masochist, delete globalsAdd()
      • 🟒 me().classAdd('red') becomes surreal.me().classAdd('red')
      • 🟠 classAdd(me(), 'red') becomes surreal.classAdd(surreal.me(), 'red')

See: Quick Start and Reference and No Surreal Needed

⚑ Quick Start

  • Add a class
    • me().classAdd('red')
    • any("button").classAdd('red')
  • Events
    • me().on("click", ev => me(ev).fadeOut() )
    • any('button').on('click', ev => { me(ev).styles('color: red') })
  • Run functions over elements.
    • any('button').run(_ => { alert(_) })
  • Styles / CSS
    • me().styles('color: red')
    • me().styles({ 'color':'red', 'background':'blue' })
  • Attributes
    • me().attribute('active', true)

Timeline animations without any libraries.

<div>I change color every second.
  <script>
    // On click, animate something new every second.
    me().on("click", async ev => {
      let el = me(ev) // Save target because async will lose it.
      me(el).styles({ "transition": "background 1s" })
      await sleep(1000)
      me(el).styles({ "background": "red" })
      await sleep(1000)
      me(el).styles({ "background": "green" })
      await sleep(1000)
      me(el).styles({ "background": "blue" })
      await sleep(1000)
      me(el).styles({ "background": "none" })
      await sleep(1000)
      me(el).remove()
    })
  </script>
</div>
<div>I fade out and remove myself.
  <script>me().on("click", ev => { me(ev).fadeOut() })</script>
</div>
<div>Change color every second.
  <script>
    // Run immediately.
    (async (e = me()) => {
      me(e).styles({ "transition": "background 1s" })
      await sleep(1000)
      me(e).styles({ "background": "red" })
      await sleep(1000)
      me(e).styles({ "background": "green" })
      await sleep(1000)
      me(e).styles({ "background": "blue" })
      await sleep(1000)
      me(e).styles({ "background": "none" })
      await sleep(1000)
      me(e).remove()
    })()
  </script>
</div>
<script>
  // Run immediately, for every <button> globally!
  (async () => {
    any("button").fadeOut()
  })()
</script>

Array methods

any('button')?.forEach(...)
any('button')?.map(...)

πŸ‘οΈ Functions

Looking for DOM Selectors? Looking for stuff we recommend doing in vanilla JS?

🧭 Legend

  • πŸ”— Chainable off me() and any()
  • 🌐 Global shortcut.
  • πŸ”₯ Runnable example.
  • πŸ”Œ Built-in Plugin

πŸ‘οΈ At a glance

  • πŸ”— run
    • It's forEach but less wordy and works on single elements, too!
    • πŸ”₯ me().run(e => { alert(e) })
    • πŸ”₯ any('button').run(e => { alert(e) })
  • πŸ”— remove
    • πŸ”₯ me().remove()
    • πŸ”₯ any('button').remove()
  • πŸ”— classAdd πŸŒ— class_add πŸŒ— addClass πŸŒ— add_class
    • πŸ”₯ me().classAdd('active')
    • Leading . is optional
      • Same thing: me().classAdd('active') πŸŒ— me().classAdd('.active')
  • πŸ”— classRemove πŸŒ— class_remove πŸŒ— removeClass πŸŒ— remove_class
    • πŸ”₯ me().classRemove('active')
  • πŸ”— classToggle πŸŒ— class_toggle πŸŒ— toggleClass πŸŒ— toggle_class
    • πŸ”₯ me().classToggle('active')
  • πŸ”— styles
    • πŸ”₯ me().styles('color: red') Add style.
    • πŸ”₯ me().styles({ 'color':'red', 'background':'blue' }) Add multiple styles.
    • πŸ”₯ me().styles({ 'background':null }) Remove style.
  • πŸ”— attribute πŸŒ— attributes πŸŒ— attr
    • Get: πŸ”₯ me().attribute('data-x')
      • For single elements.
      • For many elements, wrap it in: any(...).run(...) or any(...).forEach(...)
    • Set: πŸ”₯me().attribute('data-x', true)
    • Set multiple: πŸ”₯ me().attribute({ 'data-x':'yes', 'data-y':'no' })
    • Remove: πŸ”₯ me().attribute('data-x', null)
    • Remove multiple: πŸ”₯ me().attribute({ 'data-x': null, 'data-y':null })
  • πŸ”— send πŸŒ— trigger
    • πŸ”₯ me().send('change')
    • πŸ”₯ me().send('change', {'data':'thing'})
    • Wraps dispatchEvent
  • πŸ”— on
    • πŸ”₯ me().on('click', ev => { me(ev).styles('background', 'red') })
    • Wraps addEventListener
  • πŸ”— off
    • πŸ”₯ me().off('click', fn)
    • Wraps removeEventListener
  • πŸ”— offAll
    • πŸ”₯ me().offAll()
  • πŸ”— disable
    • πŸ”₯ me().disable()
    • Easy alternative to off(). Disables click, key, submit events.
  • πŸ”— enable
    • πŸ”₯ me().enable()
    • Opposite of disable()
  • 🌐 createElement πŸŒ— create_element
  • 🌐 sleep
    • πŸ”₯ await sleep(1000, ev => { alert(ev) })
    • async version of setTimeout
    • Wonderful for animation timelines.
  • 🌐 halt
    • πŸ”₯ halt(event)
    • When recieving an event, stop propagation, and prevent default actions (such as form submit).
    • Wrapper for stopPropagation and preventDefault
  • 🌐 tick
    • πŸ”₯ await tick()
    • await version of rAF / requestAnimationFrame.
    • Waits for 1 frame (browser paint).
    • Useful to guarantee CSS properties are applied, and events have propagated.
  • 🌐 rAF
    • πŸ”₯ rAF(e => { return e })
    • Calls after 1 frame (browser paint). Alias of requestAnimationFrame
    • Useful to guarantee CSS properties are applied, and events have propagated.
  • 🌐 rIC
  • 🌐 onloadAdd πŸŒ— onload_add πŸŒ— addOnload πŸŒ— add_onload
    • πŸ”₯ onloadAdd(_ => { alert("loaded!"); })
    • πŸ”₯ <script>let e = me(); onloadAdd(_ => { me(e).on("click", ev => { alert("clicked") }) })</script>
    • Execute after the DOM is ready. Similar to jquery ready()
    • Add to window.onload while preventing overwrites of window.onload and predictable loading!
    • Alternatives:
      • Skip missing elements using ?. example: me("video")?.requestFullscreen()
      • Place <script> after the loaded element.
        • See me('-') / me('prev')
  • πŸ”Œ fadeOut
    • See below
  • πŸ”Œ fadeIn
    • See below

πŸ”Œ Built-in Plugins

Effects

Build effects with me().styles({...}) with timelines using CSS transitioned await or callbacks.

Common effects included:

  • πŸ”— fadeOut πŸŒ— fade_out

    • Fade out and remove element.
    • Keep element with remove=false.
    • πŸ”₯ me().fadeOut()
    • πŸ”₯ me().fadeOut(ev => { alert("Faded out!") }, 3000) Over 3 seconds then call function.
  • πŸ”— fadeIn πŸŒ— fade_in

    • Fade in existing element which has opacity: 0
    • πŸ”₯ me().fadeIn()
    • πŸ”₯ me().fadeIn(ev => { alert("Faded in!") }, 3000) Over 3 seconds then call function.

βšͺ No Surreal Needed

More often than not, Vanilla JS is the easiest way!

Logging

  • πŸ”₯ console.log() console.warn() console.error()
  • Event logging: πŸ”₯ monitorEvents(me()) See: Chrome Blog

Benchmarking / Time It!

  • πŸ”₯ console.time('name')
  • πŸ”₯ console.timeEnd('name')

Text / HTML Content

  • πŸ”₯ me().textContent = "hello world"
    • XSS Safe! See: MDN
  • πŸ”₯ me().innerHTML = "<p>hello world</p>"
  • πŸ”₯ me().innerText = "hello world"

Children

  • πŸ”₯ me().children
  • πŸ”₯ me().children.hidden = true

Append / Prepend elements.

  • πŸ”₯ me().prepend(new_element)
  • πŸ”₯ me().appendChild(new_element)
  • πŸ”₯ me().insertBefore(element, other_element.firstChild)
  • πŸ”₯ me().insertAdjacentHTML("beforebegin", new_element)

AJAX (replace jQuery ajax())

me().on("click", async event => {
  let e = me(event)
  // EXAMPLE 1: Hit an endpoint.
  if((await fetch("/webhook")).ok) console.log("Did the thing.")
  // EXAMPLE 2: Get content and replace me()
  try {
    let response = await fetch('/endpoint')
    if (response.ok) e.innerHTML = await response.text()
    else console.warn('fetch(): Bad response')
  }
  catch (error) { console.warn(`fetch(): ${error}`) }
})
  • Example using XMLHttpRequest()
me().on("click", async event => {
  let e = me(event)
  // EXAMPLE 1: Hit an endpoint.
  var xhr = new XMLHttpRequest()
  xhr.open("GET", "/webhook")
  xhr.send()
  // EXAMPLE 2: Get content and replace me()
  var xhr = new XMLHttpRequest()
  xhr.open("GET", "/endpoint")
  xhr.onreadystatechange = () => {
    if (xhr.readyState == 4 && xhr.status >= 200 && xhr.status < 300) e.innerHTML = xhr.responseText
  }
  xhr.send()
})

πŸ’Ž Conventions & Tips

  • Many ideas can be done in HTML / CSS (ex: dropdowns)
  • _ = for temporary or unused variables. Keep it short and sweet!
  • e, el, elt = element
  • e, ev, evt = event
  • f, fn = function

Scope functions and variables inside <script>

  • ⭐ Use a block { let note = "hi"; function hey(text) { alert(text) }; me().on('click', ev => { hey(note) }) }
    • let and function is scoped within { }
  • ⭐ Use me()
    • me().hey = (text) => { alert(text) }
    • me().on('click', (ev) => { me(ev).hey("hi") })
  • ⭐ Use an event me().on('click', ev => { /* add and call function here */ })
  • Use an inline module: <script type="module">
    • Note: me() in modules will not see parentElement, explicit selectors are required: me(".mybutton")

Select a void element like <input type="text" />

  • Use: me('-') or me('prev') or me('previous')
    • πŸ”₯ <input type="text" /> <script>me('-').value = "hello"</script>
    • Inspired by the CSS "next sibling" combinator + but in reverse -
  • Or, use a relative start.
    • πŸ”₯ <form> <input type="text" n1 /> <script>me('[n1]', me()).value = "hello"</script> </form>

Ignore call chain when element is missing.

  • πŸ”₯ me("#i_dont_exist")?.classAdd('active')
  • No warnings: πŸ”₯ me("#i_dont_exist", document, false)?.classAdd('active')

πŸ”Œ Your own plugin

Feel free to edit Surreal directly- but if you prefer, you can use plugins to effortlessly merge with new versions.

function pluginHello(e) {
  function hello(e, name="World") {
    console.log(`Hello ${name} from ${e}`)
    return e // Make chainable.
  }
  // Add sugar
  e.hello = (name) => { return hello(e, name) }
}

surreal.plugins.push(pluginHello)

Now use your function like: me().hello("Internet")

  • See the included pluginEffects for a more comprehensive example.
  • Your functions are added globally by globalsAdd() If you do not want this, add it to the restricted list.
  • Refer to an existing function to see how to make yours work with 1 or many elements.

Make an issue or pull request if you think people would like to use it! If it's useful enough we'll want it in core.

⭐ Awesome Surreal examples, plugins, and resources: awesome-surreal !

πŸ“šοΈ Inspired by

  • jQuery for the chainable syntax we all love.
  • BlingBling.js for modern minimalism.
  • Bliss.js for a focus on single elements and extensibility.
  • Hyperscript for Locality of Behavior and awesome ergonomics.
  • Shout out to Umbrella, Cash, Zepto- Not quite as ergonomic. Requires build step to extend.

🌘 Future