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).classAdd('.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()
  • ๐ŸŒ sleep
    • โ–ถ๏ธ await sleep(1000, ev => { alert(ev) })
    • async version of setTimeout
    • Wonderful for animation timelines.
  • ๐ŸŒ tick
    • โ–ถ๏ธ await tick()
    • await version of rAF / requestAnimationFrame.
    • Animation tick. Waits 1 frame.
    • Great if you need to wait for events to propagate.
  • ๐ŸŒ rAF
    • โ–ถ๏ธ rAF(e => { return e })
    • Animation tick. Fires when 1 frame has passed. Alias of requestAnimationFrame
    • Great if you need to wait for events to propagate.
  • ๐ŸŒ rIC
    • โ–ถ๏ธ rIC(e => { return e })
    • Great time to compute. Fires function when JS is idle. Alias of requestIdleCallback
  • ๐ŸŒ halt
    • โ–ถ๏ธ halt(event)
    • Prevent default browser behaviors.
    • Wrapper for preventDefault
  • ๐ŸŒ createElement ๐ŸŒ— create_element
    • โ–ถ๏ธ e_new = createElement("div"); me().prepend(e_new)
    • Alias of vanilla document.createElement
  • ๐ŸŒ onloadAdd ๐ŸŒ— onload_add ๐ŸŒ— addOnload ๐ŸŒ— add_onload
    • โ–ถ๏ธ onloadAdd(_ => { alert("loaded!"); })
    • Execute after the DOM is ready. Similar to jquery ready()
    • Queues functions onto window.onload
    • Why? So you don't overwrite window.onload, also predictable sequential loading!
  • ๐Ÿ”Œ 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}`) }
})
  • 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 inside <script>

  • โญ On me()
    • me().hey = (text) => { alert(text) }
    • me().on('click', (ev) => { me(ev).hey("hi") })
  • โญ Use a block: { function hey(text) { alert(text) }; me().on('click', ev => { hey("hi") }) }
  • โญ Use an event: me().on('click', ev => { /* add and call function here */ })
  • Use an inline module: <script type="module">
    • Note: me() will no longer see parentElement so 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