src/abyme_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["template", "associations", "fields", "newFields"];
connect() {
console.log("Abyme Connected");
if (this.count) {
// If data-count is present,
// add n default fields on page load
this.add_default_associations();
}
}
// return the value of the data-count attribute
get count() {
return this.element.dataset.minCount || 0;
}
// return the value of the data-position attribute
// if there is no position specified set end as default
get position() {
return this.associationsTarget.dataset.abymePosition === "end"
? "beforeend"
: "afterbegin";
}
// ADD_ASSOCIATION
// this function is call whenever a click occurs
// on the element with the click->abyme#add_association
// <button> element by default
// if a data-count is present the add_association
// will be call without an event so we have to check
// this case
// check for limit reached
// dispatch an event if the limit is reached
// - call the function build_html that take care
// for building the correct html to be inserted in the DOM
// - dispatch an event before insert
// - insert html into the dom
// - dispatch an event after insert
add_association(event) {
if (event) {
event.preventDefault();
}
if (this.element.dataset.limit && this.limit_check()) {
this.create_event("limit-reached");
return false;
}
const html = this.build_html();
this.create_event("before-add");
this.associationsTarget.insertAdjacentHTML(this.position, html);
this.create_event("after-add");
}
// REMOVE_ASSOCIATION
// this function is call whenever a click occurs
// on the element with the click->abyme#remove_association
// <button> element by default
// - call the function mark_for_destroy that takes care
// of marking the element for destruction and hiding it
// - dispatch an event before mark & hide
// - mark for descrution + hide the element
// - dispatch an event after mark and hide
remove_association(event) {
event.preventDefault();
this.create_event("before-remove");
this.mark_for_destroy(event);
this.create_event("after-remove");
}
// LIFECYCLE EVENTS RELATED
// CREATE_EVENT
// take a stage (String) => before-add, after-add...
// create a new custom event
// and dispatch at at the controller level
create_event(stage, html = null) {
const event = new CustomEvent(`abyme:${stage}`, {
detail: { controller: this, content: html },
});
this.element.dispatchEvent(event);
// WIP
this.dispatch(event, stage);
}
// WIP : Trying to integrate event handling through controller inheritance
dispatch(event, stage) {
if (stage === "before-add" && this.abymeBeforeAdd)
this.abymeBeforeAdd(event);
if (stage === "after-add" && this.abymeAfterAdd) this.abymeAfterAdd(event);
if (stage === "before-remove" && this.abymeBeforeRemove)
this.abymeBeforeAdd(event);
if (stage === "after-remove" && this.abymeAfterRemove)
this.abymeAfterRemove(event);
}
abymeBeforeAdd(event) {}
abymeAfterAdd(event) {}
abymeBeforeRemove(event) {}
abymeAfterRemove(event) {}
// BUILD HTML
// takes the html template and substitutes the sub-string
// NEW_RECORD for a generated timestamp
// then if there is a sub template in the html (multiple nested level)
// set all the sub timestamps back as NEW_RECORD
// finally returns the html
build_html() {
let html = this.templateTarget.innerHTML.replace(
/NEW_RECORD/g,
new Date().getTime()
);
if (html.match(/<template[\s\S]+<\/template>/)) {
const template = html
.match(/<template[\s\S]+<\/template>/)[0]
.replace(/(\[\d{12,}\])(\[[^\[\]]+\]"){1}/g, `[NEW_RECORD]$2`);
html = html.replace(/<template[\s\S]+<\/template>/g, template);
}
return html;
}
// MARK_FOR_DESTROY
// mark association for destruction
// get the closest abyme--fields from the remove_association button
// set the _destroy input value as 1
// hide the element
// add the class of abyme--marked-for-destroy to the element
mark_for_destroy(event) {
let item = event.target.closest(".abyme--fields");
item.querySelector("input[name*='_destroy']").value = 1;
item.style.display = "none";
item.classList.add("abyme--marked-for-destroy");
}
// LIMIT_CHECK
// Check if associations limit is reached
// based on newFieldsTargets only
// persisted fields are ignored
limit_check() {
return (
this.newFieldsTargets.filter(
(item) => !item.classList.contains("abyme--marked-for-destroy")
).length >= parseInt(this.element.dataset.limit)
);
}
// ADD_DEFAULT_ASSOCIATION
// Add n default blank associations at page load
// call sleep function to ensure uniqueness of timestamp
async add_default_associations() {
let i = 0;
while (i < this.count) {
this.add_association();
i++;
await this.sleep(1);
}
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}