@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.
272 lines (257 loc) • 8.99 kB
JavaScript
let generic = {
unitName: "Generic",
unitRemoved(){
this.el._unit = undefined
},
unitConnected(){
},
unitEvents: [],
addUnitEvent(evInfo, actionNode) {
let action = evInfo.action
let f_name = evInfo.f_name
let input = evInfo.input
let f = this[f_name]
if(!f){console.error(`Could not find function ${f} on unit: `, this, "data-on defined on: ", actionNode); return}
let f3
let dis = this
if(input.length > 0){
//we want the function to look like this x(event, input1, input2, ...)
//but by binding before the event listener it ends up looking like (input 1, ..., event)
f3 = (event) => f.bind(dis)(event, ...input)
} else {
f3 = (event) => f.bind(dis)(event)
}
let f_str = JSON.stringify(evInfo)
if(action == "_remove"){
this.unitEvents.push({actionNode, action, f: f3, f_str, f_name})
} else if(action == "_parse"){
actionNode.addEventListener(action, f3)
actionNode.dispatchEvent(new Event(action))
actionNode.removeEventListener(action, f3)
} else {
actionNode.addEventListener(action, f3)
this.unitEvents.push({actionNode, action, f: f3, f_str, f_name})
}
},
removeUnitEvent(evInfo, actionNode){ //obj, nm, f, f_str) {
//find the action based on the information provided
let f_str = JSON.stringify(evInfo)
let comp = (ue) => (actionNode == ue.actionNode && ue.f_str == f_str)
let e = this.unitEvents.find(comp)
if(e == undefined){return e}
e = this.unitEvents.splice(this.unitEvents.indexOf(e), 1)[0]
if(e.action == "_remove"){
e.actionNode.addEventListener(e.action, e.f)
e.actionNode.dispatchEvent(new Event(e.action))
e.actionNode.removeEventListener(e.action, e.f)
} else {
e.actionNode.removeEventListener(e.action, e.f)
}
return e
},
// cool, now we can go e.ct.ds.val = this.element.q('[data-target]').d.val and its workable.
parentUnit(unitc){
let p = this.el.parentElement
if(unitc == undefined) {
while(p != null && !(p.dataset.unit == undefined)){
p = p.parentElement
}
} else {
while(p != null && !(p.dataset.unit.split(' ').includes(unitc))){
p = p.parentElement
}
}
if(p == undefined){ return p }
return p._unit
},
childUnitsFirstLayer(unitc){
let arr
if(unitc == undefined) {
arr = this.el.qa(':scope [data-unit]:not(:scope [data-unit] [data-unit])');
} else {
arr = this.el.qa(":scope [data-unit] [data-unit='" + unitc + "']:not(:scope [data-unit] [data-unit~='" + unitc + "'])");
}
return Array.from(arr).map(el => el._unit)
},
childUnitsDirect(unitc){
let arr
if(unitc == undefined) {
arr = this.qa(':scope > [data-unit]')
} else {
arr = this.qa(":scope > [data-unit~='" + unitc + "']")
}
return Array.from(arr).map(el => el._unit)
},
childUnits(unitc){
let arr
if(unitc == undefined) {
arr = this.qa(':scope [data-unit]')
} else {
arr = this.qa(":scope [data-unit~='" + unitc + "']")
}
return Array.from(arr).map(el => el._unit)
},
diveOptParams: ['url', 'method', 'csrfContent',
'csrfHeader', 'csrfSelector', 'confirm',
'withCredentials', 'e', 'submitter'
],
diveErrParams: ['formInfo', 'xhrChangeF', 'baseElement'],
relevantData(e){
if(e){
let unitId = this.el.id
let submitterId = e.ct.id
let d1 = this.el.dataset
let d2 = e.ct.dataset
let out = {unitId, submitterId, ...d1, ...d2}
return out
} else {
return {unitId: this.el.id, ...this.el.dataset}
}
},
diveRepeatDefaultStopF(h){
//due to spa this pageChanged thing can be a bother.
let pageChanged = window.location.href != h.originalPageLocation
let askedToStop = ( h.lastResult == "STOP" )
return (pageChanged || askedToStop)
},
diveBehaviors: {
repeat: async function(e, h) { //async
//h.stopF, h.repeats, h.originalPageLocation, h.waitTime, h.lastResults, h.processF
h.originalPageLocation = window.location.href
//get the functions requested from the unit, passing actual function objects not supported
if(! h.stopF){
h.stopF = this.diveRepeatDefaultStopF.bind(this)
} else {
h.stopF = (this[h.stopF]).bind(this)
}
if(h.processF){h.processF = (this[h.processF]).bind(this)}
h.repeats = 0
if(!h.waitTime){h.waitTime = 3000}
let inf = this.diveInfo(e, h)
while(!h.stopF(h)){
h.lastResult = otty.dive(inf)
if(h.processF){h.processF(h)}
h.repeats += 1
let justWait = (resolver) => setTimeout(resolver, h.waitTime)
await new Promise(justWait, justWait)
}
},
stagger: async function(e, h){
this.delayInfo = this.diveInfo(e, h)
if(this.timeoutWait){return}
let doit = (() => {
this.lastStagger =(new Date().getTime())
this.timeoutWait = undefined
return otty.dive(this.delayInfo)
}).bind(this)
if(!this.lastStagger){this.lastStagger = 0}
let waitedFor = (new Date().getTime()) - this.lastStagger
let stag = h.stagger || 500
if(waitedFor < stag){
this.timeoutWait = setTimeout((() => {
doit()
}).bind(this), stag - waitedFor)
} else {
doit()
}
},
limit: async function(e, h){
let tn = new Date().getTime()
let stag = h.waitTime || 500
if ( this.lastStagger && (tn - this.lastStagger) < stag){
return
}
this.lastStagger = tn
return this.diveBehaviors.default.bind(this)(e, h)
},
default: function(e, h){
return otty.dive(this.diveInfo(e, h))
}
},
diveInfo(e, h = {}){
//this function is for formatting html data into a format for an otty dive. Behaviours:
// 1. returns an object with the dive options, includexing the formInfo option (which has the information sent to the server)
// - if a dataset key is included in diveOptParams, it will be added to the options.
// - if a dataset key is included in diveErrParams, you will get a warning.
// - otherwise, it will be added as a form field in formInfo.
// ('csrfHeader' key = the data-csrf-header parameter in the html. As csrfHeader is in diveOptParams, it will become an option.)
// 2. formInfo includes unitId, submitterId, the units dataset, and the event's currentTarget dataset (with the currentTarget taking priority.)
// 3. options:
// opts: override default options hash (must still have formInfo object)
// data: override the dataset aquisition
// formData: override the default FormData object
// withform: This does a few things:
// - includes any input elements within the unit (not the form) in the request
// - if the data-path / data-method is not included, falls back to closest form's action/formaction/method/formethod html attitrubes
// - you can think of this boolean as saying "play nice with the html"
//
// note that if the unit is on a form, it will automatically activate withForm and use that forms
// formdata
let defaults = {
opts: {e: e, formInfo: {}},
withform: false
}
//get the input data elements
let data = this.relevantData(e)
//parse the settings into appropriate locations
h = { ...defaults, ...h }
h.opts = {...defaults.opts, ...h.opts} //regain e and formInfo
let inp, els, k
for(k of Object.keys(data)){
if(this.diveOptParams.includes(k)){
h.opts[k] = data[k]
} else if(this.diveErrParams.includes(k)){
console.error('bad key for diveDataset: ' + k)
} else {
h.opts.formInfo[k] = data[k]
}
}
// if its a form treat inputs as natively as possible. Otherwise
// if someone wants it to act form-like, then try that. Note
// that in this case it doesn't work with everything...
// (checkboxes for example). Usually fine though.
if(this.element.nodeName == "FORM"){
h.withform = true
h.formData = new FormData(this.element)
} else if(h.withform){
els = Array.from(this.el.qsa('input'))
if(this.el.nodeName == 'INPUT'){els.push(this.el)}
for(inp of els) {
if(inp.name != null && inp.value != null) {
h.formData.append(inp.name, inp.value)
}
}
}
if(!(h.opts.url)){ h.opts.url = h.opts.formInfo.path }
if(!(h.opts.method)){h.opts.method = h.opts.formInfo.method}
if(h.withform){
if(!( h.opts.url)){ h.opts.url = e.ct.getAttribute("formaction")}
if(! (h.opts.url) ){ h.opts.url = e.ct.closest('form')?.getAttribute('action') }
if(!(h.opts.method)){ h.opts.method = e.ct.getAttribute("formmethod")}
if(!(h.opts.method)){ h.opts.method = e.ct.closest('form')?.getAttribute('method') }
if(e.ct.name && e.ct.value){
h.formData.append(e.ct.name, e.ct.value)
}
}
if(!(h.opts.method)){ h.opts.method = "POST"}
h.opts.formInfo = otty.obj_to_fd(h.opts.formInfo, h.formData)
return h.opts
},
dive(e, h){
if(!h['allowDefault']){e.preventDefault()}
let act
if(h.behavior == undefined){h.behavior = 'default'}
if(act = this.diveBehaviors[h.behavior]){
act.bind(this)(e, h)
} else {
console.error("bad behavior type for a dive")
}
}
}
Object.defineProperties(generic, {
el: {
get: function(){return this.element},
set: function(v){return this.element = v}
}
})
export default generic