Christian Heilmann

Providing script configuration in-line and programatically

Friday, May 23rd, 2008 at 10:26 am

One of the things I’ve been quite consistent and pushy about when writing code is to separate out all the things that should be customizable into an own configuration object.

A normal script I write these days would look like this:

var module = function(){
  // configuration, change things here
  var config = {
    CSS:{
     classes:{
       hover:'hover',
       active:'current',
       jsEnabled:'js'
     },
     ids:{
       container:'maincontainer'
     } 
    },
    timeout:2000,
    userID:'chrisheilmann'
  };
 
  // start of main code 
  function init(){
 
  };
  // ... more methods and other code ...
 
  // make init a public method
  return {
    init:init
  };
}();
module.init();

The benefits should be quite obvious:

  • Implementers don’t need to hunt through the whole script to find what they need to change
  • There’s a clear separation of “change what you need to change here” and “only touch this if you know what you are doing” – allowing more developers to use your code.
  • You show implementers in a single location where you overlap with other development layers, for example by defining IDs of HTML elements you use and CSS class names you apply to generated elements.
  • Having all the strings in one place makes for easier localisation and also speeds up the script (IE6 creates a string object for every string – even in conditions inside loops for example!)

I was quite OK with that until Ara Pehlivanian asked for an option to programatically override the configuration files, much like the YUI allows you to do with the YAHOO.util.Config utility. He is right of course, sometimes you’d want to change the config and re-initiate the script (the other way of course is to write a module with instantiation).

The easiest way to approach that is to make the config object public:

var module = function(){
  // configuration, change things here
  var config = {
    CSS:{
     classes:{
       hover:'hover',
       active:'current',
       jsEnabled:'js'
     },
     ids:{
       container:'maincontainer'
     } 
    },
    timeout:2000,
    userID:'chrisheilmann'
  };
 
  // start of main code 
  function init(){
 
  };
  // ... more methods and other code ...
 
  // make init and config a public method
  return {
    init:init,
    config:config
  };
}();

That way you can override the properties you need before you call init():

module.config.CSS.ids.container = 'header';
module.config.userID = 'alanwhite';  
module.init();

However, Ara thought it more convenient to be able to provide an object as a parameter to init() that overrides certain properties. You can do that by checking for this object, looping through its properties and recursively trying to find and match a property of the config object:

var module = function(){
  // configuration, change things here
  var config = {
    CSS:{
     classes:{
       hover:'hover',
       active:'current',
       jsEnabled:'js'
     },
     ids:{
       container:'maincontainer'
     } 
    },
    timeout:2000,
    userID:'chrisheilmann'
  };
 
  // start of main code 
  function init(){
    // check if the first argument is an object
    var a = arguments;
    if(isObj(a[ 0 ])){
      var cfg = a[ 0 ];
 
      // loop through arguments and alter the configuration
      for(var i in cfg){
        setConfig(config,i,cfg[i]);
      }
    }
  };
 
  function setConfig(o,p,v){
    // loop through all the properties of he object
    for(var i in o){
      // when the value is an object call this function recursively
      if(isObj(o[i])){
        setConfig(o[i],p,v);
 
      // otherwise compare properties and set their value accordingly
      } else {
        if(i === p){o[p] = v;};
      }
    }
  };
 
  // tests if a parameter is an object (and not an array)
  function isObj(o){
    return (typeof o === 'object' && typeof o.splice !== 'function');
  }
  // ... more methods and other code ...
  // make init a public method
  return {
    init:init
  };
}();
module.init({
  container:'header',
  'timeout':1000
});

This works swimmingly when all the configuration properties are unique. It fails though when a property in a nested object has the same name as another one on a higher level. In order to allow for this, we can offer the option to send a string with the path to the property as the property name. Then it gets messy as we need to eval() that string and make sure we return the value in the right format. All in all it could look like this:

