Showing posts with label observer pattern. Show all posts
Showing posts with label observer pattern. Show all posts

Thursday, March 2, 2017

Object-Oriented JavaScript in the UI [4]

The last piece of this OO JavaScript UI is the bit that handles the form for adding jobs to the JobManager.

This is not a Real World Example...

In a real-world application scenario, this would probably be a lot more complicated — The current popular approaches would probably have it posting data to a web-service end-point that would return a JSON result-set that would be (or include) the validated job-data, possibly with some additional data indicating the success or failure of the post-attempt. That job-data would then be passed to the JobManager's addJob method, and the various displays would update accordingly.

The POC-level approach here doesn't do any of that, though there are a couple of places where that sort of connection to a server-side process could be introduced. Me, I think I would drop it into JobManager.addJob, but that's a knee-jerk thought, and other places might well be better.

At any rate, here's the last piece of the puzzle...

The JobAdderForm class

JobAdderForm attaches to the job-entry form in the main UI:

Create a New Job


It is responsible for accepting data for new jobs, building a data-structure that can be submitted to JobManager.addJob, and submitting it. Its implementation looks like this:

function JobAdderForm( wrapper, form )
{
    /*
     * Provides the functionality for adding a job to the JobManager
     * 
     * wrapper ........... (element or string, required) The element 
     *                     (or the ID of the element) that wraps all 
     *                     of the UI.
     */
    // Processing that needs to happen before definition, if any
    if ( typeof wrapper == 'string' )
    { wrapper = document.getElementById( wrapper ); }
    if ( typeof form == 'string' )
    { form = document.getElementById( form ); }
    // Instance properties:
    //  + Private properties
    var _cancelControl = null;
    var _contactField = null;
    var _createControl = null;
    var _form = form;
    var _nameField = null;
    var _numberField = null;
    var _startControl = null;
    var _wrapper = wrapper;
    var _jobManager = new JobManager();
    // - Find the job-fields and -actions elements, and assign 
    //   them as appropriate
    allChildren = wrapper.getElementsByTagName( '*' );
    for( ci=0; ci<allChildren.length; ci++ )
    {
        child = allChildren[ ci ];
        // data-jobfield
        jobField = child.getAttribute( 'data-jobfield' );
        switch( jobField )
        {
            case 'contact':
                _contactField = child;
                break;
            case 'name':
                _nameField = child;
                break;
            case 'number':
                _numberField = child;
                break;
        }
        // data-jobaction
        jobAction = child.getAttribute( 'data-jobaction' );
        switch( jobAction )
        {
            case 'cancelJob':
                _cancelControl = child;
                break;
            case 'createJob':
                _createControl = child;
                break;
            case 'startJob':
                _startControl = child;
                break;
        }
    }
    // TODO: This is where any validation that the REQUIRED fields 
    //       and actions exist should be done.
    // Initialize as needed based on incoming arguments
    // Instance methods:
    //  + public methods
    this.addJobToManager = function()
    {
        /*
         * Collects the job-data from the form, generates a new 
         * data-object with all the job-data in it, and adds it 
         * to the JobManager.
         */
        newJob = {
            'contact':_contactField.value,
            'name':_nameField.value,
            'number':_numberField.value,
            };
        _jobManager.addJob( newJob );
        this.clearForm();
    }
    this.clearForm = function()
    {
        /*
         * Clears all of the fields in the job-adder form
         */
        if ( _contactField )
        { _contactField.value = ''; }
        if ( _nameField )
        { _nameField.value = ''; }
        if ( _numberField )
        { _numberField.value = ''; }
    }
    this.hide = function()
    {
        /*
         * Hides the form
         */
        _form.style.display = 'none';
    }
    this.show = function()
    {
        /*
         * Shows the form
         */
        _form.style.display = 'block';
    }
    // Event-like methods of the instance
    // Processing that needs to happen before returning the instance, if any
    // - Attach events to controls
    if ( _cancelControl )
    {
        _cancelControl.jobAdderForm = this;
        _cancelControl.addEventListener( 'click', function( event )
            {
                this.jobAdderForm.clearForm();
                this.jobAdderForm.hide();
            }
        );
    }
    if ( _createControl )
    {
        _createControl.jobAdderForm = this;
        _createControl.addEventListener( 'click', function( event )
            {
                this.jobAdderForm.addJobToManager();
                this.jobAdderForm.hide();
            }
        );
    }
    if ( _startControl )
    {
        _startControl.jobAdderForm = this;
        _startControl.addEventListener( 'click', function( event )
            {
                this.jobAdderForm.show();
            }
        );
        // If there is a start-control, it's responsible for showing 
        // the form, so hide the form...
        this.hide();
    }
    // Return the instance
    return this;
}

The interface:

Properties
_cancelControl [private]
The UI control (a button, link, or other element) that cancels a job-submission.
_contactField [private]
The text-field that will contain the contact property-value of the new job.
_createControl [private]
The UI control (a button, link, or other element) that submits a job-submission.
_form [private]
The outermost element of the form that contains all of the _*Field elements and the _*Control elements associated with the form. This probably won't include the _startControl (since it should reside outside the _form, lest it be hidden).
_jobManager [private]
The JobManager object that new job-submissions will be sent to.
_nameField [private]
The text-field that will contain the name property-value of the new job.
_numberField [private]
The text-field that will contain the number property-value of the new job.
_startControl [private]
The UI control (a button, link, or other element) that starts the job-submission process (by showing the form).
_wrapper [private]
The element that contains the entire UI for the application, including the _form and all of the _*Control elements.
Methods
addJobToManager()
Gathers the values from the _*Field elements, builds a Job-compatible data-structure from them, and sends that to JobManager.addJob.
clearForm()
Clears the values of all the _*Field elements.
hide()
Hides the _form element.
show()
Shows the _form element.

JobAdderForm has no public properties — the wrapping UI element, the form and all of the fields and controls are auto-assimilated using a process much like the one built out for JobListView early in the initialization process of the object's construction. While it wouldn't be too difficult, I think, to allow items to be specified as part of a parameters argument (as has been the pattern elsewhere), but my gut feeling was to auto-connect everything involved. That's simpler from an integration standpoint (and it was less code for me to write, to boot).

