Skip to content

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 340 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>
    • Get an element without creating a unique name: No .class or #id needed!
    • this but better!
    • 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 $()

  • ๐Ÿ’ก We solve the classic jQuery code 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 = you write less code. Bonus: Code 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 340 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 a start location in the DOM with the 2nd argument. Default is document
        • โ–ถ๏ธ any('button', me('#header')).classAdd('red')
          • Add .red to any <button> inside of #header
    • me() Get current element for Locality of Behavior in <script> without an explicit .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(...)
    • Similar to me() but guaranteed to return an array (or empty array).
    • any(".foo") Gets all matching elements, such as: <div class="foo">...</div>
    • Feel free to 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 about any clobbering issues.
      • If you prefer 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() )
    • on(any('button'), '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 for all class functions, and is removed automatically.
      • These are the same: 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')
      • Get is only for single elements. For many, wrap the call 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().remove('click')
    • 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)
    • Great to prevent default browser behavior: such as displaying an image vs letting JS handle it.
    • 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 (alternatives to jquery ajax())

  • First, check out htmx ..and if you need more control:
  • Using fetch() โ–ถ๏ธ
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}`) }
})
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().doIt = (message) => { alert(message) }
    • me().on('click', (ev) => { me(ev).doIt("hello") })
  • โญ Or, in an event: me().on('click', ev => { /* add and call function here */ })
  • Or, use an inline module: <script type="module">
    • Note: me() will no longer see parentElement so explicit selectors are required: me(".mybutton")
  • Or, use backend code to generate unique names for anything not scoped by me()

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

  • Use: me('-') or me('prev') or me('previous')
    • โ–ถ๏ธ <input type="text" /> <script>me('-').value = "hello"</script>
    • Shortcut for me(document.currentScript.previousElementSibling)
    • Inspired by the CSS sibling combinator + but in reverse -
  • Or, use a relative start.
    • โ–ถ๏ธ <input type="text" n1 /> <script>me('[n1]', me()).value = "hello"</script>

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 modify Surreal for a project any way you like- but you can use plugins to effortlessly merge functions 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)

You can now use it like: me().hello("Internet")

  • See the included pluginEffects for a more comprehensive example.
  • Your functions will be 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