I am working on a vanilla Javascript jQuery application that uses classes and subclasses to populate the browser with cards, each of which display data from session storage. There is a Box
parent class, and then two subclasses for the different types of boxes: class GroupBox extends Box
and class SubjectBox extends Box
. The way the application works is, on initial page load, all the Group Boxes appear on the page. The user can click on a group box in order to drill down in the data navigate to the Subject Boxes associated with each group.
I wrote a function displayGroupBoxes
that runs on page load. It iterates through the data in session storage, and for each relevant item in the data, it generates a new GroupBox
object and assigns it the properties in the data.
Now, every time this function generates a new group box object, I want to run another function that generates the HTML tags for each box and appends them to the browser page. The way I accomplished this at first was to write all that DOM manipulation code in the constructor function. That worked perfectly, but a more experienced developer pointed out to me that all that code should be separated out into its' own function. So I created a createGroupElements
function and moved the DOM manipulation code there. I am now calling that function from within the constructor so that it will run every time a new object is instantiated.
In my previous S/O post about this project, it was pointed out to me that the function "shouldn't be called from inside the constructor. The constructor should only concern itself with setting up the instance properties. That way, your subclass can finish initialization before the method is called."
My question is, if not in the constructor, where can I call the createGroupElements
function so that it will run once every time a new GroupBox is instantiated?
See below for relevant code.
//////////////////////////////////////////////////////////////////////////////
// CLASSES \\
//////////////////////////////////////////////////////////////////////////////
//////////////////////
// Box Class (parent)
//////////////////////
/**
* Represents a box.
* @constructor
* @param {object} boxOptions - An object to hold our list of constructor args
* @param {string} name - Name associated with account
* @param {string} type - Type associated with account
* @param {string} logo - Logo associated with account
*/
class Box {
constructor(boxOptions) {
this.name = boxOptions.name;
this.type = boxOptions.type;
this.logo = boxOptions.logo;
}
}
///////////////////////////
// Group SubClass
//////////////////////////
/**
* Represents a type "B" Group box. New group boxes are instantiated when the "displayGroupBoxes()" function is called.
* @constructor
* @param {object} groupOptions - An object to store our list of constructor args
* @param {array} subjectBoxes - An array to store subject boxes so that once they've been instantiated, we can store them here and then toggle to hidden/visible.
*/
class GroupBox extends Box {
constructor(groupOptions) {
super({
name: groupOptions.name,
type: groupOptions.type,
logo: groupOptions.logo,
});
// Use this array to store the subject boxes that have already been created, so if the user clicks on the same subject, we can grab those boxes and show them rather than rebuilding.
this.subjectBoxes = [];
this.name = groupOptions.name;
this.type = groupOptions.type;
this.logo = groupOptions.logo;
this.gotoGroup = groupOptions.gotoGroup;
this.container = groupOptions.container;
this.subjects = groupOptions.subjects;
this.subjectIcons = groupOptions.subjectIcons;
// Create HTML Elements and Append To Page
// ------ >>>> This should be called from outside the constructor?? <<<< ---- \\
this.createGroupBox();
}
// Function to create HTML tags and event listeners for group box elements, and append them to the page
createGroupBox() {
// Create container div for box
const newGroupBox = document.createElement("div");
newGroupBox.className = "group-box-container";
//-----> a bunch of code to create html tags and append them goes here, removed for brevity and clarity!
newGroupBox.append(mainContainer);
////////////////////////////////////////////////
// EVENT LISTENER
//////////////////////////////////////////////
// If a user clicks on a group box, we want to rebuild the page with the relevant subject boxes (drilling down in the data), similar to how a SPA application would work.
newGroupBox.addEventListener("click", () => {
// ---> a bunch of code to hide the group boxes that were on the page before, change the header, etc.
// --->> a bunch of code to generate the subject boxes (or simply toggle them to visible if they've already been created), create HTML tags, and append them to the page
// Append group box to document inside "group-boxes" container
this.container.append(newGroupBox);
}
}
//////////////////////////////////
// Subject SubClass
/////////////////////////////////
/**
* Represents a "P" type Subject Box
* @param {object} subjectOptions - An object to store our constructor args.
* @param {number} subjectId - The Subject Id associated with a given subject box.
* @param container - <div></div> element for the subject boxes to be placed in once they're instantiated.
*/
class SubjectBox extends Box {
constructor(subjectOptions) {
super({
name: subjectOptions.name,
type: subjectOptions.type,
logo: subjectOptions.logo,
});
this.name = subjectOptions.name;
this.type = subjectOptions.type;
this.logo = subjectOptions.logo;
this.subjectId = subjectOptions.subjectId;
this.container = document.createElement("div");
}
show(isShow = true) {
if (isShow) {
$(this.container).show();
} else {
$(this.container).hide();
}
}
}
So the function that actually generates the new GroupBoxes already runs on page load inside the document.ready
function. But the GroupBox class method createGroupElements
, which creates the HTML tags for each new group box and appends them to the page, is currently being called from inside the constructor. How can I address this?
CodePudding user response:
"The constructor should only concern itself with setting up the instance properties"
That's generally a good guideline. You rarely (if ever) want to use constructors for their external side-effects. Leveraging factories like @NicholasCarey suggested can usually be a good way to solve that problem.
However, when it comes to UI class components there's 3 basic solutions I've seen work pretty well:
You let the component render itself in a sandbox (container) element that you specify from the caller. Rendering happens at instantiation time. There are side-effects in the constructor, but they are limited to the provided sandbox element which allows to remain modular.
What could be tricky here is if the component needs some kind of async initialization in which case you could dispatch an async event when fully initialized/rendered.
The caller renders the component by calling
component.render(container)
. The component would throw, do nothing or perhaps refresh it's view ifrender
is called more than once. The constructor is now void of side-effects, but the caller has taken upon the responsibility of initiating the rendering (which is good).The component exposes it's DOM element, but doesn't append it. This is probably the most flexible approach. You could either initialize the DOM directly when the constructor is called or do a lazy initialization when the
component.domElement
property is accessed. The component doesn't have to be aware of the context in which it's used.The caller is the responsible for adding
component.domElement
where it wants in the document, e.gmainContainer.appendChild(component.domElement)
.
CodePudding user response:
Don't publicly expose the class. Instead, expose a factory function that instantiates the class and invokes your function before returning the newly created instance. That way, you control how objects are created.
If it's important that the sidecar function (if it's not synchronous) have completed before you return the newly created object instance, make it async
and await
the sidecar's completion.