All of its method are pretty straightforward, too. The only potential caveat is that as things stand right now, there is no checking performed with regards to whether the various _*Field fields are found and associated. Again, since this is pretty much a POC-level effort on my part, I simply didn't want to take the time to implement that level of initialization-validation.

Putting it All Together

With all of the markup-template in place (with all of the necessary data-* attributes), and the full object-library included, the only thing remaining is to initialize the set of objects. That's a fairly small chunk of code:

// Create some test jobs for inclusion into the initial view
testJobs = [
    { 'number':'OOK00001', 'name':'Job Number One', 'contact':'Joe Smith' },
    { 'number':'WOK00001', 'name':'Job Number Two', 'contact':'John Doe' },
    { 'number':'OOK00002', 'name':'The Third Job', 'contact':'Richard Roe' },
    { 'number':'WOK00002', 'name':'Fourth Is This Job', 'contact':'Pat Jones' },
    { 'number':'OOK00003', 'name':'The Fifth Job', 'contact':'Richard Roe' },
    { 'number':'WOK00003', 'name':'Sixth Is This Job', 'contact':'Pat Jones' },
];

// Create the main job-manager instance:
jobManager = new JobManager( testJobs );

// Initialize the main job-list view, with all the initial sort-settings
currentJobListParameters = {
    'top':'currentJobList',
    'listElement':'managedJobsList',
    'sortField':'number',
    'sortOrder':'up',
};
mainListView = new JobListView( currentJobListParameters );

// Create the job-contact-list view, with all the initial sort-settings
jobContactListParameters = {
    'top':'jobContactList',
    'listElement':'managedContactlist',
};
jobContactList = new JobContactList( jobContactListParameters );

// Create the job-name-list view, with all the initial sort-settings
jobNameListParameters = {
    'top':'jobNameList',
    'listElement':'managedNameList',
};
jobNameList = new JobNameList( jobNameListParameters );

// Set up the job-counter(s)
mainJobCounter = new JobCounter( 'jobCounter' );

// Set up the job-adder form
jobAdderForm = new JobAdderForm( 'ui-example', 'newJobForm' );

// After all the initialization is done, and all the objects' inter-relationships 
// are established, start the job-update cycle across all of the displays:
jobManager.jobsChanged();

And here it is, live:

Current Jobs [job-count]
Job Number Job Title Job Contact
number name contact
Create a New Job


All Jobs
name
All Job Contacts
contact


Is This an MVC...?

Eh, maybe.

If it isn't a Model-View-Controller, it certainly shares some of that pattern's structure. Consider:

JobManager is Model-like
It directly manages the data of the application;
It also directly manages all of the logic (such as there is) of the application, and could be modified to manage any rules around the data, if any were needed;
The display-objects (JobDisplayList, JobContactList, JobNameList, and JobCounter) are definitely Views
That's what they do: Displaying data from (or derived from) the JobManager
Where it gets questionable, I think, is in the Controller part of the pattern. There's no one, single controller-object in this mix, though the JobAdderForm arguably comes closest. Any of the various sort-controls in the JobDisplayList are also controller-like, and at some level, so is the entire page that the UI resides in.

If you want to play with the code, it's available for download. It runs well enough on my local environment as a local HTML page and JavaScript file (that's in the current version of Google Chrome, your mileage may vary from browser to browser):

6.9kB

Tuesday, February 28, 2017

Object-Oriented JavaScript in the UI [3]

Today I'm going to dive into the code for implementing the display-oriented classes: JobListView, JobContactList, JobNameList and JobCounter.

Some Commonalities in these Classes

Given that three of these four classes (all but JobCounter) have common intentions, it may not be surprising that they share some common implementation details. Those common intentions (in JobListView, JobContactList and JobNameList) boil down to generating a list of elements based on the current Jobs in the JobManager. The resulting interface/members that they share (barring implementation details) are:

Properties
_jobManager [private]
A reference to the JobManager that provides the collection of Jobs that the instance will display.
_listElement [private]
A reference to the page-element where the display's list of items will be shown. Since the contents of this element will be completely replaced when JobManager tells all of the display-elements that an update is needed, it has to be specifically identified, and should be free of any content that isn't related directly to the list-items that will be housed there.
_listItemTemplate [static]
The markup (as a string) that will be used as a template for generating the display-elements for a list-item.
I've set usable default template-string values in the _listItemTemplate properties of each class, but they are overridden by code in the initialization of the class, if template markup can be found.
sortField
The name of the Job property that will be used to sort Jobs for display in the UI that the instance controls.
sortOrder
The sort-order (up or down for ascending and descending, respectively) that will be used for display in the UI that the instance controls.
_top [private]
A reference to the top-most page-element that contains the entire job-object UI that the instance relates to. Note: this might not be the outer-most element of all the collected UI markup, though it could be. In most cases, this will be the wrapper tag of the UI for the instance (that is, the outermost tag that contains the markup, templates, etc., for a JobListDisplay, but not the wrapper for the separate JobContactList or JobNameList UI). This is mostly needed so that the objects can find and identify elements that have relevance to what they control.
Methods
clearDisplay()
Removes all content from the _listElement element.
displayItems()
Populates the _listElement element with the current sequence of Job displays, retrieved by getSortedJobs and formatted by getJobDisplay.
getJobDisplay( job )
Generates DOM-elements using the template markup of _listItemTemplate, and populating the placeholders therein with data from the supplied job.
setListItemTemplate( template ) [static]
Sets the markup to be used to generate display-markup in the static _listItemTemplate property.
onJobsChanged [event]
Called by JobManager.jobsChanged, this is the method that refreshes the job-item list in _listElement.
I toyed briefly with the idea of creating a base class that contained all of these members, then attaching their functionality to the concrete classes with an apply call. After giving it a bit of thought, I opted not to do this, partly because I didn't want to take the time to explore whether the private property-members would still work as I wanted. Since this code is also more of a POC, it didn't seem needful either. If I were to implement this for a live system, I'd probably want to do the research/exploration, though — and if the private-member concern were resolved to my satisfaction, I'd likely implement that way, if only to try and keep common code in a common location. On top of that, I expect that I'd want to discuss that in a fair amount of detail, and I do want to get back to the Python codebase soon, since that's my main focus for the moment.

