Logo
ManuelSchoebel

Creating a meteor.js component - Scrollable Button Group

Components that are flexible and easy to configure are really great and will enhance your coding speed. Yet there is no official way of building components so I tried to formalize a way how it could be done. We will try to create a simple component as an example how it can be done. Feedback is, as always, very much appreciated.

##Goal The goal is to have a typical twitter bootstrap justified button group that can be configured. Also if there are more than an amount of X buttons in the group, it should have a left and a right arrow to scroll through all buttons without showing those all at once. In the end it should look something like this:

The component is called ScrollableBtnGroup (SBG).

##Files I usually put components like this into this folder in my application structure:

/client/views/components/
/client/views/components/scrollableBtnGroup.html
/client/views/components/scrollableBtnGroup.js

Since this is a quite tiny component I will not put it in its own folder, but you could if you like, of course. Often times you can see that people would also put the css/less/sass file here as well. I personaly do not like to have the styling anywhere near the component itself, because a component in my oppinion should only contain the structure (*.html) and the logic (*.js). Since you may want to use a good component in more than one projects the css should not come with the component itself. In this example there will be some basic bootstrap styling of course, but not more. In my applications I have a fixed structure for css, you can have a coser look how I like to do this here. Basically I use a less file that goes here:

/client/stylesheets/components/scrollableBtnGroup.import.less

When I need a component and I create one, I also do not put it into its own package immediately. If I need it in another project I would copy & paste if it is a small component or decide to make a package out of it.

##Component Settings At some place in the application we want to use the new component and it should be very easy to do so. I want to use the component in my application like this:

    // /views/someWidget/someTemplate.html
    <template name="someTemplate">
        {{> scrollableBtnGroup settings=settings}}
    </template>
    // /views/someWidget/someTemplate.js
    Template.someTemplate.helpers({
        settings: function(){
            return {
                ...
            }
        }
    })

This way you have a settings object that you can easily extend the way you like. You could of course also pass options as single parameters like this:

    {{> scrollableBtnGroup optionOne=optionOne optionTwo=optionTwo ...}}

I prefer the first option in most cases since it is more easy to extend and more readable for me.

##The structure of a component Creating components is not yet as smooth as it should be and this has some reasons. I experienced some strange behavior while developing components that includes wired stuff depending on the Iron Router, reactive template variables and so on. Especially if you reuse a component multiple times in one screen it can lead to some strange behaviour. But I figured that there is a good solution that works quite nicely for me.

###Component Class Every logic aspect and every settings, states, functionalities and the like are handled by the components own JavaScript class. The idea is to instanciate a component object for each component template that is created.

###Component Controller Since you sometimes may want to reuse the component multiple times, like in a list, you want to keep track of the components that are initiated. Especially if your user could for example open and close tabs, components are created and destroyed often. Every template has a guid property and we can use this to relate the component template to its component class object. The controller is used to add, get and remove objects if the template is created, needs something or is destroyed. If you want to access a component object from outside this file, you could also make the component controller globally accessible and write a little searchByKeyValue function.

###Template Helpers To access the initiated component object from the component template we write helpers that directly relate to the component object, that is associated to the template.

###Component skeleton All in all this could look something like this:

    // client/views/components/scrollableBtnGroup.js
 
    // The component controller keeps track about instanciated component objects
    var SBGCtrl = {
        components: {},
        add: function(guid, settings) {
            this.components[guid] = new ScrollableBtnGroup(settings);
        },
        get: function(guid) {
            return this.components[guid];
        },
        remove: function(guid) {
            this.components[guid]["delete"]();
            this.components[guid] = null;
            delete this.components[guid];
        }
    };
 
    // The component class
    var ScrollableBtnGroup = (function() {
        ScrollableBtnGroup.prototype["delete"] = function() {};
 
        ScrollableBtnGroup.prototype.someFunction = function(value) {
            ...
        };
        return ScrollableBtnGroup;
    })();
 
    // The component template helpers
    Template.scrollableBtnGroup.created = function() {
        this.data['guid'] = this.__component__.guid;
        SBGCtrl.add(this.__component__.guid, this.data.settings);
    };
 
    Template.scrollableBtnGroup.destroyed = function() {
        SBGCtrl.remove(this.__component__.guid);
    };
 
    Template.scrollableBtnGroup.helpers({
        someVariable: function() {
            return SBGCtrl.get(this.guid).settings.someVariable;
        }
    });