var module = function(){
  // configuration, change things here
  var config = {
    CSS:{
     classes:{
       hover:'hover',
       active:'current',
       jsEnabled:'js'
     },
     ids:{
       container:'maincontainer'
     } 
    },
    timeout:2000,
    userID:'chrisheilmann'
  };
 
  // start of main code 
  function init(){
    if(isObj(arguments[ 0 ])){
      var cfg = arguments[ 0 ];
      for(var i in cfg){
        if(i.indexOf('.')!== -1){
          var str = '["' + i.replace(/\./g,'"]["') + '"]';
          var val = getValue(cfg[i]);
          eval('config' + str + '=' + val);
        } else {
          setConfig(config,i,cfg[i]);
        }
      }
    }
  };
  function setConfig(o,p,v){
    for(var i in o){
      if(isObj(o[i])){
        setConfig(o[i],p,v);
      } else {
        if(i === p){o[p] = v;};
      }
    }
  };
  function isObj(o){
    return (typeof o === 'object' && typeof o.splice !== 'function');
  };
  function getValue(v){
    switch(typeof v){
      case 'string':
        return "'" + v + "'";
      break;
      case 'number':
        return v;
      break;
      case 'object':
        if(typeof v.splice === 'function'){
          return '[' + v + ']';
        } else {
          return '{' + v + '}';
        }
      break;
      case NaN:
      break;
    };
  };
 
  // ... more methods and other code ...
  // make init a public method
  return {
    init:init
  };
}();
module.init({
  'container':'header',
  'CSS.classes.active':'now',
  'timeout':1000
});

In order to make that readable, let’s encapsulate all the configuration alteration code in an own module:

var module = function(){
  // configuration, change things here
  var config = {
    CSS:{
     classes:{
       hover:'hover',
       active:'current',
       jsEnabled:'js'
     },
     ids:{
       container:'maincontainer'
     } 
    },
    timeout:2000,
    userID:'chrisheilmann'
  };
 
  // start of main code 
  function init(){
    console.log(config);
  };
 
  // ... more methods and other code ...
 
  // Configuration changes 
  var changeConfig = function(){
    function set(o){
      var reg = /\./g;
      if(isObj(o)){
        for(var i in o){
          if(i.indexOf('.')!== -1){
            var str = '["' + i.replace(reg,'"]["') + '"]';
            var val = getValue(o[i]);
            eval('config' + str + '=' + val);
          } else {
            findProperty(config,i,o[i]);
          }
        }
      }
    };
    function findProperty(o,p,v){
      for(var i in o){
        if(isObj(o[i])){
          findProperty(o[i],p,v);
        } else {
          if(i === p){o[p] = v;};
        }
      }
    };
    function isObj(o){
      return (typeof o === 'object' && typeof o.splice !== 'function');
    };
    function getValue(v){
      switch(typeof v){
        case 'string': return "'"+v+"'"; break;
        case 'number': return v; break;
        case 'object':
          if(typeof v.splice === 'function'){
            return '['+v+']';
          } else {
            return '{'+v+'}';
          }
        break;
        case NaN: break;
      };
    };
    return{set:set};
  }();
  // make init a public method
  return {
    init:init
  };
}();
module.init({
    'container':'header',
    'CSS.classes.active':'now',
    'timeout':1000
});

And that is one way to provide a configuration object and make it possible to change it programatically in the init() method. Can you think of a better one?

Tags: , ,

Share on Mastodon (needs instance)

Share on Twitter

Newsletter

Check out the Dev Digest Newsletter I write every week for WeAreDevelopers. Latest issues:

Dev Digest 146: 🥱 React fatigue 📊 Query anything with SQL 🧠 AI News

Why it may not be needed to learn React, why Deepfake masks will be a big problem and your spirit animal in body fat! 

Dev Digest 147: Free Copilot! Panel: AI and devs! RTO is bad! Pi plays!

Free Copilot! Experts discuss what AI means for devs. Don't trust containers. Mandated RTO means brain drain. And Pi plays Pokemon!

Dev Digest 148: Behind the scenes of Dev Digest & end of the year reports.

In 50 editions of Dev Digest we gave you 2081 resources. Join us in looking back and learn about all the trends this year.

Dev Digest 149: Wordpress break, VW tracking leak, ChatGPT vs Google.

Slowly starting 2025 we look at ChatGPT vs Google, Copilot vs. Cursor and the state of AI crawlers to replace web search…

Dev Digest 150: Shifting manually to AI.

Manual coding is becoming less of a skill. How can we ensure the quality of generated code? Also, unpacking an APK can get you an AI model.

My other work: