Intro.
In the past I made use of the "document.querySelector(All) " all over the place and what often resulted in "undefined" errors.
To be honest, it took me a long time to find a solution for this.
- At first I began with creating a function for the "document.querySelector(All) (that I to date still use!)"
export async function elQuery(...args){
const [elem,el_all=false,el_parent] = args;
let el;
if(true === el_all){
if(el_parent) el = el_parent.querySelectorAll(elem);
else el = document.querySelectorAll(elem);
}else{
if(el_parent) el = el_parent.querySelector(elem);
else el = document.querySelector(elem);
}
return await el;
}
There were some improvements with this steps but it was still struggling to get my queries right.
A Quantum Leap forward.
Finally I found a way to get this done by using Map() and that is what I want to share with you.
First this.
I use javascript with a module approach, working with a folder/file structure and a bunch of custom (reusable) functions, aside of that I use classes too.
The given example here is a reflection of that!
1 structure:
assets/
scripts/
factory/
dom_objects.js
functions.js
generals.js
handlers.js
modules/
actions/
set_actions_1.js
set_actions_2.js
set_default_action.js
get_actions.js
callbacks/
sub_callbacks_1.js
sub_callbacks_2.js
defaults/
set_defaults.js
mdl_factory/
//module specific files
templates/
sub_templates_1.js
sub_templates_2.js
index.js
2 dom_objects.js:
That's the file where I collect my dom objects and it holds two functions, called getBaseObjects and getExtendedObjects.
// assets/scripts/factory/dom_objects.js */
import * as FT from './functions.js';
export const getBaseObjects = async()=>{
const map = new Map([['base_objects',{
vvp: window.visualViewport,
location_base: window.location.origin,
body: document.body,
wrap_ctn: await FT.elQuery('div.wrap.container', false, self.body),
main_elem: await FT.elQuery('main', false, self.wrap_ctn),
actions_block: await FT.elQuery('.actions-block_ctn',false,self.wrap_ctn),
//other base objects
}]]);
return map.get('base_objects');
}
export const getExtendedObjects = async (base_dom)=>{
const {vvp,location_base,body,main_elem,actions_block} = base_dom;
const map = new Map([['ext_objects',{
vvp,location_base,body,main_elem,actions_block,
main_sub_ctn: await FT.elQuery('div.main_sub_ctn',false,self.main_elem),
//option 1 (not recommended), is just to show that you can group objects
action_items:{
action_item_1: await FT.elQuery('.action-item-1',false,self.actions_block),
action_item_2: await FT.elQuery('.action-item-2',false,self.actions_block),
action_item_3: await FT.elQuery('.action-item-3',false,self.actions_block),
action_item_4: await FT.elQuery('.action-item-4',false,self.actions_block),
},
//option 2 (if your actions_block isn't going to change)
action_items: await FT.elQuery('.action-item',true,self.actions_block),
//option 3 (if your actions_block has live events and is going to change)
action_items: await FT.getClassHelper('action-item',self.actions_block),
// uses 'getElementsByClassName'
//-----------------------------
//other ext objects
}]]);
return map.get('ext_objects');
}
explanation:
I have created two functions here but for a reason.
The getBaseObjects have values that are available as soon as the website loads and will never give an undefined error:
//assets/scripts/index.js
import {getBaseObjects} from './factory/dom_objects.js';
import {getActions} from './modules/actions/get_actions.js';
(async ()=>{
const base_elems = await getBaseObjects();
await getActions(base_elems);
//sure, there are more functions and classes that I call here.
})();
The steps here:
- 'getBaseObjects' is imported from 'dom_objects.js'.
this function is called and applied to a const 'base_elems'.
'getActions' is called from 'get_actions.js'.
- the values of step2 are passed as an arg to the 'getActions(base_elems)' function.
From here, the objects{keys:values} of 'base_elems' are available in the getActions function.
The getExtendedObjects might have values that has not been created yet and could give an undefined error if so?
/** modules/actions/get_actions.js */
import {getExtendedObjects} from './../../factory/dom_objects.js';
import * as SA1 from './set_actions_1.js';
export const getActions = async(base_elems)=>{
const ext_elems = await getExtendedObjects(base_elems);
await setActions1(ext_elems);
}
The steps here:
- 'getExtendedObjects' is imported from 'dom_objects.js'.
- SA1.setActions1 is imported from 'set_actions_1.js'.
- function 'getActions' has been created together with a param 'base_elems'.
- within that function, 'getExtendedObjects' is called and args 'base_elems' are applied to it. Then this function is applied to a const 'ext_elems'.
- 'SA1.setActions1' is called and has args 'ext_elems' applied to it.
From here all objects are available when called.
In a real situation.
In a real situation you only want the objects you need for a certain part of your webpage.
Let say:
- 'header' related objects for in the header.
- 'main' related objects for in the main.
- 'footer' related objects for in the footer.
The best way to do that is to create nested object groups in your Map().
const map = new Map([['ext_objects',{
header_objects:{
//header objects here
},
main_objects:{
//main objects here
},
footer_objects:{
//footer objects here
},
})
Then in your 'getActions', you are doing this:
export const getActions = async(base_elems)=>{
const ext_elems = await getExtendedObjects(base_elems);
//note: this is called 'destructuring', for objects it is {} and for arrays []
const {header_objects,main_objects,footer_objects} = ext_elems;
await headerAction(header_objects);
await mainAction(main_objects);
await footerAction(foot_objects);
}
Now, each action get it's own objects.
Enough for now!
There is way more to say about it but I think this is enough for now!
Cheers!