Search code examples
javascriptjqueryarraysselectors-api

Generic class addition using querySelectorAll / classList without having to constantly loop


I'm facing the following issue.

Everytime I have to use querySelectorAll with Element.classList, I need to

  1. convert the NodeList returned from Element.querySelectorAll to an Array
  2. perform a forEach to the Array to manipulate each item separately.

jQuery abstracts the above, so I would like to develop a helper method, similar to how jQuery works, that works as such:

myhelper('.someClass').classList.add('newClass'); // there are more than 1 .someClass items
myhelper('#id').classList.remove('existingClass');

Essentially myhelper(selector) should under the hood abstract the points 1+2 above: get the NodeList from querySelectorAll, convert it to an Array, forEach the array and *executed the method that has been given by the user.

PS: to simplify things, it could work for a specific set of native methods: the classList methods and textContent for example.


Solution

  • My first impulse would be to suggest just using jQuery and extend where needed ;)

    But to give it a go: if the my_Helper function is used as an object, it can buffer the elements and contain functions that use those elements on itself. If my_Helper is called directly, it can be forced to return a new object. Further the added functionality can return the object itself, so that chaining can be used just as in jquery. A simple example with addClass:

    function my_Helper(query){
    	if(this.constructor !== my_Helper)
      	 return new my_Helper(query); //if called directly (not as new()), return a new object
         
      this.elements = document.querySelectorAll(query);  
      this.addClass = function(className) {
      	for(var el of this.elements)
        	el.classList.add(className);
      	return this; //to be able to use chaining
      }
      return this;
    }
    
    
    
    my_Helper('.someClass').addClass('newClass').addClass('newClass2'); //2 separate classes to test chaining
    .newClass{
      width:100px;
      height:100px;  
    }
    
    .newClass2{
      border:1px solid black;
    }
    <div class= 'someClass'></div>
    <div class= 'someClass'></div>
    <div class= 'someClass'></div>

    edit, based on the comment, it wouldn't be a problem to add the extra methods manually, but the wish is to make adding individual methods easier without copying the foreach? The below has a general invoke function which can be called from outside the objects and helper functions that use this invoke to create the other methods (and also introduces a classList wrapper that simply calls addClass, for making it easier to migrate code)

    function my_Helper(query){
    	if(this.constructor !== my_Helper)
      	 return new my_Helper(query); //if called directly (not as new()), return a new object
      
      this.elements = document.querySelectorAll(query);        
      let self = this;
      
      this.invoke = function(property , func, ...pars){
      	for(let el of self.elements){    
        	if(!func){  //no function given -> property setter
          	if(property)
            	el[property] = [pars]
          }
          else {
            let p = property ? el[property] : el; //if no property is given, use element itself
            if(!p) continue;
    
            let fn = p[func];
            if(!fn) continue; //function does not exist on the property or element      
            fn.apply(p,pars);
          }
        }
        return self;
      }
      
      function fn(property, functionName, ...pars){
      	return (...pars) => self.invoke(property, functionName, pars);
      }
      
      this.addClass = fn('classList', 'add');
      this.removeClass = fn('classList', 'add');
      this.text = fn('textContent');
      
      this.classList = {add:self.addClass, remove:self.removeClass}; //if classlist has to be used instead of addClass
      
      return this;
    }
    
    my_Helper('.someClass').classList.add('newClass').addClass('newClass2').text('aaa');
    .newClass{
      width:100px;
      height:100px;  
    }
    
    .newClass2{
      border:1px solid black;
    }
    <div class= 'someClass'></div>
    <div class= 'someClass'></div>
    <div class= 'someClass'></div>

    In this basic implementation the invoke expects strings, but it could be easily extended to accept functions as well.