Observing Events in an MVC Application

Continuing from the last post, I’m going to expand on the Profile “application” that was started. For this stage, the requirements will change to now allow the user to edit the Name.

I’ll restate again something that I wasn’t clear about in the first post. These posts are an exploration of which problems frameworks like React/Redux, Angular, Ember, etc. solve by coding myself into those problems and then identifying how different frameworks attempt to solve these problems.

To recap, the application displays a Profile that consists of a name, a profile image and a list of hobbies. The last post finished with a “minimum viable product” that looked something like the screen capture below.

final-0.0.0

In this fictitious software company, the change control board has met and the highest priority feature has been assigned: enable the user to edit their profile name. Our finished product will look like:

final-0.0.1

Retroactively Adding Tests

Something that wasn’t done in the previous post was to create unit tests. Working for a mid-sized business such as Fictitious Company, we use the QUnit as our testing framework. We have several JavaScript applications and we’ve not felt enough pain from it to warrant moving to Jasmine or Mocha or any of the other many options. Using this framework requires the application code to be placed in a separate file from the hosting document. So the contents of the script tag from the previous post have been moved into an app.js file. A new file has been added to the project called tests.html which will serve as the host for the test harness.

app.js:

var DATA = {    
    name: 'John Smith',
    imgURL: 'http://lorempixel.com/100/100/',
    hobbyList: ['coding', 'writing', 'skiing']
};

var App = {
    main: (function () {
        window.addEventListener('load', function () {
            var profile = new App.View.Profile(DATA);
            profile.render(document.body);
        });
    })()
 };

App.View = (function () { //the View 'class' library component
    var _library = {
            Profile: function (model) {
                         var _private = {
                                 div: document.createElement('div'),
                                 heading: document.createElement('h3'),
                                 img: document.createElement('img'),
                                 hobbies: new App.View.Hobbies(model.hobbyList)
                             },
                             _public = {
                                 render: function (container) {
                                             _private.heading.appendChild(document.createTextNode(model.name));
                                             _private.img.setAttribute('src', model.imgURL);
                                             _private.hobbies.render(_private.div);
                                             container.appendChild(_private.div);
                                         }
                             };
                         _private.div.appendChild(_private.heading);
                         _private.div.appendChild(_private.img);
                         _private.div.appendChild(_private.hobbies.element);
                         return _public;
                     },
            Hobbies: function (model) {
                         var _private = {
                                 div: document.createElement('div'),
                                 heading: document.createElement('h5'),
                                 list: document.createElement('ul')
                             },
                             _public = {
                                 element: _private.div,
                                 render: function (container) {
                                             model.map(function (hobby) {
                                                 var li = document.createElement('li');
                                                 li.appendChild(document.createTextNode(hobby));
                                                 _private.list.appendChild(li);
                                             });
                                             _private.div.appendChild(_private.list);
                                             container.appendChild(_private.div);
                                         }
                             };
                         _private.heading.appendChild(document.createTextNode('My hobbies:'));
                         _private.div.appendChild(_private.heading);
                         return _public;
                     }
        };
    return _library;
})();

