0

So I'm currently building a pretty large framework to be used for a lot of web projects. I'd love to get some advice or best practises to the following scenario.

I have different chapters in a web-app. Only one chapter is visible at a time.

The function createView() is called when a chapter is visited the first time. It creates the view via a handlebars template, inserts all the copy from an XML doc (we have an XML for each language), and starts the chapter via the start() function. start() is called whenever a chapter gets visited again. It's just inserting the view into the DOM and enables all the functionality like click-events, other modules (galleries, sliders, ect) and so forth.

var Chapter = function(name) {
    this.name = name;
    this.createView();
};

Chapter.prototype.createView = function() {
    var self = this;
    
    // get handlebars template
    this.template = config.app.getTemplate('chapters/'+self.name).then(function(hbs) {
        
        // compile hbs template, insert data here - if there is any
        var html = hbs();
        
        // save jQuery collection of HTML as view
        self.view = $(html);
        
        // insert copy from XML doc
        self.view.find('[data-text]').each(function() {
            var theID = '#'+$(this).attr('data-text');
            var theText = config.xml.find(theID).text();
            $(this).html(theText);
        });
        
        // start the experience
        self.start();
        
    });
};

Chapter.prototype.start = function() {
    
    // stop active experience
    if(config.experiences.active) {
        config.experiences.active.stop();
    }
    
    // scroll to top
    $(window).scrollTop(0);
    
    // save this chapter as the active one
    config.experiences.active = this;
    
    // insert view into DOM
    this.view.appendTo($('main'));
    
    // enable functionality
    this.initModules();
    this.initNavigation();
    this.initLocking();
    
};

Now, every instance of Chapter will need some custom functions as every chapter can look different across a lot of projects. But I am not sure if this is the best approach:

Use a lof of if-else in initCustoms.

Chapter.prototype.start = function() {
    ...
    this.initCustoms();
};

Chapter.prototype.initCustoms = function() {
    if(this.name === 'Fire and Ice') {
        // do abc
    } else if(this.name === 'Second Sons') {
        // do xyz
    }
};

So is adding a bunch of if-else statements really the way to go? It seems so... dirty. I also though about:

create an object for the custom chapter and inherit its prototype

var FireAndIce = function() {
    // do abc
}

FireAndIce.prototype = Chapter.prototype;
FireAndIce.prototype.constructor = Chapter;

Chapter.prototype.start = function() {
    ...
    this.initCustoms();
};

Chapter.prototype.initCustoms = function() {
    if(this.name === 'Fire and Ice') {
        // inherit functionality of FireAndIce. Can't post it here as I have not been able to do this properly.
    }
    ...
};

But that just caused problems with inheritance, changing other instances or the main Chapter prototype. I don't want to copy all the functionality of FireAndIce over to the instance and based on my research on this subject, I can't properly inheritance from a prototype (FireAndIce) to an instance of another prototype (Chapter).

Also, just out of curiousity, is the following a bad idea?

Create a custom start event via jQuery to which I could bind as many custom handlers as I want

Chapter.prototype.start = function() {
    ...
    this.initCustoms();
    this.trigger('start');
};

Chapter.prototype.initCustoms = function() {
    if(this.name === 'Fire and Ice') {
        this.on('start', function() {
            // do abc
        });
    }
    ...
};

Leaving aside the reasons why I would do it, is there anything wrong with it? I kinda like the idea of having a start and stop event for each chapter to which I could bind additional functionality from everywhere.

Thanks in advance for advice.

Community
  • 1
  • 1
