8
\$\begingroup\$

I looked up how to use the PubSub pattern in JS and came across a few articles. I wanted to have two way data binding, so when a one of an Object's Properties value was set, it would update x number of subscribers.

It sounds confusing, but here's the relevant sections of my code:

// data-cluckles-jumbotron="padding" adds the input to the subscribers array
// in clucklesEditor.jumbotron.padding.subscribers
// In context Jumbotron is a constructor function, padding is a property
// So this.padding = {}
<input type="number" data-cluckles-jumbotron="padding" value="30" placeholder="30" min="1" step="1" />

Here's the Jumbotron constructor function. As you can see it inherits from ThemeModifiers, which is where the two way data binding, and the PubSub is setup.

var Jumbotron = function (editor) {
    ThemeModifier.call(this, editor); // Call parent constructor

    this.subscriberDataAttribute = 'data-cluckles-jumbotron';

    // Configure the Modifiers
    this.bg = {
        variable:           '@jumbotron-bg',
        subscribeProperty:  'bg',
        changeFn:           this.setBackgroundColor.bind(this),
        subscribers:        [],
        _value:             null
    };

    this.setupDataBinding();
};

// Inherit from parent Prototype and preserve constructor
Jumbotron.prototype             = Object.create(ThemeModifier.prototype);
Jumbotron.prototype.constructor = Jumbotron;

Jumbotron.prototype.setBackgroundColor = function (backgroundColor) {
    this.modifiers.bg.value = backgroundColor;
};

Now here's ThemeModifiers. I have stripped out the non relevant parts:

var ThemeModifier = function (editor) {
    Object.defineProperties(this, {
        'editor': {
            enumerable: false,
            value: editor
        },
        'modifiers': {
            enumerable: false,
            writable: true,
            value: {}
        }
    });
};

/**
 * Configured the Two Way Databinding for the modifiers, which includes
 * binding multiple DOM Element subscribers which have the "data-cluckles-{{type}}" attribute,
 * which makes them update when the modifiers change, and changing the modifiers when the DOM
 * Element's values change.
 * 
 * Example of Two Way Databinding:
 * 
 * editor.jumbotron.setBackgroundColor('#000000'); // Will Update the modifier and all Subscribers
 * <input type="color" data-cluckles-jumbotron="bg" /> // Will Update the modifier and all Subscribers
 * 
 * @returns {undefined}
 */
ThemeModifier.prototype.setupDataBinding = function () {
    var editor = this.editor, // ClucklesEditor instance
        // DOM Element Subscribers                                       // *[data-cluckles-{{type}}] e.g. *[data-cluckles-jumbotron]
        subscribers = Array.prototype.slice.call(document.querySelectorAll('*[' + this.subscriberDataAttribute + ']'));

    // Setup the value accessors, and configure them to Notify the subscribers of changes
    Object.keys(this.modifiers).forEach(function(modifierName) {
        var modifier = this.modifiers[modifierName];

        // If a value property has already been defined, we dont need to attach
        // a generic value accessor methods
        if (!modifier.hasOwnProperty('value')) {
            // Define getters/setters to change the value, apply it, and notify subscribers
            Object.defineProperty(modifier, 'value', {
                get: function () { return this._value; },
                set: function (val) {
                    var unit = 'px'; // Default unit to append (px, em, rem, etc)

                    // If this property requires a suffix unit
                    if (this.suffixUnit) {
                        // Store the raw value
                        this._rawValue = val;

                        // If a custom unit is specified
                        if (this.unit) {
                            // Set the unit to append
                            unit = this.unit;
                        }

                        // Combine the value with the unit
                        this._value = val + unit;
                    } else {
                        // Store the new value
                        this._value = val;
                    }

                    // Queue the modifications to be applied by less
                    editor.queueModifications();

                    // Notify each of the Subscribers of the value change
                    this.subscribers.forEach(function (subscriber) {
                        subscriber.value = val;
                    });
                } 
            });
        }
    }, this);

    // Store the Subscribers, and setup their 'change' listeners
    subscribers.forEach(function (subscriber) {
        // Get the data attribute which should match the subscribeProperty of a modifier
        // which it wants to bind to
        var subscribeToProperty = subscriber.getAttribute(this.subscriberDataAttribute);

        Object.keys(this.modifiers).forEach(function (modifierName) {
            // Get the modifier object
            var modifier = this.modifiers[modifierName];

            // If this modifiers handles the property we want to subscribe to
            if (modifier.subscribeProperty === subscribeToProperty) {
                // Store the subscriber for this modifier
                modifier.subscribers.push(subscriber);

                // Add a change event which will call the change function and pass
                // through the value of the DOM Element
                subscriber.addEventListener('change', function (e) {
                    var suffixUnit = e.target.getAttribute('data-cluckles-unit');

                    // If the DOM Element has a "unit" data binding
                    if (suffixUnit) {
                        // Call the change function and provide the extra suffix
                        modifier.changeFn(e.target.value, suffixUnit);
                    } else {
                        // else call change function as default
                        modifier.changeFn(e.target.value);
                    }
                }, false);
            }
        }, this);
    }, this);
};

All inputs which have data-cluckles-jumbotron="PROPERTY" will automatically be added to the subscribers array of the property which match PROPERTY (e.g. bg). Then when the inputs value changes, that change is sent to the changeFn e.g. Jumbotron#setBackgroundColor.

This means that you can also do the same thing from the console.

In ThemeModifier I have add an Object.defineProperty block, which handles when the value is set and automatically sets the value of each of the inputs in the subscribers array of the associated property.

The full source for this is from my project Cluckles on Github and to see how it works in action you can go to cluckles.com.

It makes sense to me and seems to work well, but I'm not sure if it's the best way to do this? I'm looking for feedback on if this is an effective way to do what I'm trying to achieve.

\$\endgroup\$
3
\$\begingroup\$

From a once over;

  • I would have put the articles URL's you read in the comments up to
  • Your commenting within the functions is too often obvious, like here:

    // Get the modifier object
    var modifier = this.modifiers[modifierName];
    
  • Jumbotron constructor looks good to me, HTML as well
  • This

    if (suffixUnit) {
        // Call the change function and provide the extra suffix
        modifier.changeFn(e.target.value, suffixUnit);
    } else {
        // else call change function as default
        modifier.changeFn(e.target.value);
    }
    

    is equivalent to

    modifier.changeFn(e.target.value, suffixUnit || undefined );
    
  • This

                var unit = 'px'; // Default unit to append (px, em, rem, etc)
    
                // If this property requires a suffix unit
                if (this.suffixUnit) {
                    // Store the raw value
                    this._rawValue = val;
    
                    // If a custom unit is specified
                    if (this.unit) {
                        // Set the unit to append
                        unit = this.unit;
                    }
    
                    // Combine the value with the unit
                    this._value = val + unit;
                } else {
                    // Store the new value
                    this._value = val;
                }
    

    is equivalent to

                if (this.suffixUnit) {
                    this._rawValue = val;
                    this.value = val + ( this.unit || 'px' );
                } else {
                    this._value = val;
                }
    

All in all, I still think I would go for a more known library. But I salute you for writing a readable two-way data binding solution.

\$\endgroup\$
  • \$\begingroup\$ Hey Kojin, thank you very much for the reply and the salute. \$\endgroup\$ – iLikePrograms Nov 24 '14 at 20:09
  • \$\begingroup\$ The code is actually quite a bit different now, if your interested in taking a look and giving it a review I would be thankful for the feedback! \$\endgroup\$ – iLikePrograms Nov 24 '14 at 20:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.