We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Otterly JS is a frontend javascript framework built to be small, highly customizable and intuitive.
The core of otterly JS are units of code attached to HTML elements. We can then add event-handlers within that unit. This looks like:
<div data-unit="Logger">
<p data-on="click->log['Hi!']"> click me! </div>
</div>
But why stop there? Otterly offers much more. We have AJAX and form helpers, as well as Single Page Application functionality.
Otterly JS is motivated by my experience using stimulus JS. Some issues I had were:
let unit = element._unit
otterly js relys on previous knowlege of the following javascript information. If you dont know, mdn is the best reference for javascript.
Install otterly as an npm package by adding to it your package manager:
npm i @luketclancy/otterly
Here is an example entry setup for your JS with Otterly:
import {Otty, AfterDive, UnitHandler, Generic, Debug} from 'otterly'
//otterly stuff
startApp = function(){
let csrfSelector = 'meta[name="csrf-token"]'
let csrfSendAs = 'X-CSRF-Token'
let isDev = true
//Set up otty as a global variable named otty, along with various settings.
//AfterDive is set as the response handling class for the otty.dive method.
window.otty = Otty.init(isDev, AfterDive, csrfSelector, csrfSendAs)
//Set up units, events and shortcuts. Add your units to this list. Generic is set as the default the other units are built on
otty.unitHandler = UnitHandler.init(Generic, [Generic, Debug])
//Optional SPA navigation
otty.handleNavigation()
}
//double check correct and only script running
let version = 1
if(window.yourApp && window.yourApp.version != version){
window.location.reload()
} else if(!(window.yourApp)) {
window.yourApp = {version: version}
startApp()
}
the UnitHandler defines a couple useful shortcuts. In my opinion it makes things easier to read, and likely makes file sizes shorter. You dont have to use them though, they are purely optional!:
event.currentTarget.dataset.potatoes = this.element.querySelector('#potatoCount').dataset.potatoes
//into
event.ct.ds.potatoes = this.el.qs('#potatoCount').ds.potatoes
Units underpin otterlyjs’s interaction with the DOM. This is the unit I used to syntax highlight this text block:
export default {
unitName: "Syntax",
onConnected: function(){
let lang = this.element.ds.language
let txt = this.element.innerText
let out = otty.highlighter.highlight(txt, {language: lang})
this.el.innerHTML = out.value
}
}
I then import it and add it to the UnitHandler in the setup
import {Syntax} from "./js/units/Syntax"
//setup otty.highligher here...
otty.unitHandler = UnitHandler.init(Generic, [Generic, Debug, Syntax])
I can invoke the unit in the html with the data-unit attribute
<code data-unit="Syntax" data-language='javascript'>
let a = "otterly"
</code>
In niche scenarios, if I add multiple unit names, the units will be merged into one. This is convenient, but troublesome from an Object Oriented view-point. In practice, this will rarely if ever be a problem.
<code data-unit="Logger Syntax" data-language='javascript'>
//equivilent to Object.assign({}, Generic, Logger, Syntax).
//I know 'Logger' doesn't use data-language, and Syntax only uses onConnected, so this should be fine.
</code>
Base Functions:
Overridable function for when unit is connected.
Overridable function for when unit is removed.
Function for when unit is connected, If there are multiple units assigned, each units’s onConnected function will be called in sequence
similar to onConnected but for when the unit is removed.
collects the dataset of the unit’s element aswell as the unit’s id (as unitId). If an event is provided, collects the currentTargets dataset and id as (submitterId)
an event function for AJAX. It is used to send the relevantData(e) above to a url. Optionally, it can also send inputs. It then accepts back JSON, which determines what actions it takes in the AfterDive class. For example:
<div data-unit='Generic' data-on="click->dive" data-url="/test"> </div>
and if one wanted to intercept a form to validate inputs server side before leaving the page…
<form data-unit='Generic' data-on="submit->dive[{\"withform\": true}]" data-url="/test">
<input type='hidden' name='otter_count' value=5/>
<button data-hit-button="true" type='submit'> OK! </button>
</form>
dive can then accept a json response like this from the server:
[{
"morph": {
"html": "<div id='test'><h1> HI! </h1></div>",
"id": "test"
}
}, {
"log": "tested!!"
}]
and act upon that json as defined in the AfterDive class. I intercept all my forms, as I do not want to lose input values before I leave the page. If the creation/update was successfull, I return this:
{"redirect": "object path here..."}
The event handling of otterlyjs. They are attached to an element like this:
<div data-unit='Generic' data-on="click->dive" data-url="/test">
the data-on string is made up of four parts, where two are optional.
bind arguments are good at quick functionality and options, but that data can not be changed in the html. Due to this, data attributes are often but not always prefered..
1 2 3 4
click->PostCard#comment[{"withform": true}]
1 3
click->comment
Here is a rough idea of how this works, with ‘element’ being and element with a data-on attribute
//click->dive
let unit = element.closest('[data-unit]')._unit
let dive = unit.dive.bind(unit)
element.addEventListener('click', dive )
//click->dive[{input: true}]
let unit = element.closest('[data-unit]')._unit
let dive = unit.dive.bind(unit, {input: true})
element.addEventListener('click', dive)
//click->PostCard#dive[{input: true}]
let unit = element.closest("[data-unit~='PostCard']")._unit
let dive = unit.dive.bind(unit, {input: true})
element.addEventListener('click', dive)
There are two special event values:
I use the following in a recovery step in midflip to check if they have clicked an email
link yet.
data-on='_parse->dive[{"behavior": "repeat"}]'
Navigate to the unit’s dive function above to see how to trigger a dive.
AfterDive is a class containing functions defining how to treat json that is returned from a dive. Feel free to add your own functionality.
let x = Function("data", "selector", 'baseElement', 'submitter', `"use strict"; ${data['code']};`)(data, selector, this.baseElement, this.submitter)
otty is the global class instance that we assigned to the widow during our setup. here are some methods on it:
//assume hostname = 'a.b.c'
isLocalUrl('d.b.c') // true
isLocalUrl('d.b.c', -3) //false
isLocalUrl('c', -1) //true
subscribeToPoll should autherize the user for the queues provided. subscribeToPoll uses an otty dive. The otty dive should return an encrypted string that contains authentication and queue information. This information will be passed to the poll method as the ‘otty-store’ field. Note that only one polling subscription can exist at a time, but you can add multiple queues to each subscription.
{returning: "no_updates"}
{returning: "should_resub"}
[{morph: {...},}, {returning: "[YOUR ENCRYPTED STORE STRING]"}]
If one is having issues with users getting repeat dive commands by doing an action, and then having a poll re-do the action, you can add a dive_id to that action to make sure it does not repeat: {morph: {…}, dive_id: 8470175}.
Otty polling requires some backend work. Here is a partial example in ruby.
These are the SPA functions on Otty.
options:
- navigationReplaces: a fallback style querySelector list for what document navigation replaces. Default = ['body]. For example:
- ['.content', 'body'] will replace the div with the 'content' class if available on both the current document and the new document we navigated to.
- [['.content', '.navbar'], 'body'] will replace both the content area and the navbar area. Unless one of them is missing, in which case it will fall back to the body.
options:
- reload: stop otty from pushing to the history stack. Defaults to false
<a href="/page" data-native-href> go to page </a>