ProblemsOfSumit
  • 19,543
  • 9
  • 50
  • 61
  • 1
    *"But that just caused problems with inheritance, changing other instances or the main `Chapter` `prototype`"* No it doesn't, not the code you've posted. It creates a `prototype` property on the *instance`, which is completely unrelated to the `Chapter` function's `prototype` property or the prototype underlying instances created via `new Chapter`. – T.J. Crowder Nov 14 '14 at 12:03
  • yes, I chose a bad example. It creates a `prototype` property but I would want the methods of `FireAndIce` to be part of the instances `__proto__`. Edited for clarity. – ProblemsOfSumit Nov 14 '14 at 12:07
  • 1
    Maybe mix ins can work but if some functions are only used for one instance you can define the functions in FireAndIce and do FireAndIce.call(this) to have that instance have those functions. Maybe the following can help as it contains a mix in pattern and explains how call can be used: http://stackoverflow.com/questions/16063394/prototypical-inheritance-writing-up/16063711#16063711 – HMR Nov 14 '14 at 13:46

1 Answers1

1

A couple of options:

Just assign a function to them

It sounds as though there will only be a few Chapter instances and that each chapter is a one-off (there's only one copy of the FireAndIce chapter), for instance. In that case, you can just create the chapter instances and then assign their initCustoms function after you create them:

var fireAndIce = new Chapter();
fireAndIce.initCustoms = function() {
    // stuff for Fire and Ice
};
var secondSons = new Chapter();
secondSons.initCustoms = function() {
    // stuff for Second Sons
};
// ...

Use Prototypical Inheritance and Constructor Functions

But if you want to do this with inheritance, here's how that looks:

function Chapter() {
}
Chapter.prototype.setup = function() {
    // ...chapter common stuff
};

function FireAndIce() {
    Chapter.apply(this, arguments); // Chain to parent constructor
    // ...fire and ice stuff
}
FireAndIce.prototype = Object.create(Chapter.prototype);
FireAndIce.prototype.constructor = FireAndIce
FireAndIce.prototype.initCustoms = function() {
    // ...Fire and Ice custom stuff
};

function SecondSons() {
    Chapter.apply(this, arguments); // Chain to parent constructor
    // ...Second Sons stuff
}
SecondSons.prototype = Object.create(Chapter.prototype);
SecondSons.prototype.constructor = SecondSons
SecondSons.prototype.initCustoms = function() {
    // ...Second Sons custom stuff
};

That way, FireAndIce instances share the initCustoms on their prototype, and also share everything on the Chapter prototype, because the Chapter.prototype is the prototype behind the FireAndIce.prototype object. (And similarly for SecondSons.)

Note that if the derived functions (FireAndIce, SecondSons) and Chapter have different argument lists, instead of passing all arguments to Chapter from the derived function, you can just pass what's appropriate:

Chapter.call(this, the, appropriate, args, go, here);

Use Prototypical Inheritance without Constructor Functions

Some people prefer to use prototypical inheritance without constructor functions, and therefore without using new. That's also an option:

var ChapterProto = {
    setup: function() {
        // ...common chapter stuff...
    }
};
function createChapter() {
    var rv = Object.create(ChapterProto);
    // ...common chapter stuff...
    return rv;
}

var FireAndIceProto = Object.create(ChapterProto);
FireAndIceProto.initCustoms = function() {
    // ...Fire and Ice custom stuff...
};
function createFireAndIce() {
    var rv = Object.create(FireAndIceProto);
    createChapter.apply(rv, arguments);
    // ...Fire and Ice stuff...
    return rv;
}

var SecondSonsProto = Object.create(SecondSonsProto);
SecondSonsProto.initCustoms = function() {
    // ...Second Sons custom stuff...
};
function createSecondSons() {
    var rv = Object.create(SecondSonsProto);
    createChapter.apply(rv, arguments);
    // ...Second Sons stuff...
    return rv;
}

And again, if argument lists vary, you can use .call instead of .apply.

Conclusions

For just a few objects, I would prefer just assigning functions to the instances after creating them. Less code, simpler.

But for classes of objects (in the lower-case sense), I'd use prototypical inheritance with constructor functions as above (but some others would use it without constructor functions).


The above uses Object.create, which is new with ES5 (about five years old now). If you need to support really old engines, you can polyfill the one-argument version used above (the second argument cannot be polyfilled on ES3 engines):

if (!Object.create) {
    Object.create = function(proto, props) {
        if (props) {
            throw "The second argument to Object.create cannot be polyfilled.";
        }

        function ctor() {
        }
        ctor.prototype = proto;
        return new ctor();
    };
}
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Thank you for this detailed response. Really appreciate it! I like your first approach, but the instances of `Chapter` aren't created individually. They're created in a loop whenever another Chapter gets called. Imagine this framework will get used by devs to create a virtual book. Ergo: I need a way to let other devs "hook into" `Chapter` with custom functionality, which I try to provide a way for here. So I can't declare a custom function after creating an instance of `Chapter` - as I don't know what instances there'll be. – ProblemsOfSumit Nov 14 '14 at 13:32
  • Your second and third approach is what I was thinking of at first. Problem is: An instance of `Chapter` is already and always there. Now some of those instances will need custom functionality. So it's not that I could re-create the chapters with a different prototype/constructor. I'd need to somehow include/import all methods from a custom prototype - made by someone else - so that this one particular instance got those custom functions/methods – ProblemsOfSumit Nov 14 '14 at 13:34
  • FYI: your post made me realize a lot about the acutal problem (making a way for other devs to hook into `Chapter`). Thank you for that! – ProblemsOfSumit Nov 14 '14 at 13:35
  • @Sumit: *"Problem is: An instance of `Chapter` is already and always there."* I'm sorry, I don't understand what you mean by that. – T.J. Crowder Nov 14 '14 at 13:38
  • I think my problem is, that I create the `Chapter` instances whenever a chapter is visited first (click on a link, for example "visit chapter 3"), instead of creating all instances at pageload. That's why I can't declare custom functionality on one instance - because it doesn't exist yet. I tried to call custom functions after creating an instance but I think your first approach works like a charm if I create all the instances beforehand. – ProblemsOfSumit Nov 14 '14 at 13:46
  • @Sumit: It would work either way, it just changes where you put the code, but I probably don't have a good understanding of your overall design. – T.J. Crowder Nov 14 '14 at 13:52