One thing that all of today's classes do share is that they are all subjects in an Observer-pattern relationship with the JobManager. As a result, the initialization-code for each of them will register the instance as it's created with JobManager.addDisplay.

The JobListView class

JobListView attaches to the main job-list view, this part of the UI:

Current Jobs [# of jobs displayed here]
Job Number Job Title Job Contact
Job-Number #1 Job-Title #1 Job-Contact #1
Job-Number #2 Job-Title #2 Job-Contact #2
...
Job-Number #10 Job-Title #10 Job-Contact #10
It also needs to be able to set up sort-controls (the Job Number, Job Title and Job Contact headers) to allow the user to sort by those values, and to toggle the sort-order between up and down sorts. Its implementation is:

function JobListView( parameters )
{
    /*
     * Provides a list-view of all job data that responds to changes in the jobs 
     * tracked by the UI, includes the ability to sort the items in the display 
     * up or down by the fields of the items.
     * 
     * parameters ...... (object, required) The job's data:
     *  + listElement .. (str|element) The element (or its ID) that will contain
     *                   the list-items, which will be cleared of all child nodes
     *                   and re-populated as needed/arbitrarily.
     *  + sortField .... (str) The name of the Job data-field to be used for 
     *                   sorting list-members in the display.
     *  + sortOrder .... (str: "up" | "down") The sort-order to be used for 
     *                   sorting list-members in the display.
     *  + top .......... (str|element) The element (or its ID) that wraps ALL of 
     *                   the UI managed by this object.
     */
    // Processing that needs to happen before definition, if any
    if ( typeof parameters.listElement == 'string' )
    { parameters.listElement = document.getElementById( parameters.listElement ); }
    if ( typeof parameters.sortField == 'undefined' )
    { parameters.sortField = 'name'; }
    if ( typeof parameters.sortOrder == 'undefined' )
    { parameters.sortOrder = 'up'; }
    if ( typeof parameters.top == 'string' )
    { parameters.top = document.getElementById( parameters.top ); }
    // Instance properties:
    //  + Public properties
    this.sortControls = [];
    this.sortField = parameters.sortField;
    this.sortOrder = parameters.sortOrder;
    //  + Private properties
    var _jobManager = new JobManager();
    var _listElement = parameters.listElement;
    var _top = parameters.top;
    //  + Static properties
    JobListView._listItemTemplate = '<div>Number: <span data-field="number">number</span>; Number: <span data-field="name">name</span>; Contact: <span data-field="contact">contact</span></div>';
    // Initialize as needed based on incoming arguments
    // Instance methods:
    //  + public methods
    this.clearDisplay = function()
    {
        /*
         * Removes all child element from the list-view
         */
        while( _listElement.lastChild )
        { _listElement.removeChild( _listElement.lastChild ); }
    }
    this.displayItems = function()
    {
        /*
         * Populates the listElement with zero-to-many job-displays using the 
         * jobManager's jobs and the current sort-field and -order.
         */
        // Get the jobs that we're concerned with showing
        displayJobs = jobManager.getSortedJobs( this.sortField, this.sortOrder );
        // Generate display markup for each job
        for ( ji=0; ji<displayJobs.length; ji++ )
        {
            children = this.getJobDisplay( displayJobs[ ji ] );
            for( ci=0; ci<children.length; ci++ )
            { _listElement.appendChild( children[ ci ] ); }
        }
    }
    this.getJobDisplay = function( job )
    {
        /*
         * Generates and returns a sequence of nodes based on the class-level 
         * HTML template, populated with the specified job's data, ready to be 
         * appended to the list-display element.
         * 
         * job .............. (Job or equivalent object, required) The job to 
         *                    generate the display for
         */
        // Generate a look-up of nodes by data-name
        dataChildren = {};
        for( dci=0; dci<Job.fieldNames.length; dci++ )
        { dataChildren[ Job.fieldNames[ dci ] ] = null; }
        // Create the wrapper-node that will be used to perform all the DOM 
        // examination and manipulation, and populate it with the template.
        wrapper = document.createElement( 'results' )
        wrapper.innerHTML = JobListView._listItemTemplate;
        // Find all the data-display nodes and keep track of them.
        allChildren = wrapper.getElementsByTagName( '*' );
        for( ci=0; ci<allChildren.length; ci++ )
        {
            node = allChildren[ ci ];
            dataField = node.getAttribute( 'data-field' );
            if( dataField )
            {
                dataChildren[ dataField ] = node;
                // Remove the data-field attribute so as not to clutter the
                // final markup with it...
                node.removeAttribute( 'data-field' );
            }
            dataAction = node.getAttribute( 'data-action' );
            switch( dataAction )
            {
                case 'remove':
                    node.job = job;
                    node.onclick = function()
                        {
                            jobManager = new JobManager();
                            jobManager.removeJob( this.job );
                        }
            }
        }
        // Populate the nodes' innerHTML with the job's values.
        for( dci=0; dci<Job.fieldNames.length; dci++ )
        {
            if ( dataChildren[ Job.fieldNames[ dci ] ] )
            { dataChildren[ Job.fieldNames[ dci ] ].innerHTML = job[ Job.fieldNames[ dci ] ]; }
        }
        // Return the sequence of nodes
        return wrapper.childNodes;
    }
    this.registerSortControl = function( element, field )
    {
        /*
         * Registers a sort-control element with the instance
         *
         * element .......... (str or element) The element or the ID of the element to register as a sort-control for the instance.
         * field ............ (str, required) The field-name that the sort-control will use to specify the sort-order
         */
        if ( typeof element == 'string' )
        { element = document.getElementById( element ); }
        // The element needs to keep track of what field and order it's got
        element.sortField = field;
        element.sortOrder = 'up';
        // The element will need a handle to the JobListView instance, so...
        element.display = this;
        // Make the cursor over it into a pointer, just because
        element.style.cursor = 'pointer';
        // Set up the click-event.
        element.onclick = function()
        {
            if ( this.display.sortField != this.sortField )
            { this.sortOrder = 'up'; }
            else
            {
                if ( this.sortOrder == 'down' )
                { this.sortOrder = 'up'; }
                else
                { this.sortOrder = 'down'; }
            }
            this.display.setSortCriteria( this.sortField, this.sortOrder );
        }
        // Add the element to the sortControls, so that the list knows about it
        this.sortControls.push( element );
    }
    this.setSortCriteria = function( field, order )
    {
        /*
         * Sets the display's sort criteria (which field, and in what order) 
         * and triggers a display-update with those criteria.
         * 
         * field ............ (str, required) The field-name to sort display-list 
         *                    members by
         * order ............ (str, "up" or "down", required) The sort-order to 
         *                    sort display-list members by
         */
        this.sortField = field;
        this.sortOrder = order;

        // Clear all the sort-styles from all the sort-controls
        for( ci=0; ci<this.sortControls.length; ci++ )
        {
            sortControl = this.sortControls[ ci ];
            if ( sortControl.fieldName != this.sortField )
            { sortControl.className = 'sort-inactive'; }
            else
            { sortControl.className = 'sort-' + this.sortOrder; }
        }
        this.onJobsChanged();
    }
    // Static methods of the instance
    JobListView.setListItemTemplate = function( markup )
    {
        /*
         * Sets the class-level display-list member HTML template to use to 
         * display item data
         * 
         * markup ........... (str, required) The HTML markup to be used in 
         *                    generating list-item member displays. Uses 
         *                    data-field attributes to indicate what field-name 
         *                    from the jobs are displayed where.
         */
        JobListView._listItemTemplate = markup;
    }
    // Event-like methods of the instance
    this.onJobsChanged = function()
    {
        /*
         * Clears the current list-display of all members, and re-creates it with 
         * the current data. Called by JobManager.jobsChanged as part of their 
         * Observer-pattern relationship.
         */
        this.clearDisplay();
        this.displayItems();
        // Also, we need to update the various sort-controls:
        // Clear all the sort-styles from all the sort-controls
        for( ci=0; ci<this.sortControls.length; ci++ )
        {
            if ( this.sortField == this.sortControls[ ci ].sortField )
            { this.sortControls[ ci ].className = 'sort-' + this.sortControls[ ci ].sortOrder; }
            else
            { this.sortControls[ ci ].className = 'sort-inactive'; }
        }
    }
    // Processing that needs to happen before returning the instance, if any
    // - See if there are data-template and/or data-sort elements specified, 
    //   and if there are, set the template and sort-controls accordingly
    allChildren = _top.getElementsByTagName( '*' );
    templateNode = null;
    for( ci=0; ci<allChildren.length; ci++ )
    {
        checkNode = allChildren[ ci ];
        // Check for a data-template specification.
        isTemplate = checkNode.getAttribute( 'data-template' );
        if( ! templateNode && isTemplate == 'true' )
        {
            templateNode = checkNode;
            templateNode.removeAttribute( 'data-template' );
            break;
        }
        // Check for a data-sort specification
        isSort = checkNode.getAttribute( 'data-sort' );
        if ( isSort )
        {
            this.registerSortControl( checkNode, isSort );
            checkNode.removeAttribute( 'data-sort' );
        }
    }
    if( templateNode )
    { JobListView.setListItemTemplate( templateNode.outerHTML ); }
    // - Register this display with the JobManager
    jobManager.addDisplay( this );
    // Return the instance
    return this;
}

JobListView has a few additional members:

Properties
sortControls
A collection of sort-controls, created with the registerSortControl method.
Methods
registerSortControl( element, field )
Attaches events to the specified element to allow it to act as a sort-control, using the specified field as the sort-key.
setSortCriteria( field, order ) [scope]
Sets the sortField and sortOrder of the instance and triggers a refresh of the display (onJobsChanged)

The initialization/construction process also examines the _top element, starting with allChildren = _top.getElementsByTagName( '*' );, to locate any child elements with data-template and/or data-sort attributes. When they are found, each is handled as follows:

The first data-template element
...is identified as the templateNode for the instance, and its outerHTML (which would include the tag itself) is passed to JobListView.setListItemTemplate, replacing the default _listItemTemplate markup used to generate list-member displays.
(I find myself wondering how difficult it would be to implement multiple templates, so that each list-view generation would use the next template until it cycled back to the start of the set again. That's something for a later day, though... It doesn't make much sense for this POC effort.)
All data-sort elements
...are registered as a sort-control (registerSortControl) for the instance, using the value of the data-sort attribute as the field that the element will sort by/on.
It is, perhaps, worth noting that the processes for assigning the template markup and registering sort-controls will remove those data-* attributes from the final generated displays.

If no data-template or data-sort elements are found, the instance will still work. The templates and sort-controls could even be specified separately, since both of them have methods that can be used to those ends. I think, however, that it's more convenient, and that it allows a designer-level developer more and better control over the page markup to tweak, twiddle or frobnicate with as they see fit.

The getJobDisplay method might be worth taking a close look at — As part of it's generation of display-markup, it also looks for and sets up events on child elements with specific data-action attribute-values. Right now all it recognizes is remove, but it will link those items (the buttons) to JobManager.removeJob, storing the Job that the removal-action would apply to as a property of the button itself. This process could easily be extended to add, say, an Edit Job button or link to the template, though it would require some code on the job-UI side of things and on the design side to make that fully functional.

The JobContactList class

JobContactList attaches to the job-contact list-view, this part of the UI:

All Job Contacts
contact
All it does is generate a distinct, sorted list of all contacts across all Jobs in the JobManager. Its implementation is:

function JobContactList( parameters)
{
    /*
     * Provides a list-view of distinct job-contacts that responds to 
     * changes in the jobs in the UI.
     * 
     * parameters ...... (object, required) The job's data:
     *  + listElement .. (str|element) The element (or its ID) that will 
     *                   contain the list-items, which will be cleared of 
     *                   all child nodes and re-populated as needed/
     *                   arbitrarily.
     *  + top .......... (str|element) The element (or its ID) that wraps 
     *                   ALL of the UI managed by this object.
     */
    // Processing that needs to happen before definition, if any
    if ( typeof parameters.listElement == 'string' )
    { parameters.listElement = document.getElementById( 
        parameters.listElement ); }
    if ( typeof parameters.top == 'string' )
    { parameters.top = document.getElementById( parameters.top ); }
    // Instance properties:
    //  + Public properties
    this.sortField = 'contact'
    this.sortOrder = 'up'
    //  + Private properties
    var _jobManager = new JobManager();
    var _listElement = parameters.listElement;
    var _top = parameters.top;
    //  + Static properties
    JobContactList._listItemTemplate = '<div>Contact: <span data-field="contact">contact</span></div>';
    // Initialize as needed based on incoming arguments
    // Instance methods:
    //  + public methods
    this.clearDisplay = function()
    {
        /*
         * Removes all child element from the list-view
         */
        while( _listElement.lastChild )
        { _listElement.removeChild( _listElement.lastChild ); }
    }
    this.displayItems = function()
    {
        /*
         * Populates the listElement with zero-to-many job-displays using the 
         * jobManager's jobs and the current sort-field and -order.
         */
        // Get the jobs that we're concerned with showing
        displayJobs = jobManager.getSortedJobs( this.sortField, this.sortOrder );
        // Generate display markup for each job
        addedItems = {};
        for ( ji=0; ji<displayJobs.length; ji++ )
        {
            if ( typeof addedItems[ displayJobs[ ji ].contact ] == 'undefined' )
            {
                children = this.getJobDisplay( displayJobs[ ji ] );
                for( ci=0; ci<children.length; ci++ )
                { _listElement.appendChild( children[ ci ] ); }
                addedItems[ displayJobs[ ji ].contact ] = true;
            }
        }
    }
    this.getJobDisplay = function( job )
    {
        /*
         * Generates and returns a sequence of nodes based on the class-level 
         * HTML template, populated with the specified job's data, ready to be 
         * appended to the list-display element.
         * 
         * job .............. (Job or equivalent object, required) The job to 
         *                    generate the display for
         */
        // Generate a look-up of nodes by data-name
        dataChildren = {};
        for( dci=0; dci<Job.fieldNames.length; dci++ )
        { dataChildren[ Job.fieldNames[ dci ] ] = null; }
        // Create the wrapper-node that will be used to perform all the DOM 
        // examination and manipulation, and populate it with the template.
        wrapper = document.createElement( 'results' )
        wrapper.innerHTML = JobContactList._listItemTemplate;
        // Find all the data-display nodes and keep track of them.
        allChildren = wrapper.getElementsByTagName( '*' );
        for( ci=0; ci<allChildren.length; ci++ )
        {
            node = allChildren[ ci ];
            dataField = node.getAttribute( 'data-field' );
            if( dataField )
            {
                dataChildren[ dataField ] = node;
                // Remove the data-field attribute so as not to clutter the
                // final markup with it...
                node.removeAttribute( 'data-field' );
            }
            dataAction = node.getAttribute( 'data-action' );
            switch( dataAction )
            {
                case 'remove':
                    node.job = job;
                    node.onclick = function()
                        {
                            jobManager = new JobManager();
                            jobManager.removeJob( this.job );
                        }
            }
        }
        // Populate the nodes' innerHTML with the job's values.
        for( dci=0; dci<Job.fieldNames.length; dci++ )
        {
            if ( dataChildren[ Job.fieldNames[ dci ] ] )
            { dataChildren[ Job.fieldNames[ dci ] ].innerHTML = job[ Job.fieldNames[ dci ] ]; }
        }
        // Return the sequence of nodes
        return wrapper.childNodes;
    }
    // Static methods of the instance
    JobContactList.setListItemTemplate = function( markup )
    {
        /*
         * Sets the class-level display-list member HTML template to use to 
         * display item data
         * 
         * markup ........... (str, required) The HTML markup to be used in 
         *                    generating list-item member displays. Uses 
         *                    data-field attributes to indicate what field-name 
         *                    from the jobs are displayed where.
         */
        JobContactList._listItemTemplate = markup
    }
    // Event-like methods of the instance
    this.onJobsChanged = function()
    {
        /*
         * Clears the current list-display of all members, and re-creates it with 
         * the current data. Called by JobManager.jobsChanged as part of their 
         * Observer-pattern relationship.
         */
        this.clearDisplay();
        this.displayItems();
    }
    // Processing that needs to happen before returning the instance, if any
    // - See if there are data-template and/or data-sort elements specified, 
    //   and if there are, set the template and sort-controls accordingly
    allChildren = _top.getElementsByTagName( '*' );
    templateNode = null;
    for( ci=0; ci<allChildren.length; ci++ )
    {
        checkNode = allChildren[ ci ];
        // Check for a data-template specification.
        isTemplate = checkNode.getAttribute( 'data-template' );
        if( ! templateNode && isTemplate == 'true' )
        {
            templateNode = checkNode;
            templateNode.removeAttribute( 'data-template' );
            break;
        }
    }
    if( templateNode )
    { JobContactList.setListItemTemplate( templateNode.outerHTML ); }
    // - Register this display with the JobManager
    jobManager.addDisplay( this );
    // Return the instance
    return this;
}

JobContactList uses the same template-markup-acquisition process as JobListDisplay to auto-set the markup for its list-members, but doesn't look for sort-controls — this view doesn't support sorting. It could, but won't for now.

One item of potential interest/note: The displayItems method keeps track of which values (contacts) it has already added to the display-list by using the contact value as a property-name in a local object, and checking on each pass through the look whether that value is already accounted for. It's a simple, brute-force approach to making sure that no value appears in the display more than once, but it's not foolproof at this point.

The JobNameList class

JobNameList is almost identical to JobContactList (it displays job-names instead of -contacts). It relates to this part of the POC UI:

All Jobs
name
I've stripped the identical code out of the listing below for brevity:

function JobNameList( parameters )
{
    // Code is identical to JobContact list until this point

    //  + Public properties
    this.sortField = 'contact'
    this.sortOrder = 'up'

    // ...

    //  + Static properties
    JobNameList._listItemTemplate = '<div>Contact: <span data-field="name">name</span></div>';

    // ...

    this.displayItems = function()
    {
        /*
         * Populates the listElement with zero-to-many job-displays using the 
         * jobManager's jobs and the current sort-field and -order.
         */
        // Get the jobs that we're concerned with showing
        displayJobs = jobManager.getSortedJobs( this.sortField, this.sortOrder );
        // Generate display markup for each job
        addedItems = {};
        for ( ji=0; ji<displayJobs.length; ji++ )
        {
            if ( typeof addedItems[ displayJobs[ ji ].name ] == 'undefined' )
            {
                children = this.getJobDisplay( displayJobs[ ji ] );
                for( ci=0; ci<children.length; ci++ )
                { _listElement.appendChild( children[ ci ] ); }
                addedItems[ displayJobs[ ji ].name ] = true;
            }
        }
    }

    // ...

    // Static methods of the instance
    JobNameList.setListItemTemplate = function( markup )
    {
        /*
         * Sets the class-level display-list member HTML template to use to 
         * display item data
         * 
         * markup ........... (str, required) The HTML markup to be used in 
         *                    generating list-item member displays. Uses 
         *                    data-field attributes to indicate what field-name 
         *                    from the jobs are displayed where.
         */
        JobNameList._listItemTemplate = markup
    }
    // Event-like methods of the instance

    // ...

    // - Register this display with the JobManager
    jobManager.addDisplay( this );
    // Return the instance
    return this;
}

Given the similarities between this and JobContactList, I'm not sure that there is any meaningful commentary I can add about it...

That wraps up the three classes that share the common interface noted earlier. The one remaining class to detail before I'm done with this post is...

The JobCounter class

JobCounter is a much simpler class that the three I've shown so far, because its purpose is much simpler: To show the number of jobs currently in the JobManager's collection of Jobs. It lives in the header of the job-display list (the bold, red part below):

Current Jobs [job-count] ...

function JobCounter( element )
{
    /*
     * Provides an observer member that displays a count of jobs in the 
     * JobManager instance at a specified location
     * 
     * element ......... (element or str) The element, or the ID of the 
     *                   element, whose content will be updated with the 
     *                   current job-count.
     */
    // Processing that needs to happen before definition, if any
    if ( typeof element == 'string' )
    { element = document.getElementById( element ); }
    // Instance properties:
    //  + Private properties
    var _element = element;
    var _jobManager = new JobManager();
    // Event-like methods of the instance
    this.onJobsChanged = function()
    {
        /*
         * Clears the current list-display of all members, and re-creates 
         * it with the current data. Called by JobManager.jobsChanged as 
         * part of their Observer-pattern relationship.
         */
        jobCount = _jobManager.getSortedJobs().length;
        switch ( jobCount )
        {
            case 0:
                _element.innerHTML = 'No jobs';
                break;
            case 1:
                _element.innerHTML = '1 job';
                break;
            default:
                _element.innerHTML = jobCount + ' jobs';
                break;
        }
    }
    // Processing that needs to happen before returning the instance, if any
    // - Register this display with the JobManager
    jobManager.addDisplay( this );
    // Return the instance
    return this;
}

Its interface is correspondingly simple:

Properties
_element [private]
The page-element whose content will be updated with the current job-count when onJobsChanged is called.
_jobManager [private]
A reference to the JobManager that provides the collection of Jobs that the instance will display the count of.
Methods
onJobsChanged()
Counts the current jobs in _jobManager, generates formatted output, and replaces the contents of the _element with that output.

That wraps up all of the display-related classes, and allof the classes that are members in the Observer-pattern relationship between JobManager and display-classes at large. The only item remaining is the JobAdderForm class, which relates to the form for adding jobs to the JobManager's collection of Jobs. That, plus a functional demo of the whole UI is coming next...

Thursday, February 23, 2017

Object-Oriented JavaScript in the UI [2]

Today I'm going to start digging in to the implementation of the first couple of classes in the Jobs-UI object-structure. The classes I'm going to implement are some of the behind-the-scenes items, defining the data-structure of a job, and an object for keeping track of the collection of jobs, adding and removing jobs from that collection, and notifying other objects of changes to the collection. There is also some utility functionality that other classes, particularly the display-oriented ones, will need as well.

A JavaScript class-template

Given my recent attention to Python code-templates, it should maybe come as no great surprise that I also have one for JavaScript classes. I mentioned previously that there are several different ways to define classes in JavaScript, even without the new class and extends syntax-sugar. One of them uses the JavaScript prototype property. A reasonably-complete breakdown of that structure and some of it's permutations can be found on the w3schools website. That works reasonably well, but as far as I've been able to tell, there's no good/real way to emulate private properties and methods. The way that I generally define JavaScript classes, I find that I frequently want private members, so I use what might be called a function-based approach that I know supports them. That approach looks something like this:

function ClassName()
{
    /*
     * TODO: Describe the class (Represents a BLAH, or whatever)
     * 
     * argumentName .... TODO: Describe the argument
     */
    // Processing that needs to happen before definition, if any
    // Instance properties:
    //  + Public properties
//    this.publicName = null;
    //  + Private properties
//    var privateName = null;
    //  + Static properties
//    ClassName.staticName = null;
    // Initialize as needed based on incoming arguments
    // Instance methods:
    //  + public methods
//    this.publicMethod = function()
//    {
//        /*
//         * TODO: Describe the method
//         * 
//         * argumentName ..... TODO: Describe the argument
//         */
//        return;
//    }
    //  + private methods
//    function privateMethod()
//    {
//        /*
//         * TODO: Describe the method
//         * 
//         * argumentName ..... TODO: Describe the argument
//         */
//        return;
//    }
    // Static methods of the instance
//    ClassName.staticMethod = function()
//    {
//        /*
//         * TODO: Describe the method
//         * 
//         * argumentName ..... TODO: Describe the argument
//         */
//        return;
//    }
    // Event-like methods of the instance
//    this.eventMethod = function()
//    {
//        /*
//         * TODO: Describe the method
//         * 
//         * argumentName ..... TODO: Describe the argument
//         */
//        return;
//    }
    // Processing that needs to happen before returning the instance, if any
    // Return the instance
    return this;
}
I've made this template-file available for download — The link is at the end of the post.

The UI Markup

The basic markup for the UI (without any styling) will provide a number of data-* attributes that the UI objects will use to detect elements that serve various purposes once the code executes:

<div id="ui-example">
    <fieldset id="currentJobList">
        <legend>Current Jobs [<span id="jobCounter">job-count</span>]</legend>
        <div>
            <span data-sort="number"><strong>Job Number</strong></span>
            <span data-sort="name"><strong>Job Title</strong></span>
            <span data-sort="contact"><strong>Job Contact</strong></span>
        </div>
        <div id="managedJobsList">
            <div data-template="true">
                <span data-field="number">number</span>
                <span data-field="name">name</span>
                <span data-field="contact">contact</span>
                <button data-action="remove">X</button>
            </div>
        </div>
        <div>
            <label > </label>
            <button data-jobaction="startJob">Add A New Job</button>
        </div>
        <fieldset id="newJobForm">
            <legend>Create a New Job</legend>
            <label>Job Number</label>
            <input name="number" type="text" data-jobfield="number"/>
            <br />
            <label>Job Title</label>
            <input name="title" type="text" data-jobfield="name"/>
            <br />
            <label>Job Contact</label>
            <input name="contact" type="text" data-jobfield="contact"/>
            <br />
            <label> </label>
            <button data-jobaction="createJob">OK</button>
            <button data-jobaction="cancelJob">Cancel</button>
        </fieldset>
    </fieldset>

    <fieldset id="jobNameList">
        <legend>All Jobs</legend>
        <div id="managedNameList">
            <div data-template="true" data-field="name">name</div>
        </div>
    </fieldset>

    <fieldset id="jobContactList">
        <legend>All Job Contacts</legend>
        <div id="managedContactlist">
            <div data-template="true" data-field="contact">contact</div>
        </div>
    </fieldset>
</div>

The data-* attributes in play are:

data-sort
Indicates that the element is a sort-control, and provides the name of the Job field that the control will sort by when it is clicked by the user.
data-template
Indicates that the element and its children are to be used as a markup template by the code, allowing the original markup to define what the structure of the display will look like when it's generated.
data-field
Indicates what field from a Job should be displayed in the element's content.
data-action and data-jobaction
Indicates that the element performs some sort of action in the final, rendered UI. The values of this attribute are tied to specific action sequences, with specific events associated, etc.
The idea of tagging items in the markup to control some aspect of the results of the code is something that experience has shown me to be very useful when a web-application has separate (and differently-focussed) developers for logic and design. Allowing a designer to take near-total control over the appearance of an application's UI requires more thought (and more code), but has almost always been a good time-investment.

There will also be some initialization code that creates the various UI-object instances and initializes them, but I've left that out for now. It will be included in the final file-set that I'll make available for download once this series of posts is complete.

Implementing the Job UI Classes

As noted in the previous post, there are seven different classes involved in this UI codebase. In the interests of keeping post-length down to something reasonable (and readable, I hope), I'm going to break the coverage of them out into several posts (three, I think). I'm also leaving all their comments in — even though that adds to the length of the post (maybe more than I'd like), it'll make it easier to discuss interesting and salient points about them, I hope.

The Job class

Job is a dumb data object that represents a single job in the UI. As such, it's mostly just object-properties and instance-representation (through the toString method).

function Job( parameters )
{
    /*
     * Represents a job.
     * 
     * parameters ...... (object, required) The job's data:
     *  +- number ...... (str, required) The job-number of the job;
     *  +- name ........ (str, required) The name of the job;
     *  +- contact ..... (str, required) The contact-of-record for the job
     */
    // Instance properties:
    //  + Public properties
    this.contact = parameters.contact;
    this.number = parameters.number;
    this.name = parameters.name;
    //  + Static properties
    if( typeof Job.fieldNames == 'undefined' )
    { Job.fieldNames = [ 'contact', 'name', 'number' ]; }
    // Instance methods:
    //  + public methods
    this.toString = function()
    {
        /*
         * TODO: Describe the method
         */
        return '<Job number="' + this.number + '" name="' + 
            this.name + '" contact="' + this.contact + '"/>';
    }
    // Return the instance
    return this;
}

Because a Job's properties are used to generate displayed content across several of the other objects in the UI code, they are all defined as public properties. Those property-values can be read and altered without restriction. In a live-system environment, I might spend some time trying to come up with some way to arrange things so that they could not be set once they were defined — in order to keep the UI code from being able to easily alter them on the fly. It would depend heavily on where those Job objects were coming from, though. If they were being provided in real time from a back-end web-service, for example, I would be much less concerned, since that would imply that the service was responsible for managing the data-integrity of the job-objects.

The JobManager class

The JobManager, as the name implies, keeps track of and (to some degree) manages a collection of Jobs, allowing the addition and removal of individual Job objects. It is also responsible for dispatching messages to other objects (the display-oriented ones) that those objects can use to update their content when a change in the managed Jobs has occurred. JobManager is the subject in an Observer pattern relationship with any number of display-component observers.

function JobManager( jobs )
{
    /*
     * Keeps track of the _jobs for the application/UI
     * 
     * _jobs ............ (array of objects, optional) The data of the _jobs 
     *                   to keep track of.
     */
    // Processing that needs to happen before definition, if any
    if ( typeof JobManager.instance != 'undefined' )
    {
        //  Singleton-ish behavior: There can be only one, and it should live 
        //  in JobManager.instance if it exists, so return it
        return JobManager.instance;
    }
    // Instance properties:
    //  + Private properties
    var _displays = [];
    var _jobs = [];
    //  + Static properties
    // NOTE: JobManager.instance is set at the end of instantiation!
    // Instance methods:
    //  + public methods
    this.addDisplay = function( display )
    {
        /*
         * Adds a display to the instance's collection of _displays to be 
         * notified when the collection of _jobs changes
         */
        if ( typeof display.onJobsChanged == 'function' )
        { _displays.push( display ); }
    }
    this.addJob = function( job, updateNow )
    {
        /*
         * Adds a job to the instance's collection of _jobs
         * job ......... (object, required) The job to add to the collection
         * updateNow ... (bool, optional, defaults to true) Indicates whether 
         *               or not to refresh all job-_displays immediately upon 
         *               completion of adding the job.
         */
        if ( typeof updateNow == 'undefined' )
        { updateNow = true; }
        _jobs.push( new Job( job ) );
        if( updateNow )
        { this.jobsChanged(); }
    }
    this.compareJobs = function( job1, job2 )
    {
        /*
         * Compares two Job (or equivalent) objects, returning true if they have 
         * the same field-values, false otherwise
         */
        result = true;
        for( fi=0; fi<Job.fieldNames.length; fi++ )
        {
            field = Job.fieldNames[ fi ];
            try
            {
                if ( job1[ field ] != job2[ field ] )
                {
                    result = false;
                    break;
                }
            }
            catch( error )
            { result = false; }
        }
        return result;
    }
    this.getSortedJobs = function( sortKey, sortOrder )
    {
        /*
         * Gets a COPY of the main job-list (because each different display may 
         * need a different sort-order!), sorted by the sortField specified, in 
         * the order specified by sortOrder
         */
        // Make a copy of the entire current _jobs array
        results = _jobs.slice();
        // Sort it by the specified field-name using a callback 
        switch( sortKey )
        {
            // Define the callback based on the sort-field specified:
            case 'number':
                sortf = function( a, b )
                    {
                        if ( a.number < b.number )
                        { return -1; }
                        if ( a.number > b.number )
                        { return 1; }
                        return 0;
                    };
                    break;
            case 'name':
                sortf = function( a, b )
                    {
                        if ( a.name < b.name )
                        { return -1; }
                        if ( a.name > b.name )
                        { return 1; }
                        return 0;
                    };
                    break;
            case 'contact':
                sortf = function( a, b )
                    {
                        if ( a.contact < b.contact )
                        { return -1; }
                        if ( a.contact > b.contact )
                        { return 1; }
                        return 0;
                    };
                    break;
        }
        // Sort the results with the callback;
        results.sort( sortf );
        // Reverse the results if the sort-orer is "down"
        if ( sortOrder == 'down' )
        { results.reverse(); }
        return results;
    }
    this.removeDisplay = function( display )
    {
        /*
         * Removes a display from the instance's collection of _displays 
         * to be notified when the collection of _jobs changes
         */
        console.log( 'JobManager.removeDisplay( ' + display + ' ) called' );
        console.log( 'JobManager.removeDisplay complete' );
    }
    this.removeJob = function( job, updateNow )
    {
        /*
         * Removes a job from the instance's collection of _jobs
         */
        if ( typeof updateNow == 'undefined' )
        { updateNow = true; }
        new_jobs = [];
        for( ji=0; ji<_jobs.length; ji++ )
        {
            if ( ! this.compareJobs( _jobs[ ji ], job ) )
            { new_jobs.push( _jobs[ ji ] ); }
        }
        _jobs = new_jobs;
        if( updateNow )
        { this.jobsChanged(); }
    }
    //  + private methods
    // Event-like methods of the instance
    this.jobsChanged = function()
    {
        /*
         * Called when a job is added or removed, calls the onjobsChanged method 
         * of all registered _displays
         */
        for( di=0; di<_displays.length; di++ )
        { _displays[ di ].onJobsChanged(); }
    }
    // Processing that needs to happen before returning the instance, if any
    if ( jobs )
    {
        // Add all the jobs one by one, without changing the displays
        for ( ji=0; ji<jobs.length; ji++ )
        { this.addJob( jobs[ ji ], false ); }
    }
    // Store the instance in JobManager.instance, and return that
    JobManager.instance = this;
    return JobManager.instance;
}

I've defined JobManager as a Singleton, of sorts: There can be only one active instance of the class in a page, and any attempts to generate an instance will either create the first instance (which happens during the execution of the set-up code), or will return that first instance, already populated and active. You'll see that I leverage this behavior in several of the other classes in the codebase, allowing the page to create a JobManager and populate it with the Jobs for the page, then calling new JobManager() to set up a local reference to the original JobManager in instances of the other classes. The real instance is created and stored as a nominally-static property of the JobManager class (JobManager.instance at the end of the object's definition before being returned.

The two properties of JobManager, _displays and _jobs are private. I could not come up with any reason why any other object in this codebase would need to know anything about either of them, other than the need to acquire a sequence of Jobs for display purposes, and that has additional wrinkles that were better handled by a method (more on that later).

The methods of JobManager are, I think, pretty straightforward:

addDisplay( display ) and removeDisplay( display )
Allow the addition (and registration) and removal of observer display-management objects. Once added, any time the jobsChanged() method of the instance is called, all of the registered displays will be notified that they need to update because there's been a change in the Jobs that the JobManager is keeping track of.
addJob( job, updateNow ) and removeJob( job, updateNow )
Provide a mechanism for adding or removing a single Job from the collection of Jobs that the JobManager is keeping track of. The updateNow argument (which defaults to a true value) allows a developer to write code that adds a series of Jobs to the instance, then explicitly calling for an update (with jobsChanged(). That process, minus the explicit call to jobsChanged(), can be seen in the code at the end of the object-definition.
compareJobs( job1, job2 )
A helper-method (used in removeJob) that compares two objects (which may or may not be Job instances, but frequently are), returning true if both objects provided have the same values for their contact, name and number properties. In practice, I've found that one of the two objects being compared is frequently not a Job instance, though in those cases, it has always been a generic object with the same properties as a Job.
getSortedJobs( sortKey, sortOrder )
Returns a sequence of Job objects, sorted by the sortKey property-values of those objects, in the sortOrder direction. This was needed because each display-object in the codebase can have its own sort-criteria:
  • JobContactList will always sort by contact, ascending;
  • JobNameList will always sort by name, ascending; and
  • JobListView can sort by any field, in either direction
getSortedJobs, then, allows each of those display-objects to use their own separate sort-criteria and -order, no matter what the other objects are using.
jobsChanged()
Calls the onJobsChanged methods of all registered _displays.

In my next post, I'll tackle the various display-object classes: JobListView, JobContactList, JobNameList and JobCounter. Before I break for the day, though, here's a download-link for my JavaScript class-template file: