๐ฟ Mini jQuery alternative. Dependency-free animations. Locality of Behavior. Use one element or arrays transparently. Pairs with htmx. Vanilla querySelector() but better!
npm install surreal๐ฟ Surreal
Tiny jQuery alternative for plain Javascript with inline Locality of Behavior!
(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.querySelectorover.. and over.. - Hate typing
addEventListenerover.. and over.. - Really wish
document.querySelectorAllhad Array functions.. - Really wish
thiswould 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.
thisbut much more flexible!- Want
mein 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()orany()can chain with any Surreal function.me()can be used directly as a single element (likequerySelector()or$())any()can use:for/forEach/filter/map(likequerySelectorAll()or$())
- All functions can use:
- ๐ No forced style. Use:
classAddorclass_addoraddClassoradd_class- Use
camelCase(Javascript) orsnake_case(Python, Rust, PHP, Ruby, SQL, CSS).
- Use
๐ค 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(...)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 useany()
- Can also be any of:
- CSS selector:
".button","#header","h1","body > .block" - Variables:
body,e,some_element - Events:
event.currentTargetwill be used. - Surreal selectors:
me(),any()
- CSS selector:
- Choose a start location in the DOM with parameter 2. (Default:
document)any('button', me('#header')).classAdd('red')- Add
.redto any<button>inside of#header
- Add
- Select one or more elements as an array:
any(...)any(".foo")Get all matching elements.- Like
me()but guaranteed to return an array (or empty array). - Easily 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()andany()- ๐ข Style A:
me().classAdd('red')โญ Recommended! Chain style. - ๐ต Style B:
classAdd(me(), 'red')
- ๐ข Style A:
- Global conveniences help you write less code.
globalsAdd()will automatically warn you of any clobbering issues.- If you do not want global conveniences, delete
globalsAdd()- ๐ Style C:
surreal.me().classAdd('red') - ๐ก Style D:
surreal.classAdd(surreal.me(), 'red')
- ๐ Style C:
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()andany() - ๐ Global
- ๐ Alias
- ๐ Built-in Plugin
๐๏ธ At a glance
- ๐
run- It's
forEachbut less wordy and works on single elements, too! me().run(e => { alert(e) })any('button').run(e => { alert(e) })
- It's
- ๐
removeme().remove()any('button').remove()
- ๐
classAdd๐class_add๐addClass๐add_classme().classAdd('active')- Leading
.is optional- Same thing:
me().classAdd('active')๐me().classAdd('.active')
- Same thing:
- ๐
classRemove๐class_remove๐removeClass๐remove_classme().classRemove('active')
- ๐
classToggle๐class_toggle๐toggleClass๐toggle_classme().classToggle('active')
- ๐
stylesme().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(...)orany(...).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 })
- Get:
- ๐
send๐triggerme().send('change')me().send('change', {'data':'thing'})- Wraps
dispatchEvent
- ๐
onme().on('click', ev => { me(ev).styles('background', 'red') })- Wraps
addEventListener
- ๐
offme().off('click', fn)- Wraps
removeEventListener
- ๐
offAllme().offAll()
- ๐
disableme().disable()- Easy alternative to
off(). Disables click, key, submit events.
- ๐
enableme().enable()- Opposite of
disable()
- ๐
createElement๐create_elemente_new = createElement("div"); me().prepend(e_new)- Alias of document.createElement
- ๐
sleepawait sleep(1000, ev => { alert(ev) })asyncversion ofsetTimeout- Wonderful for animation timelines.
- ๐
halthalt(event)- When recieving an event, stop propagation, and prevent default actions (such as form submit).
- Wrapper for stopPropagation and preventDefault
- ๐
tickawait tick()awaitversion ofrAF/requestAnimationFrame.- Waits for 1 frame (browser paint).
- Useful to guarantee CSS properties are applied, and events have propagated.
- ๐
rAFrAF(e => { return e })- Calls after 1 frame (browser paint). Alias of requestAnimationFrame
- Useful to guarantee CSS properties are applied, and events have propagated.
- ๐
rICrIC(e => { return e })- Calls when Javascript is idle. Alias of requestIdleCallback
- ๐
onloadAdd๐onload_add๐addOnload๐add_onloadonloadAdd(_ => { 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.onloadwhile preventing overwrites ofwindow.onloadand predictable loading! - Alternatives:
- Skip missing elements using
?.example:me("video")?.requestFullscreen() - Place
<script>after the loaded element.- See
me('-')/me('prev')
- See
- Skip missing elements using
- ๐
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.
- Fade in existing element which has
โช No Surreal Needed
More often than not, Vanilla JS is the easiest way!
Logging
console.log()console.warn()console.error()monitorEvents(me())- Event logging. See: Chrome Blog
Benchmarking
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"
Child Elements
me().childrenme().children.hidden = true
Appending / Prepending Elements
me().prepend(new_element)me().appendChild(new_element)me().insertBefore(element, other_element.firstChild)me().insertAdjacentHTML("beforebegin", new_element)
๐ AJAX (replace jQuery ajax())
- Lightest, vanilla javascript: fetch() or XMLHttpRequest() (see examples below).
- Or, alternatives, light: fixi or htmx or htmz or triptych. Heavy: turbo or unpoly
- Example using
fetch()
me().on("click", async event => {
let e = me(event)
// EXAMPLE 1: Hit /thing endpoint.
if((await fetch("/thing")).ok) console.log("Got thing.")
// EXAMPLE 2: Get /thing endpoint and replace me()
try {
let response = await fetch('/thing')
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", event => {
let e = me(event)
// EXAMPLE 1: Hit /thing endpoint.
var xhr = new XMLHttpRequest()
xhr.open("GET", "/thing")
xhr.send()
// EXAMPLE 2: Get /thing endpoint and replace me()
var xhr = new XMLHttpRequest()
xhr.open("GET", "/thing")
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= elemente,ev,evt= eventf,fn= function
Scoping
- โญ Scope with block
{...}{ let note = "hi"; function hey(text) { alert(text) }; me().on('click', ev => { hey(note) }) }letandfunctionare scoped within{ }
- โญ Scope with
me()blockme().hey = (text) => { alert(text) }me().on('click', (ev) => { me(ev).hey("hi") })
- โญ Scope with event block
me().on('click', event => { /* ... */ })
- Scope with inline module
<script type="module">- Warning:
me()will not seeparentElement. Explicit selector required:me(".mybutton")
Selecting void elements (<input type="text" />)
- Use:
me('-')orme('prev')orme('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>
Ignoring missing elements (Null safety)
me("#i_dont_exist")?.classAdd('active')- Silent 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
pluginEffectsfor a more comprehensive example. - Your functions are added globally by
globalsAdd()If you do not want this, add it to therestrictedlist. - 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.
๐ Change Log
1.3.4
- New automated official NPM (https://www.npmjs.com/package/@geenat/surreal)
- New automated test system.
- This repo is actually smaller now because the github-only automations generate their own support files for testing and publishing.
- Fixed warning about
document.pluginshttps://github.com/gnat/surreal/issues/52
๐๏ธ Inspired by
- jQuery for the chainable syntax we all love.
- Hyperscript for Locality of Behavior and ergonomics.
- BlingBling.js for modern minimalism.
- Bliss.js for a focus on single elements and extensibility.
- Shout out to Umbrella, Cash, Zepto