tests.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset='utf-8'>
        <title>Testing: Your First JavaScript Project</title>
        < script src="../app.js></ script >
        <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.20.0.css">
        < script src="http://code.jquery.com/qunit/qunit-1.20.0.js"></ script >
    </head>
    <body>
        window.addEventListener('load', function () {
            QUnit.test("Given , when ...", function (assert) {
            });
        };
    </body>
</html>

Looking at the code base from the last post, there is one object representing the Model component but it isn’t really anything more than a representation of a Data Transfer Object, two objects in the Views component, and there is no Controller (yet). Based on the previous “requirements”, the filled in tests might look like the following:

Testing the Hobbies View

In the simplistic example from the previous post, calling this a View object is a bit of hyperbole, but we’ll go with it anyway. The public interface of the Hobbies view also has two members, an element attribute and a render method. It is responsible for displaying the current state of the model’s hobbyList attribute. So to test, we need to verify that instantiating a View results in a DOM element that contains an h5 heading and an ul element with li elements for each of the hobbies specified in the model.

Starting at line 15 of the tests.html:

QUnit.test("Given an instance of the Hobbies view, when it is rendered...", function (assert) {
    var mockModel = {
            name: 'John Smith',
            imgURL: 'http://lorempixel.com/100/100/',
            hobbyList: ['coding', 'testing', 'refactoring'],
            addEventListener: function (eventType, listener) {
            }
        },
        hobbiesView = new App.View.Hobbies(mockModel.hobbyList, document.createElement('div'));
    

    assert.equal(hobbiesView.element.children.length, 2, '...then it has 2 HTML elements.');
    assert.equal(hobbiesView.element.children[0].tagName, 'H5', "...then the first child is an H5.");
    assert.ok(hobbiesView.element.children[0].textContent === 'My hobbies:', "...the the H5 contains text: 'My hobbies:'");
    assert.equal(hobbiesView.element.children[1].tagName, 'UL', "...then the second child is an UL.");
    mockModel.hobbyList.forEach(function (hobby, index) {
        assert.ok(hobby === hobbiesView.element.children[1].children[index].textContent, "...then list items match.");
    });
});

To break that down, the testing style is in the given-when-then phrasing. This says which unit of the application and what behavior of that unit is being tested. In this case the rendering of the Hobbies view object. Which is fairly simple, it should contain 2 elements: a level 5 heading with the text “My hobbies:” and an unordered list of items that match the items in the model the view is presenting.

Adding the above to the code results in something like:

test-hobbies

Testing the Profile View

The public interface of the Profile view has a two members, the element attribute and the render method. It is responsible for displaying the current state of the model on the screen. So to test that we need to verify that instantiating a Profile results in a div element with a class of ‘profile-view’ that contains an h3 heading, an img element, and a div with a class of ‘hobbies-view’.

Immediately after the last test, add the following:

QUnit.test("Given an instance of the Profile view, when it is rendered...", function (assert) {
    var mockModel = {
            getName: function () {return 'John Smith';},
            imgURL: 'http://lorempixel.com/100/100/',
            hobbyList: ['coding', 'testing', 'refactoring'],
            addEventListener: function (eventType, listener) {
            }
        },
        profileView = new App.View.Profile(mockModel, document.createElement('div'));


    assert.equal(profileView.element.children.length, 5, '...then it has 5 HTML elements.');
    assert.equal(profileView.element.children[0].tagName, 'H3', '...then the first child is an H3.');
    assert.equal(profileView.element.children[0].textContent, 'John Smith', '...then the H3 contains text: "John Smith".');
    assert.equal(profileView.element.children[1].tagName, 'INPUT', '...then the second child is an input element.');
    assert.equal(profileView.element.children[1].getAttribute('type'), 'text', '...then the input element is a text field.');
    assert.equal(profileView.element.children[1].classList.contains('hidden'), true, '...then the input element is not visible.');
    assert.equal(profileView.element.children[2].getAttribute('class'), 'scl-edit-save-cancel', '...then the third child is the SCL edit-save-cancel component.');
    assert.equal(profileView.element.children[3].tagName, 'IMG', "...then the fourth child is an image.");
    assert.equal(profileView.element.children[3].getAttribute('src'), 'http://lorempixel.com/100/100/', "...then the image url matches the model.");
    assert.equal(profileView.element.children[4].getAttribute('class'), 'hobbies-view', '...then the fifith child is a hobbies-view class.');
});

After modifying the app.js file to include setting the class attribute on the root element of each view, running the tests result in the something like:

test-profile

With all of the previous requirements covered and passing, it’s now time to write the tests that will drive the design for implementing the current set of requirements.

The use case:

Use Case: Edit a Profile Name

Primary Actor: Member (Registered User)

Scope: a fictitious system

Level: User goal

User Story:

As a customer I want to be able to update my profile name so that my identity in the system is accurate.

Preconditions:

  • The profile with editing enabled is presented to the member.

Triggers:

  • The member invokes an edit request on the profile name.

Postconditions:

  • The profile name is saved and an updated view is shown.

Basic flow:

  1. The system provides an editable area populated with the current value of the Profile Name.
  2. The member modifies the Profile Name value until satisfied.
  3. The member saves the edit.
  4. The system saves the new Profile Name, logs the edit event and finishes any necessary post processing.
  5. The system presents the updated view of the Profile Name to the member.

Extensions:

3a. Cancel the edit:

  1. The member invokes a cancel request on the edit profile name.
  2. The system discards any change the member has made, then goes to step 5.

After reviewing the use case that the Business Analysts have documented, it’s time to consult the Profile View section of the UI Specification Document that the User Experience Architect has defined:

7.2.1 Profile View

Description

The Profile View enables the user to review their profile to see how other users of the site will see their identity.

This element will inherit the core styling of the app.css file (see Appendix A).

Behavior

The behavior of this element is contained in its constituent elements.

Elements

7.2.1.1 Profile Name View

Description

The Profile Name View enables users to see and edit the current value of their profile name as it appears on the site.

Behavior

The behavior of the Profile Name View is contained in its elements.

Elements

7.2.1.1.1 Profile Name Label

Description

A text label that displays the current value of the user’s profile name. Positioned at the top left of the Profile Name View as a level 3 heading.

Behavior

N/A

Elements

N/A

7.2.1.1.2 Profile Name Input Field

Description

A text entry field that accepts character string input from the user.
Positioned at the top left of the Profile Name View.

Behavior

Default HTML input @type=text behavior. Defaults to being hidden from view.

Elements

N/A

7.2.1.1.3 Edit-Save-Cancel Component

Description

The Fictitious Company’s Standard Component Library’s Edit-Save-Control.
See Reference 2 for a complete description.

Behavior

See Reference 2 for default behaviors.

For this specification, clicking the Edit button will hide the Profile Name Label and reveal the Profile Name Input Field.

Clicking the Save button will hide the Profile Name Input Field, update the supporting data model with the value entered in the Profile Name Input Field, and reveal the Profile Name Label.

Clicking the Cancel button will hide the Profile Name Input Field and reveal the Profile Name Label.

Elements

See Reference 2.

7.2.1.2 Profile Picture View

Description

The Profile Picture View enables users to see the avatar image associated with their profile. The image will be 100 x 100 pixels.

Behavior

N/A

Elements

N/A

7.2.1.3 Profile Hobbies View

Description

The Profile Hobbies View displays a list of hobbies that the user provided when they joined the site.

Behavior

N/A

Elements

7.2.1.3.1 Profile Hobbies Header

Description

A text label displays the value: “My hobbies:” as a level 5 heading.

Behavior

N/A

Elements

N/A

7.2.1.3.2 Profile Hobbies List

Description

A bulleted list of each hobby.

Behavior

N/A

Elements

N/A

Time to create the feature branch and begin coding; or is it?

Looking back to the UI specification, there is a requirement to use the company’s Standard Component Library for the Edit button that is required on the Profile Name section of the Profile view.

A Standard Component Library (SCL) is a way to keep the “look and feel” of the user interfaces consistent across the various applications that the company has implemented. It helps the user base have a sense of familiarity with an app even if it’s the first time they’ve used it. It also helps the development team with producing new applications a little faster since they don’t have to build everything from scratch with each new application/project.

The Fictitious Company’s SCL has an Edit-Save-Cancel control that allows the user to indicate they would like to Edit a value on the screen, the Edit button is hidden and in its place the Save and Cancel buttons are displayed. This control is to be used to edit the Profile Name. The first step toward implementing will be to configure the Profile View to include the Edit-Save-Control component.

Packaging and utilizing a library from a package management tool is a subject for a different post, but the SCL would normally be brought into the application with a dependency manager. For this post, the library is just going to be added via a script tag.

< script >../scl.js</ script>

The View has a dependency on this library, so that gets passed into it:

App.View = (function (scl) { //the View 'class' library component
    var _library = {
            Profile: function (model) {
                         var _private = {
                                 div: document.createElement('div'),
                                 heading: document.createElement('h3'),
                                 img: document.createElement('img'),
                                 hobbies: new App.View.Hobbies(model.hobbyList)
                             },
                             _public = {
                                 element: _private.div,
                                 render: function (container) {
                                             _private.heading.appendChild(document.createTextNode(model.name));
                                             _private.img.setAttribute('src', model.imgURL);
                                             _private.hobbies.render(_private.div);
                                             container.appendChild(_private.div);
                                         }
                             };
                         _private.div.setAttribute('class', 'profile-view');
                         _private.div.appendChild(_private.heading);
                         _private.div.appendChild(_private.img);
                         _private.div.appendChild(_private.hobbies.element);
                         return _public;
                     },
            Hobbies: function (model) {
                         var _private = {
                                 div: document.createElement('div'),
                                 heading: document.createElement('h5'),
                                 list: document.createElement('ul')
                             },
                             _public = {
                                 element: _private.div,
                                 render: function (container) {
                                             model.map(function (hobby) {
                                                 var li = document.createElement('li');
                                                 li.appendChild(document.createTextNode(hobby));
                                                 _private.list.appendChild(li);
                                             });
                                             container.appendChild(_private.div);
                                         }
                             };
                         _private.div.setAttribute('class', "hobbies-view");
                         _private.heading.appendChild(document.createTextNode('My hobbies:'));
                         _private.div.appendChild(_private.heading);
                         _private.div.appendChild(_private.list);
                         return _public;
                     }
        };
    return _library;
})(FC.SCL);

The library is declared in the Fictitious Company namespace FC as SCL. The View library will refer to it as scl.

Now we can start implementing the view as specified in section 7.2.1.1.2 of the UI Spec.

The Profile View needs a new element: a profile name input field. First thing to do is write a test for it:

QUnit.test("Given an instance of the Profile view, when it is rendered...", function (assert) {
    var dto = {
            name: 'John Smith',
            imgURL: 'http://lorempixel.com/100/100/',
            hobbyList: ['coding', 'testing', 'refactoring']
        },
        v = new App.View.Profile(dto);

    v.render(document.createElement('div'));

    assert.equal(v.element.children.length, 4, '...then it has 3 HTML elements.');
    assert.equal(v.element.children[0].tagName, 'H3', '...then the first child is an H3.');
    assert.equal(v.element.children[0].textContent, 'John Smith', '...then the H3 contains text: "John Smith".');
    assert.equal(v.element.children[1].tagName, 'INPUT', '...then the second child is an input field.');
    assert.equal(v.element.children[1].getAttribute('type'), 'text', '...then the input element is a text field.');
    assert.equal(v.element.children[1].getAttribute('style'), 'display:none', '...then the input element is not visible.');
    assert.equal(v.element.children[2].tagName, 'IMG', "...then the second child is an image.");
    assert.equal(v.element.children[2].getAttribute('src'), 'http://lorempixel.com/100/100/', "...then the image url matches the model.");
    assert.equal(v.element.children[3].getAttribute('class'), 'hobbies-view', '...then the third child is a hobbies-view class.');
});

Then run the test and see the page fill up with red:

test-input-fail

Now we update the Profile to make it pass:

Profile: function (model) {
             var _private = {
                     div: document.createElement('div'),
                     heading: document.createElement('h3'),
                     input: document.createElement('input'),
                     img: document.createElement('img'),
                     hobbies: new App.View.Hobbies(model.hobbyList)
                 },
                 _public = {
                     element: _private.div,
                     render: function (container) {
                                 _private.heading.appendChild(document.createTextNode(model.name));
                                 _private.input.value = model.name;
                                 _private.img.setAttribute('src', model.imgURL);
                                 _private.hobbies.render(_private.div);
                                 container.appendChild(_private.div);
                             }
                 };
             _private.div.setAttribute('class', 'profile-view');
             
             _private.div.appendChild(_private.heading);
             
             _private.input.setAttribute('type', 'text');
             _private.input.setAttribute('style', 'display:none');
             _private.div.appendChild(_private.input);
             
             _private.div.appendChild(_private.img);
             
             _private.div.appendChild(_private.hobbies.element);
             
             return _public;
         },

And now are test runner is nice and green again!

test-input-pass

The next specification was to add the Edit-Save-Cancel component to the Profile. Again, we go back to the test script:

QUnit.test("Given an instance of the Profile view, when it is rendered...", function (assert) {
    var dto = {
            name: 'John Smith',
            imgURL: 'http://lorempixel.com/100/100/',
            hobbyList: ['coding', 'testing', 'refactoring']
        },
        v = new App.View.Profile(dto);

    v.render(document.createElement('div'));

    assert.equal(v.element.children.length, 5, '...then it has 5 HTML elements.');
    assert.equal(v.element.children[0].tagName, 'H3', '...then the first child is an H3.');
    assert.equal(v.element.children[0].textContent, 'John Smith', '...then the H3 contains text: "John Smith".');
    assert.equal(v.element.children[1].tagName, 'INPUT', '...then the second child is an input element.');
    assert.equal(v.element.children[1].getAttribute('type'), 'text', '...then the input element is a text field.');
    assert.equal(v.element.children[1].getAttribute('style'), 'display:none', '...then the input element is not visible.');
    assert.equal(v.element.children[2].getAttribute('class'), 'scl-edit-save-cancel', '...then the third child is the SCL edit-save-cancel component.');
    assert.equal(v.element.children[3].tagName, 'IMG', "...then the fourth child is an image.");
    assert.equal(v.element.children[3].getAttribute('src'), 'http://lorempixel.com/100/100/', "...then the image url matches the model.");
    assert.equal(v.element.children[4].getAttribute('class'), 'hobbies-view', '...then the fifith child is a hobbies-view class.');
});

Which results in the predictable test run:

test-esc-fail

Update the code to make the tests pass:

Profile: function (model) {
             var _private = {
                     div: document.createElement('div'),
                     heading: document.createElement('h3'),
                     input: document.createElement('input'),
                     img: document.createElement('img'),
                     hobbies: new App.View.Hobbies(model.hobbyList)
                 },
                 _public = {
                     element: _private.div,
                     render: function (container) {
                                 _private.heading.appendChild(document.createTextNode(model.name));
                                 _private.input.value = model.name;
                                 _private.img.setAttribute('src', model.imgURL);
                                 _private.hobbies.render(_private.div);
                                 container.appendChild(_private.div);
                             }
                 };
             _private.div.setAttribute('class', 'profile-view');
             
             _private.div.appendChild(_private.heading);
             
             _private.input.setAttribute('type', 'text');
             _private.input.setAttribute('style', 'display:none');
             _private.div.appendChild(_private.input);

             _private.input.editComponent = new scl.EditSaveCancel(_private.div);
             
             _private.div.appendChild(_private.img);
             
             _private.div.appendChild(_private.hobbies.element);
             
             return _public;
         },

Which passes:

test-esc-pass

And the application looks almost as specified:

preview-with-edit

The layout will be addressed at the end. The next requirement in the specification 7.2.1.1.3: Behavior is to swap the H3 with an input when the Edit button is clicked. Time for a new test:

QUnit.test("Given a rendered Profile view, when its Edit button is clicked...", function (assert) {
    var dto = {
            name: 'John Smith',
            imgURL: 'http://lorempixel.com/100/100/',
            hobbyList: ['coding', 'testing', 'refactoring']
        },
        v = new App.View.Profile(dto),
        editButton, inputField, nameLabel;

    v.render(document.createElement('div'));
    
    editButton = v.element.getElementsByClassName('scl-edit-button')[0];
    editButton.click();
    
    inputField = v.element.children[1];
    nameLabel = v.element.children[0];

    assert.equal(inputField.classList.contains('visible'), true, '...then the profile name input field is visible.');
    assert.equal(nameLabel.classList.contains('hidden'), true, '...then the profile name label is not visible.');
});

Run the test.

edit-click-fail

A user initiated event is going to occur – the click event on the Edit button. The View is responsible for responding to user actions. So we need to “wire up” the Profile view to listen for a click event on the Edit button. The SCL component we’re using provides an addEventListener method that allows us to define a function that can be executed when the component dispatches an editClicked event.

Profile: function (model) {
             var _private = {
                     div: document.createElement('div'),
                     heading: document.createElement('h3'),
                     input: document.createElement('input'),
                     img: document.createElement('img'),
                     hobbies: new App.View.Hobbies(model.hobbyList),
                     editButtonClickHandler: function () {
                        _private.heading.classList.remove('visible');
                        _private.heading.classList.add('hidden');
                        _private.input.classList.remove('hidden');
                        _private.input.classList.add('visible');
                     }
                 },
                 _public = {
                     element: _private.div,
                     render: function (container) {
                                 _private.heading.appendChild(document.createTextNode(model.name));
                                 _private.input.value = model.name;
                                 _private.img.setAttribute('src', model.imgURL);
                                 _private.hobbies.render(_private.div);
                                 container.appendChild(_private.div);
                             }
                 };
             _private.div.setAttribute('class', 'profile-view');
             
             _private.div.appendChild(_private.heading);
             
             _private.input.setAttribute('type', 'text');
             _private.input.setAttribute('class', 'hidden');
             _private.div.appendChild(_private.input);

             _private.input.editComponent = new scl.EditSaveCancel(_private.div);
             _private.input.editComponent.addEventListener('editClicked', _private.editButtonClickHandler);
             
             _private.div.appendChild(_private.img);
             
             _private.div.appendChild(_private.hobbies.element);
             
             return _public;
         },

Run the test.

edit-click-pass

The next requirement states:

Clicking the Save button will hide the Profile Name Input Field, update the supporting data model with the value entered in the Profile Name Input Field, and reveal the Profile Name Label.

There’s a requirement to modify the data model. We’re only testing the Profile View right now, so to verify that we’re accomplishing that aspect of the requirement the test will focus on verifying that the input field is hidden and the label is visible. After that test is passing, we’ll focus on the Model updates.

QUnit.test("Given a rendered Profile view, when its Save button is clicked...", function (assert) {
    var dto = {
            name: 'John Smith',
            imgURL: 'http://lorempixel.com/100/100/',
            hobbyList: ['coding', 'testing', 'refactoring']
        },
        v = new App.View.Profile(dto),
        saveButton, inputField, nameLabel;

    v.render(document.createElement('div'));
    v.element.getElementsByClassName('scl-edit-button')[0].click();

    saveButton = v.element.getElementsByClassName('scl-save-button')[0];
    saveButton.click();

    inputField = v.element.children[1];
    nameLabel = v.element.children[0];

    assert.equal(inputField.classList.contains('hidden'), true, '...then the profile name input field is not visible.');
    assert.equal(nameLabel.classList.contains('visible'), true, '...then the profile name label is visible.');
});

(This post is getting really, really long, so I’m going to skip the screen shots of the test runner for the failing tests from here on.)

The application is supposed to be following the MVC design pattern as mentioned in the previous post. To this point, there has been no Controller. Now that the Model is going to be updated, an object needs to be responsible for that logic. So we’ll build a Profile Controller that will observe events in the View and update the Model accordingly:

Profile: function (model) {
             var _private = {
                     div: document.createElement('div'),
                     heading: document.createElement('h3'),
                     input: document.createElement('input'),
                     img: document.createElement('img'),
                     hobbies: new App.View.Hobbies(model.hobbyList),
                     editButtonClickHandler: function () {
                        _private.heading.classList.remove('visible');
                        _private.heading.classList.add('hidden');
                        _private.input.classList.remove('hidden');
                        _private.input.classList.add('visible');
                     },
                     saveButtonClickHandler: function () {
                        _private.heading.classList.add('visible');
                        _private.heading.classList.remove('hidden');
                        _private.input.classList.add('hidden');
                        _private.input.classList.remove('visible');
                     }
                 },
                 _public = {
                     element: _private.div,
                     render: function (container) {
                                 _private.heading.appendChild(document.createTextNode(model.name));
                                 _private.input.value = model.name;
                                 _private.img.setAttribute('src', model.imgURL);
                                 _private.hobbies.render(_private.div);
                                 container.appendChild(_private.div);
                             }
                 };
             _private.div.setAttribute('class', 'profile-view');
             
             _private.div.appendChild(_private.heading);
             
             _private.input.setAttribute('type', 'text');
             _private.input.setAttribute('class', 'hidden');
             _private.div.appendChild(_private.input);

             _private.input.editComponent = new scl.EditSaveCancel(_private.div);
             _private.input.editComponent.addEventListener('editClicked', _private.editButtonClickHandler);

             _private.input.editComponent.addEventListener('saveClicked', _private.saveButtonClickHandler);
             
             _private.div.appendChild(_private.img);
             
             _private.div.appendChild(_private.hobbies.element);
             
             return _public;
         },

Run the test.

save-click-pass

Lather, rinse, repeat for the cancel button.

Now the last piece of the puzzle is that the View is supposed to display the current value of the Model’s name attribute when displaying the H3 element. But if you execute this code, and change the value in the input field, the value is not updated in the header. This is because the View is not “listening” to the Model and the Model is not publishing any events for the View to listen for anyway.

Update the tests to ensure that we verify that the View renders the correctly when the Profile Name is modified. Then modify the Model so that it can dispatch events and modify the View so that it listens for the nameChanged event coming from the Model.

QUnit.test("Given a rendered Profile view, when the value in the Profile Name Input Field is modified and Saved...", function (assert) {
    var dto = {
        name: 'John Smith',
        imgURL: 'http://lorempixel.com/100/100/',
            hobbyList: ['coding', 'testing', 'refactoring']
        },
        v = new App.View.Profile(dto),
        saveButton, inputField, nameLabel;

    v.render(document.createElement('div'));
    v.element.getElementsByClassName('scl-edit-button')[0].click();
    saveButton = v.element.getElementsByClassName('scl-save-button')[0];

    inputField = v.element.children[1];
    nameLabel = v.element.children[0];

    inputField.value = 'New Value';
    saveButton.click();

    assert.equal(nameLabel.textContent, 'New Value', '...then the profile name label displays the modified value.');
    assert.equal(dto.name, 'New Value', '...then the name attribute of the model is set to the modified value.');
});

We are trying to adhere to the MVC pattern. However, the model that we have is just a DTO. The View can’t subscribe to any events on this model, so we first have to redefine the Model to provide the addEventListener method. We also don’t have a mechanism for mutating the state of the Model – which means that we have no mechanism for dispatching those events.

App.Model = (function () {
    var _library = {
        Profile: function (name, imgSrc, hobbies) {
            var _private = {
                    name: name,
                    imgURL: imgSrc,
                    hobbyList: hobbies,
                    handlers: {
                        profileNameChanged: []
                    },
                    dispatchEvent: function(evt) {
                        _private.handlers[evt.type].forEach(function (handler) {
                            handler(evt);
                        });
                    }
                },
                _public = {
                    getName: function () {
                        return _private.name;
                    },
                    setName: function (value) {
                        if (value !== _private.name) {
                            var profileNameChanged = new CustomEvent('profileNameChanged', {detail:{oldValue:_private.name, newValue:value}});
                            _private.name = value;
                            _private.dispatchEvent(profileNameChanged);
                        }
                    },
                    addEventListener: function (eventType, listener) {
                        _private.handlers[eventType].push(listener);
                    },
                    imgURL: _private.imgURL,
                    hobbyList: _private.hobbyList
                };
            return _public;
        }
    };

    return _library;
})();

After redefining what the Model.Profile’s interface is, we need to go refactor the previous test cases so that the mock Model.Profile object provides the same interface. See the finished code at the end of the post or in my GitLab repo.

With event listeners, testing was a little awkward. The mock objects needed to either receive and event listener or dispatch events to one. Here’s an example of verifying that an object successfully subscribed an event listener:

QUnit.test("Given a Profile view, when the view is constructed...", function (assert) {
    var mockModel = {
            getName: function () { return 'foo'; },
            addEventListener: function (eventType, listener) {
                assert.equal(eventType, 'profileNameChanged', "...then the Profile subscribes to the Model's 'profileNameChanged' event.");
            },
            hobbyList: []
        },
        profile = new App.View.Profile(mockModel, document.createElement('div'));
});

And here is an example of testing that an object dispatches events:

QUnit.test("Given a Profile model, when a subscriber exists and the name is changed...", function (assert) {
    var profile = new App.Model.Profile(),
        subscriber = function (evt) {
            assert.equal(evt.detail.newValue, 'My New Name', '...then the subscriber is notified of the event.');
        };

    profile.addEventListener('profileNameChanged', subscriber);
    profile.setName('My New Name');
});

Now the last piece is to modify the CSS so that the elements are laid out according to the UI Specification:

.visible {
    display: inline-block;
}

.hidden {
    display: none;
}

.profile-view h3  {
    float: left;
    margin: 3px;
}

.profile-view .scl-edit-save-cancel {
    float: left;
}

.profile-view input {
    float: left;
}

.profile-view img {
    display: block;
    clear: both;
}

Which results in:

final-with-css final-with-css-editmode

And there it is, an implementation of the MVC pattern without the need for a framework – yet. We are making use of a library, which was modified to keep it consistent with the concepts presented so far. The render method has been refactored out of the Views and replaced with an event based architecture. This allows the Model objects to store the logic for determining if a state change has occurred, and when it does, it dispatches events to the View object(s) that handle those events by updating only those elements that display the mutating attributes of the Model.

One thing to notice is the public addEventListener method and the private handlers and dispatch members. This code was duplicated in all of the objects. It would have been better to define an object that manages that work and have the M-V-C objects inherit that behavior. The next post will introduce a Publish-Subscribe pattern from the Fictitious Company’s standard libraries as well as a little more clean up in the View library. Data access has also not yet been explored and will introduce even more complexity to the system.

Full code is available at: https://gitlab.com/pct/YourFirstJavaScriptProject-0.0.1/tree/master/

 

Advertisements

2 thoughts on “Observing Events in an MVC Application

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s