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 = new Otty(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 = new UnitHandler(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. You dont have to use them though:
e.currentTarget.dataset.potatoes = this.element.querySelector('#potatoCount').dataset.potatoes
//into
e.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 = new UnitHandler(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)
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
These are SPA functions on Otty. For advanced functionality I suggest extending Otty, as everyone has different priorities. Not all functions listed.