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 events within that unit’s element. This looks like:
<div data-unit="Logger">
<p data-on="click->log['Hi!']"> click me! </div>
</div>
But 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.
Here is an example entry setup for your JS with Otterly:
import {Otty, AfterDive, UnitHandler, Generic, Debug} from 'otterly'
//otterly stuff
startApp = function(){
let csrf_selector = 'meta[name="csrf-token"]'
let csrf_send_as = 'X-CSRF-Token'
let is_dev = 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(is_dev, AfterDive, csrf_selector, csrf_send_as)
//Set up units, events and shortcuts. Add your units to this list.
otty.unitHandler = new UnitHandler([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()
}
Units underpin otterlyjs’s interaction with the DOM. This is the unit I used to syntax highlight this text block:
let Syntax = {
unitName: "Syntax",
onConnected: function(){
let lang = this.el.ds.language
let txt = this.el.innerText
// otty.highlighter is an object I set on initialization using https://highlightjs.org/
let out = otty.highlighter.highlight(txt, {language: lang})
this.el.innerHTML = out.value
}
}
export {Syntax}
I then import it and add it to the UnitHandler in the setup
import {Syntax} from "./js/units/Syntax"
otty.unitHandler = new UnitHandler([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 in an Object Oriented view-point.
<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:
unitConnected: Overridable function for when unit is connected.
unitRemoved: Overridable function for when unit is removed.
onConnected: Function for when unit is connected, If there are multiple units assigned, each units’s onConnected function will be called in sequence
onRemoved: similar to onConnected but for when the unit is removed.
relevantData(e): 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)
dive: 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[{inputs: 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..."}
dive will treat the following data variables specially: data-url, data-method, data-csrf-content, data-csrf-header, data-csrf-selector, data-confirm.
the following data-variables wont work as they are used internally: data-with-credentials, data-e, data-submitter, data-form-info, data-xhr-change-f, data-base-element
childUnits(unitc): Gets all child units, optionally with an identifier.
childUnitsDirect(unitc): Gets all direct child units, optionally with an identifier.
childUnitsFirstLayer(unitc): Gets all child units who dont have a unit between. Optionally with an identifier.
parentUnit(unitc): Gets the first pasrentUnit, optionally with an identifying name.
addUnitEvent/removeUnitEvent/unitEvents: internal event tracking for data-on functionality. Maybe usefull for debugging?
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.
prepend arguments are good at quick functionality and options, but that data can not be changed. Data attributes is prefered due to this.
1 2 3 4
click->PostCard#comment[{inputs: 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
skipping non-facing…