base on 🗿 Mini jQuery alternative. Dependency-free animations. Locality of Behavior. Use one element or arrays transparently. Pairs with htmx. Vanilla querySelector() but better! # 🗿 Surreal ### Tiny jQuery alternative for plain Javascript with inline [Locality of Behavior](https://htmx.org/essays/locality-of-behaviour/)! ![cover](https://user-images.githubusercontent.com/24665/171092805-b41286b2-be4a-4aab-9ee6-d604699cc507.png) (Art by [shahabalizadeh](https://www.deviantart.com/shahabalizadeh)) <!-- <a href="https://github.com/gnat/surreal/archive/refs/heads/main.zip"><img src="https://img.shields.io/badge/Download%20.zip-ff9800?style=for-the-badge&color=%234400e5" alt="Download badge" /></a> <a href="https://github.com/gnat/surreal"><img src="https://img.shields.io/github/workflow/status/gnat/surreal/ci?label=ci&style=for-the-badge&color=%237d91ce" alt="CI build badge" /></a> <a href="https://github.com/gnat/surreal/releases"><img src="https://img.shields.io/github/workflow/status/gnat/surreal/release?label=Mini&style=for-the-badge&color=%237d91ce" alt="Mini build badge" /></a> <a href="https://github.com/gnat/surreal/blob/main/LICENSE"><img src="https://img.shields.io/github/license/gnat/surreal?style=for-the-badge&color=%234400e5" alt="License badge" /></a>--> ## 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](#-quick-start) with no extra libraries. * Only 320 lines. No build step. No dependencies. * Pairs well with [htmx](https://htmx.org) * Want fewer layers, less complexity. Are aware of the cargo cult. ✈️ ## ✨ What does it add to Javascript? * ⚡️ [Locality of Behavior (LoB)](https://htmx.org/essays/locality-of-behaviour/) 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](https://github.com/gnat/css-scope-inline) * 🔗 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](https://htmx.org/essays/locality-of-behaviour/) like: ```html <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](https://gnat.github.io/surreal/example.html)! Then [view source](https://github.com/gnat/surreal/blob/main/example.html). ## 🎁 Install Surreal is only 320 lines. No build step. No dependencies. [📥 Download](https://raw.githubusercontent.com/gnat/surreal/main/surreal.js) 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 ### <a name="selectors"></a>🔍️ 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](#quick-start) and [Reference](#reference) and [No Surreal Needed](#no-surreal) ## <a name="quick-start"></a>⚡ 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)` <a name="timelines"></a> #### Timeline animations without any libraries. ```html <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> ``` ```html <div>I fade out and remove myself. <script>me().on("click", ev => { me(ev).fadeOut() })</script> </div> ``` ```html <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> ``` ```html <script> // Run immediately, for every <button> globally! (async () => { any("button").fadeOut() })() </script> ``` #### Array methods ```js any('button')?.forEach(...) any('button')?.map(...) ``` ## <a name="reference"></a>👁️ Functions Looking for [DOM Selectors](#selectors)? Looking for stuff [we recommend doing in vanilla JS](#no-surreal)? ### 🧭 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` * 🔥 `e_new = createElement("div"); me().prepend(e_new)` * Alias of [document.createElement](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement) * 🌐 `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](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation) and [preventDefault](https://developer.mozilla.org/en-US/docs/Web/API/Event/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](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) * Useful to guarantee CSS properties are applied, and events have propagated. * 🌐 `rIC` * 🔥 `rIC(e => { return e })` * Calls when Javascript is idle. Alias of [requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) * 🌐 `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 ### <a name="plugin-included"></a>🔌 Built-in Plugins ### Effects Build effects with `me().styles({...})` with timelines using [CSS transitioned `await` or callbacks](#timelines). 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. ## <a name="no-surreal"></a>⚪ 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](https://developer.chrome.com/blog/quickly-monitor-events-from-the-console-panel-2/) Benchmarking / Time It! * 🔥 `console.time('name')` * 🔥 `console.timeEnd('name')` Text / HTML Content * 🔥 `me().textContent = "hello world"` * XSS Safe! See: [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) * 🔥 `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()`) * Use [htmx](https://htmx.org/) or [htmz](https://leanrada.com/htmz/) or [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) or [XMLHttpRequest()](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) * Example using `fetch()` ```js 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()` ```js 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')` ## <a name="plugins"></a>🔌 Your own plugin Feel free to edit Surreal directly- but if you prefer, you can use plugins to effortlessly merge with new versions. ```javascript 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](https://github.com/gnat/surreal/issues) or [pull request](https://github.com/gnat/surreal/pulls) 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](https://github.com/gnat/awesome-surreal) ! ## 📚️ Inspired by * [jQuery](https://jquery.com/) for the chainable syntax we all love. * [BlingBling.js](https://github.com/argyleink/blingblingjs) for modern minimalism. * [Bliss.js](https://blissfuljs.com/) for a focus on single elements and extensibility. * [Hyperscript](https://hyperscript.org) for Locality of Behavior and awesome ergonomics. * Shout out to [Umbrella](https://umbrellajs.com/), [Cash](https://github.com/fabiospampinato/cash), [Zepto](https://zeptojs.com/)- Not quite as ergonomic. Requires build step to extend. ## 🌘 Future * Always more `example.html` goodies! * Automated browser testing perhaps with: * [Fava](https://github.com/fabiospampinato/fava). See: https://github.com/avajs/ava/issues/24#issuecomment-885949036 * [Ava](https://github.com/avajs/ava/blob/main/docs/recipes/browser-testing.md) * [jsdom](https://github.com/jsdom/jsdom) * [jsdom notes](https://github.com/jsdom/jsdom#executing-scripts) ", Assign "at most 3 tags" to the expected json: {"id":"3332","tags":[]} "only from the tags list I provide: [{"id":77,"name":"3d"},{"id":89,"name":"agent"},{"id":17,"name":"ai"},{"id":54,"name":"algorithm"},{"id":24,"name":"api"},{"id":44,"name":"authentication"},{"id":3,"name":"aws"},{"id":27,"name":"backend"},{"id":60,"name":"benchmark"},{"id":72,"name":"best-practices"},{"id":39,"name":"bitcoin"},{"id":37,"name":"blockchain"},{"id":1,"name":"blog"},{"id":45,"name":"bundler"},{"id":58,"name":"cache"},{"id":21,"name":"chat"},{"id":49,"name":"cicd"},{"id":4,"name":"cli"},{"id":64,"name":"cloud-native"},{"id":48,"name":"cms"},{"id":61,"name":"compiler"},{"id":68,"name":"containerization"},{"id":92,"name":"crm"},{"id":34,"name":"data"},{"id":47,"name":"database"},{"id":8,"name":"declarative-gui "},{"id":9,"name":"deploy-tool"},{"id":53,"name":"desktop-app"},{"id":6,"name":"dev-exp-lib"},{"id":59,"name":"dev-tool"},{"id":13,"name":"ecommerce"},{"id":26,"name":"editor"},{"id":66,"name":"emulator"},{"id":62,"name":"filesystem"},{"id":80,"name":"finance"},{"id":15,"name":"firmware"},{"id":73,"name":"for-fun"},{"id":2,"name":"framework"},{"id":11,"name":"frontend"},{"id":22,"name":"game"},{"id":81,"name":"game-engine "},{"id":23,"name":"graphql"},{"id":84,"name":"gui"},{"id":91,"name":"http"},{"id":5,"name":"http-client"},{"id":51,"name":"iac"},{"id":30,"name":"ide"},{"id":78,"name":"iot"},{"id":40,"name":"json"},{"id":83,"name":"julian"},{"id":38,"name":"k8s"},{"id":31,"name":"language"},{"id":10,"name":"learning-resource"},{"id":33,"name":"lib"},{"id":41,"name":"linter"},{"id":28,"name":"lms"},{"id":16,"name":"logging"},{"id":76,"name":"low-code"},{"id":90,"name":"message-queue"},{"id":42,"name":"mobile-app"},{"id":18,"name":"monitoring"},{"id":36,"name":"networking"},{"id":7,"name":"node-version"},{"id":55,"name":"nosql"},{"id":57,"name":"observability"},{"id":46,"name":"orm"},{"id":52,"name":"os"},{"id":14,"name":"parser"},{"id":74,"name":"react"},{"id":82,"name":"real-time"},{"id":56,"name":"robot"},{"id":65,"name":"runtime"},{"id":32,"name":"sdk"},{"id":71,"name":"search"},{"id":63,"name":"secrets"},{"id":25,"name":"security"},{"id":85,"name":"server"},{"id":86,"name":"serverless"},{"id":70,"name":"storage"},{"id":75,"name":"system-design"},{"id":79,"name":"terminal"},{"id":29,"name":"testing"},{"id":12,"name":"ui"},{"id":50,"name":"ux"},{"id":88,"name":"video"},{"id":20,"name":"web-app"},{"id":35,"name":"web-server"},{"id":43,"name":"webassembly"},{"id":69,"name":"workflow"},{"id":87,"name":"yaml"}]" returns me the "expected json"