UNPKG

@luketclancy/otterly

Version:

A javascript front end framework, Inspired by stimulus js. Like an otter its small, fast and versatile. Based around logical units attached to html nodes. Great for a backend-first approach to website creation.

260 lines (229 loc) 13.6 kB
# It's otterly intuitive 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: ```html <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. - We are in Version 0.1.3, the library works well and I use it regularly! You may run into some issues but its mostly stable. - The framework will stay small, understandable and hackable! This does mean Defining alternate behavior yourself when needed. ## Compared to stimulus js... Otterly JS is motivated by my experience using stimulus JS. Some issues I had were: 1. StimulusJS does not have AJAX or Single Page Application functionality. Which is fine - you can get it elsewhere or write your own. The otterly framework however, is meant to be small but fierce (like an otter. Get it? Otterly Javascript? No?) . We do that through having these aspects connected. 2. in Stimulus, elements can have many controllers, [and accessing them is... complicated](https://stimulus.hotwired.dev/reference/outlets). In otterly js there is only one "unit" (the controller analog) that you access like this: ```javascript let unit = element._unit ``` 3. StimulusJS has some "extra" features which I personally Dont like. Like [targets](https://stimulus.hotwired.dev/reference/targets) (just use a querySelector) or [values ](https://stimulus.hotwired.dev/reference/values) (just use data-attributes). In many ways stimulus js makes its life difficult by reimplementing existing functionality or worrying about complete seperation in the niche case in which an element has two controllers. ## Get Your Javascript Kung Fu on 🥋 otterly js relys on previous knowlege of the following javascript information. If you dont know, [mdn](https://developer.mozilla.org/en-US/) is the best reference for javascript. - [closest, querySelector and querySelectorAll](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) - [binds (I suggest using liberally)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind) - [events, currentTarget, addEventListener, etc](https://developer.mozilla.org/en-US/docs/Web/API/Event) - creating global variables by setting them to the [window](https://developer.mozilla.org/en-US/docs/Web/API/Window) - [data attributes and the dataset accessor](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes) - [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) ## Setup Install [otterly as an npm package](https://www.npmjs.com/package/@luketclancy/otterly) by adding to it your package manager: ```bash npm i @luketclancy/otterly ``` Here is an example entry setup for your JS with Otterly: ```javascript 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() } ``` ## Interacting with the DOM [(how does it work?)](https://github.com/LukeClancy/otterly/blob/main/unit_handler.js) ### Shortcuts the UnitHandler defines a couple useful shortcuts. In my opinion it makes things easier to read. You dont have to use them though: ```javascript e.currentTarget.dataset.potatoes = this.element.querySelector('#potatoCount').dataset.potatoes //into e.ct.ds.potatoes = this.el.qs('#potatoCount').ds.potatoes ``` - Events: - ct: currentTarget - HTMLElements: - ds: dataset - qs: querySelector - qsa: querySelectorAll - document element: - qs: querySelector - qsa: querySelectorAll - units: - el: element ### Units Units underpin otterlyjs's interaction with the DOM. This is the unit I used to syntax highlight this text block: ```javascript 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 ```javascript 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 ```html <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. - This can be used to assign an element multiple functionalities. - Beware of collisions and unexpected functionality. - the onConnected and onRemoved functions help avoid collisions as they are not overrode, but combined. - Consider for more complex scenarios: Importing unit functionality into a new unit, and using that instead. ```html <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 (AKA the AJAX function) 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: ```html <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... ```html <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: ```json [{ "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: ```json {"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 - the "withform" option, outsde of input elements, also replicates functionality of the following form attributes : "action", "formaction", "method", "formmethod" - if a "data-method", "method", or "formmethod" is not active, dive reverts to the "POST" method. #### untested functions - 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? ### Events, the Data-On Property: The event handling of otterlyjs. They are attached to an element like this: ```html <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. 1. the event name: what action will make up the event? 2. [optional] the unit name: what unit type the function belongs to. By default we assume the function belongs to the closest unit. 3. the function name: What function will the event listener call? 4. [optional] bind arguments (json): by adding brackets to the end of the function, otterly will parse those brackets as json, and add any elements as additional arguments to the function. See [arg1...argN here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind) 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 ```javascript //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) ``` ## AfterDive 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. - log: logs whatever is passed ex: {log: "hi!"} - reload: reloads the page ex: {reload: true} - redirect: redirects to a page using SPA functionality. Useful in form validation. ex: {redirect: "/"} - insert: inserts into the dom ex: {insert: {:html, [:position](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML), :selector or :id}} - morph: morphs an element ex: {morph: {:html, :selector or id, :permanent, :ignore}} - remove: removes an element ex: {remove: {:id or :selector}} - replace: replaces an element ex: {replace: {:html, :id or :selector, :childrenOnly}} - innerHtml: replaces innerHtml ex: {innerHtml: {:html, :selector or :id} } - eval: runs some code. ex: {eval: {:code, :data (json), :selector}}. This is what it actually does: ```javascript let x = Function("data", "selector", 'baseElement', 'submitter', `"use strict"; ${data['code']};`)(data, selector, this.baseElement, this.submitter) ``` - setData: sets data attributes on the base element: {setData: { ".card": {open: 'false'}}} ## Otty otty is the global class instance that we assigned to the widow during our setup. here are some methods on it: - obj_to_fd(formInfo, [optional] FormData base object): Converts formInfo to a FormData instance. Optionally adds on to pre-existing formData - sendsXHR(opts): Promise wrapped and abstracted xhr request. - dive(opts): the non-event based counterpart to the unit's dive function. Promise wrapped. - isLocalUrl(url, [optional] subdomainAccuracy = -2): returns true if the url is equal to / a subdomain of window.location.hostname. For instance: ```javascript //assume hostname = 'a.b.c' isLocalUrl('d.b.c') // true isLocalUrl('d.b.c', -3) //false isLocalUrl('c', -1) //true ``` - xss pass: determines what domains will cause a dive to throw an error (beyond the browser built in controls). Defaults to allowing only isLocalUrl(url, -2) - poll/subscribeToPoll: untested. ### SPA These are SPA functions on Otty. For advanced functionality I suggest extending Otty, as everyone has different priorities. Not all functions listed. - handleNavigation(opts): Enable navigation handling. opts.navigationReplaces is a list of querySelectors that have priority when switching DOM during page loads. For example, ['body'] will always switch the body while ['div#inner', 'body'] will replace some inner div if available before falling back to body if not. - goto(url, opts = {reload: false}): follow url using SPA functionality. Setting reload will stop otty from pushing to the history stack. - navigationHeadMorph(tmpDocHead): determines how to morph the head. - linkClickedF(e): link has been clicked function. Determines how to deal with it. You can, for instance, make it so you always use history.go() if the path was already hit.