We can see, that when a template is created we add it to the template controller and once it is destroyed it is removed from the controller. To access the template object from a template we create helpers. Also it is important to add the guid of the template to its data context when the template is created. The guid of a template is not accessible within a template helper.

##SBG Class Now we already have some basic stuff figured out for our little component in general. Let's think about what should happen if the SBG component is created. It would be good to set some basic and default values. If we do this we can always be sure those values exist and we do not have to worry and check if the value exists every time. This can be done in the constructor of the component class like this:

    // /client/views/components/scrollableBtnGroup.js
 
    // The component class
    var ScrollableBtnGroup = (function() {
 
        ScrollableBtnGroup.prototype.defaults = {
            viewBtnCount: 6,
            buttons: [],
            prevLabel: '<',
            nextLabel: '>'
        };
 
        function ScrollableBtnGroup(settings) {
            var k, v, _ref;
            this.settings = settings;
            _ref = this.defaults;
            for (k in _ref) {
                v = _ref[k];
                if (!this.settings[k]) {
                    this.settings[k] = v;
                }
            }
        }
 
        ...
 
        return ScrollableBtnGroup;
    })();

##Scrollable Btn Group Template Also we want to be able to use the settings in our component template. Our basic component template does not much, it shows some prev and next button and also iterates through all buttons that should be visible at the current state.

    // /views/components/scrollableBtnGroup.html
 
    <template name="scrollableBtnGroup">
      <div class="btn-group btn-group-justified">
        {{#if $gt buttonCount viewBtnCount}}
          <div class="btn-group">
            <button type="button" class="btn btn-default scrollable-btn-group-prev">{{{prevLabel}}}</button>
          </div>
        {{/if}}
        {{#each buttonsActive}}
          <div class="btn-group">
            <button type="button" class="btn btn-default" data-value="{{value}}">{{label}}</button>
          </div>
        {{/each}}
        {{#if $gt buttonCount viewBtnCount}}
          <div class="btn-group">
            <button type="button" class="btn btn-default scrollable-btn-group-next">{{{nextLabel}}}</button>
          </div>
        {{/if}}
      </div>
    </template>

Note that the $gt helper comes from the Meteor handle bar helpers package.

Also you can see that the prevLabel and nextLabel are in three and not two brackets {{{prevLabel}}}. This means you can also pass html as options and if you want to add some font-icons, images or what ever, you can do so as well.

Since the variables of the component are only available through the component object, we need to write some helpers in order to access those directly from the template. For that we use the component controller to the component object based on the templates guid.

    // /client/views/components/scrollableBtnGroup.js
    Template.scrollableBtnGroup.helpers({
        viewBtnCount: function() {
            return SBGCtrl.get(this.guid) && SBGCtrl.get(this.guid).settings.viewBtnCount;
        },
        buttonCount: function() {
            return SBGCtrl.get(this.guid) && SBGCtrl.get(this.guid).settings.buttons.length;
        },
        buttons: function() {
            return SBGCtrl.get(this.guid) && SBGCtrl.get(this.guid).settings.buttons;
        },
        prevLabel: function() {
            return SBGCtrl.get(this.guid) && SBGCtrl.get(this.guid).settings.prevLabel;
        },
        nextLabel: function() {
            return SBGCtrl.get(this.guid) && SBGCtrl.get(this.guid).settings.nextLabel;
        },
        buttonsActive: function() {
            return SBGCtrl.get(this.guid) && SBGCtrl.get(this.guid).getActiveButtons();
        }
    });

Note that I guarded the specific component object since due to the reactivity it might happen sometimes, that in some state the component object is null. You can find a great article about that by David Weldon here.

##Show active buttons One key part of this component is, that it does not show every button if there are more than a specified number. In order to do that we have the little getActiveButtons function that we use in the buttonsActive helper. The calculation of the amount of buttons and which buttons should be rendered has to be reactive. If we click on the prev or next button, it should reactively update the buttons that are rendered. This can be easily done by a reactive variable.

    // /views/components/scrollableBtnGroup.js
    var ScrollableBtnGroup;
 
    var ScrollableBtnGroup = (function() {
      ScrollableBtnGroup.prototype.currentIndex = 0;
 
      ScrollableBtnGroup.prototype._ciDeps = new Deps.Dependency;
 
      ...
 
      ScrollableBtnGroup.prototype.setCurrentIndex = function(value) {
        if (this.currentIndex === value) {
          return;
        }
        this.currentIndex = value;
        return this._ciDeps.changed();
      };
 
      ScrollableBtnGroup.prototype.getCurrentIndex = function() {
        this._ciDeps.depend();
        return this.currentIndex;
      };
 
      ScrollableBtnGroup.prototype.next = function() {
        var ci;
        ci = this.getCurrentIndex();
        if ((ci + this.settings.viewBtnCount) >= this.settings.buttons.length) {
          return;
        }
        return this.setCurrentIndex(ci + 1);
      };
 
      ScrollableBtnGroup.prototype.prev = function() {
        var ci;
        ci = this.getCurrentIndex();
        if (ci <= 0) {
          return;
        }
        return this.setCurrentIndex(this.getCurrentIndex() - 1);
      };
 
      ScrollableBtnGroup.prototype.getActiveButtons = function() {
        var endIndex, startIndex;
        startIndex = this.getCurrentIndex();
        endIndex = startIndex + this.settings.viewBtnCount;
        return this.settings.buttons.slice(startIndex, endIndex);
      };
 
      return ScrollableBtnGroup;
 
    })();

In this functions we make the currentIndex variable reactive. There are also some packages to make the creation of reactive variables even more easy. Also we added the functionality for the next and prev buttons.

##Enable the prev and next buttons Next, the component should react on the user. If you click on the prev or next button, the active buttons should update accordingly. Since we are using a reactive variable already, this is a really easy task. We simply bind the event handlers and use our component object to handle the action.

    Template.scrollableBtnGroup.events({
        'click .scrollable-btn-action': function(evt, tpl) {
            SBGCtrl.get(tpl.data.guid)[$(evt.currentTarget).data('action')](this);
        }
    });

Note that there is not a event handler for each button itself, but instead I bind every click on something with the class .scrollable-btn-action to the function of the component class specified through the data-action property. Because you sometimes need the datacontext of the component I pass it along as well.

##Action hooks If you use the component you sometimes want to react on an action of the component and you can do this by specifying some hooks. For example we want to do something if a button is clicked. For that we simply pass a function that should be called in the settings like this:

    settings = {
        viewBtnCount: 6,
        buttons: buttons,
        _id: selectedShift._id,
        onSet: function(value){ ... }
    }

Now in our component object we check for the onSet hook and call it if it exists.

    ...
    set: function(tplData) {
        this.setSelectedButton(tplData.value);
        if (_.isFunction(this.settings.onSet)) {
          this.settings.onSet(tplData.value);
        }
    }
    ...

##Conclusion Creating components, that have a single purpose is a really nice thing to do since you can reuse those and save a lot of time. There is still no standard way to create components and I guess since blaze will develope there will be more easy ways to do so. I hope that there will be a standard component framework in meteor core, but since then I will work with the solution posted above.

©️ 2024 Digitale Kumpel GmbH. All rights reserved.