# 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.