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:

Tuesday, February 21, 2017

Object-Oriented JavaScript in the UI [1]

JavaScript is an object-oriented language, though most of the development with it that I've seen doesn't take advantage of that. Granted, the syntax and language-structure of JavaScript didn't lend itself well to real OO coding-styles until ECMAScript 6 came out with an official(?) class definition syntax, along with extends capabilities. There's still no interface or abstract class declaration, though, as far as I've been able to tell. The underlying ability to create classes, extend them, make them (kinda) abstract, and to generate interface-like behavior has been available in the language for a long time, though, even if the methods to achieve those ends were odd, cryptic, or could be implemented in several different ways.

I look forward to the new class and extends being standard in all the active browsers on the market. It's looking good for all the modern browsers, at least on the desktop, but support on mobile and older desktop browsers is not as good. With around 20% of the market share still showing Internet Explorer 8, 9, 10 and 11 (as of the end of 2016 at netmarketshare.com), I'm not going to hold out high hopes for adoption of the new JavaScript capabilities for a while yet...

But, since it is an object-oriented language, that means that there is at least some possibility for writing JavaScript code that follows good, solid OO-design principles — Things like design patterns, for example...

A Basic Form/UI and Requirements

So, indulge me in a little conscientous role-playing. Say there's a need for a UI and functionality to manage a list of jobs in a web-application, and a somewhat functional POC UI has to be built out.

In this scenario, a user is on some sort of dashboard page, and can see a list of current jobs that looks something like this:

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
The bold headings will (eventually) be clickable to toggle a sort of the listed items by that column's values, and the X buttons will allow the user to remove a job from the list.

They can also enter data for a new job using a form that looks something like this:

Create a New Job


Once the fields are populated and the user creates the job, it should be inserted into the job-list, paying attention to the current sort-order so as not to just be added to the end of the list. The Current Jobs header's job-count should also update when a job is added or removed.

There is also some talk about showing a distinct list of job-names and a separate list of job-contacts elsewhere on the dashboard (it's still on the same page, however):

All Jobs
Job-Title #1
Job-Title #2
...
Job-Title #3
All Job Contacts
Job-Contact #1
Job-Contact #2
...
Job-Contact #3

Finally, for reasons unspecified (something, something, UI design, something):

  • The Create a New Job form is hidden until the Add A New Job button is clicked;
  • When the Create a New Job form is made visible, the Add A New Job button is hidden or disabled;
  • When the OK or Cancel buttons are clicked, the Add A New Job button is shown or enabled; and
  • When the OK or Cancel buttons are clicked, the Create a New Job form is hidden and all of its fields are cleared;

All of these processes would normally involve some sort of server connection, probably an AJAX/XHR call, that returned some data-set to be added to the active jobs, but the basic mechanism can be established without that server-side connection, as can the removal mechanism, so for the POC, all that needs to happen is being able to demonstrate the job-addition, -removal and -counting functionality that the UI needs.

There are a lot of moving parts here, so I'm going to spend this post mostly just delving into the design of the UI components. Getting into the weeds will follow starting with my next post. There may be several — as I'm writing this, I don't really know how much code this is going to entail. I'm also going not going to worry about member-scopes for this write-up. That is, I'm going to stick to public members across the board until or unless I can think of a good reason to try and make things private, and I can come up with a way to implement it — I know it can be done with one object-construction pattern, but I'm not sure if it can with the pattern that I'm currently planning to use...

What That's Going To Look Like

After some analysis, and deciding on the architecture (yes, it really is architecture, even if it is simple and in JavaScript), I can accomplish the POC with seven classes:

  • Job to represent individual job-items;
  • JobManager to keep track of job-data, handle addition and removal of jobs from the data-set, and act as an originator of updates in an Observer relationship with various display-entities;
  • JobListView to manage the display of the big job list portion of the UI;
  • JobContactList to manage the display of job contact-names;
  • JobNameList to manage the display of job-names;
  • JobCounter to manage the display of job-counters; and
  • JobAdderForm to provide the functionality for adding a job to the JobManager.
In my opinion, the POC doesn't really need to have classes built out (or members added to any of the items listed above) to handle the eye-candy of showing and hiding things when various buttons are clicked. That can be handled with some simple event-setup, so I'm going to set those aside for now.

I'm going to write the code for these classes in plain-old-JavaScript, with an eye towards making sure it'll run on older browsers. Converting the code into something that uses jQuery (or any of the other multitude of JavaScript frameworks should not be difficult for those so inclined. For now, though, I'm going to keep it as free-standing as I can, so noting but standard, local JS implementation.

I will, however, try to make the code reasonably smart — allowing it to do at least some automatic attachment to elements in the page markup.

The Job class

A Job is a dumb data object — It's really just a data-structure, with no methods or logic behind it other than a constructor. That constructor, at least for a POC-level effort, doesn't need to do anything more than set instance properties from their corresponding values in the parameters, so I won't go into the class' interface in any detail. If this were to be implemented in a system to be deployed, I'd want to add some type- and value-checking to those property-setting processes, and some error-handling as well, in all likelihood, but that's more effort than I'm going to put in here and now. Here's the class-diagram for a Job:

While I was working through the design and implementation, I determined that I had at least an occasional need to be able to dump a string-value for a Job from time to time, so I added a toString method to it after the initial class-diagram had been drawn up. It's nothing too fancy, returning an XML-like tag along the lines of
<Job number="[number]" name="[name]" contact="[contact]" />
but it did prove invaluable when I found that I needed to be able to see what was happening while debugging some methods in other classes.

The JobManager class

First up, I'm going to define the class that will be used to keep track of all of the jobs in the UI. That class, JobManager is mostly a data-repository, but one that participates in an Observer pattern relationship with any number of display-oriented objects. As a class-diagram, it looks like this (at first blush — as development goes on, I may want/need to add or remove members):

The interface of the class, in some detail:
Properties
_displays [private]
The collection of display-related objects that will be notified when the instance's jobsChanged method is executed.
_jobs [private]
The collection of Job objects that the instance is keeping track of.
Methods
.ctor( jobs )
The object constructor/initializer. Returns a new instance if one does not already exist, otherwise returns the previously-created instance.
addDisplay( display )
Adds a display-related object to the instance's displays collection
addJob( job, updateNow )
Adds a Job object to the instance's jobs collection.
compareJobs( job1, job2 )
Compares two Job (or equivalent) objects, returning true if they have the same field-values, false otherwise
getSortedJobs( 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.
jobsChanged()
Calls the onJobsChanged methods of all the display-related objects in the instance's displays.
removeDisplay( display )
Removes the specified display-related object from the instance's displays collection, which means that it will no longer get updates from jobsChanged.
removeJob( job, updateNow )
Removes the specified Job object from the instance's jobs collection
The JobManager should probably be a Singleton: The underlying rationale for this is that there should (probably) be only one job-data set that's being tracked and manipulated. The .ctor (constructor) method is where I'm planning to manage that need.

The JobListView class

JobListView is a display-control mechanism that is part of an Observer realtionship with the JobManager on the page. When a job is added to or removed from the JobManager, it will update the displayed list of Jobs, keeping the specified sort-order for items in the display. It also provides a process for attaching sort-control functionality to arbitrary elements on the page so that, when clicked, the display will be re-sorted according to the sort-criteria relevant to the sort-control.

The interface of the class, in some detail:
Properties
_jobManager [private]
The JobManager where the display's jobs can be found and manipulated.
_listElement [private]
The display-element that all of the list-members will be shown in.
_listItemTemplate [static]
The HTML template to be used in generating a list-member's display in the list.
sortControls
An array that keeps track of all the sort-controls for the display.
sortField
The name of the Job property that is used in the sort-process for the display.
sortOrder
The sort-direction that is used in the sort-process for the display.
_top [private]
The display-element that wraps all of the UI elements for the display; the display's root tag, as it were.
Methods
.ctor( parameters )
The object constructor/initializer. The parameters argument is an object that is expected to contain the following fields:
listElement
The element that will contain the list-items, which will be cleared of all child nodes and re-populated as needed/arbitrarily.
sortField
The name of the Job data-field to be used for sorting list-members in the display.
sortOrder
The sort-order to be used for sorting list-members in the display.
top
The element that wraps all of the UI managed by this object.
clearDisplay()
Removes all child element from the list-view.
displayItems()
Populates the listElement with zero-to-many job-displays using the jobManager's jobs and the current sort-field and -order.
getJobDisplay( 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.
registerSortControl( element, field )
Registers a sort-control element with the instance.
setSortCriteria( field, order )
Sets the display's sort criteria (which field, and in what order), and triggers a display-update with those criteria.
setListItemTemplate( markup ) [static]
Sets the class-level display-list member HTML template to use to display item data
Event-like Methods
onJobsChanged()
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.

The JobContactList class

JobContactList is a display-control object (like JobListView) that displays a sorted list of all job-contacts across all the jobs in the JobManager.

Properties
_jobManager [private]
The JobManager where the display's jobs can be found.
_listElement [private]
The display-element that all of the list-members will be shown in.
_listItemTemplate [static]
The HTML template to be used in generating a list-member's display in the list.
sortField
Description
sortOrder
Description
Methods
.ctor( parameters )
The object constructor/initializer.
listElement
The element that will contain the list-items, which will be cleared of all child nodes and re-populated as needed/arbitrarily.
top
The element that wraps all of the UI managed by this object.
clearDisplay()
Removes all child element from the list-view.
displayItems()
Populates the listElement with zero-to-many job-displays using the jobManager's jobs and the current sort-field and -order.
getJobDisplay( 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.
Event-like Methods
onJobsChanged()
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.

The JobNameList class

Apart from the fact that it focuses on displaying the name values of all the Jobs in the JobManager, JobNameList is pretty much identical to JobContactList.

Properties
_jobManager [private]
The JobManager where the display's jobs can be found.
_listElement [private]
The display-element that all of the list-members will be shown in.
_listItemTemplate [static]
The HTML template to be used in generating a list-member's display in the list.
sortField
Description
sortOrder
Description
Methods
.ctor( parameters )
The object constructor/initializer.
listElement
The element that will contain the list-items, which will be cleared of all child nodes and re-populated as needed/arbitrarily.
top
The element that wraps all of the UI managed by this object.
clearDisplay()
Removes all child element from the list-view.
displayItems()
Populates the listElement with zero-to-many job-displays using the jobManager's jobs and the current sort-field and -order.
getJobDisplay( 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.
Event-like Methods
onJobsChanged()
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.

The JobCounter class

Displays a count of current Jobs in the JobManager that updates as jobs are added or removed.

Properties
_element [private]
The document element whose content will be updated as jobs are added to or removed from the JobManager.
_jobManager [private]
The JobManager where the display's jobs can be found.
Methods
.ctor( element )
The object constructor/initializer.
Event-like Methods
onJobsChanged()
Updates the _element witht he current count of jobs. Called by JobManager.jobsChanged as part of their Observer-pattern relationship.

The JobAdderForm class

JobAdderForm defines a class that keeps track of all of the form-fields and action-elements (buttons) involved in the UI, connects those elements with other elements and objects (like the JobManager) to allow the form in the UI to add jobs to the JobManager. It also proves some of the eye-candy noted in the initial POC requirements (showing and hiding the job-adding form). I wasn't going to bother with that initially, but it was simple to implement, and it just made sense to wire the show/hide functionality up to the UI elements rather than assuming that it would be handled later. It could always be changed...

Properties
_cancelControl [private]
The element (a button) that the user clicks to cancel a job-creation.
_contactField [private]
The form-field that is the source for a new Job's contact value.
_createControl [private]
The element (a button) that the user clicks to submit a job-creation.
_form [private]
The outermost element that contains all of the _*Field elements of the form. Also the chunk of the UI that will be shown or hidden by various actions (buttons).
_jobManager [private]
The JobManager that new jobs will be added to when they are created.
_nameField [private]
The form-field that is the source for a new Job's name value.
_numberField [private]
The form-field that is the source for a new Job's number value.
_startControl [private]
The element (a button) that the user clicks sow the job-creation form.
_wrapper [private]
The element that wraps all of the UI — including the _form and all _*Control and _*Field elements
Methods
.ctor( wrapper, form )
The object constructor/initializer.
addJobToManager()
Builds the data-structure for a new job, then hands it off to the JobManager to be added.
clearForm()
Clears the values of all of the _*Field elements
hide()[scope]
Hides the _form element
show()[scope]
Shows the _form element

Next time, I'll start digging in to the actual implementation of these classes, and show how I'm integrating them into a process that allows the page mark-up to control what the UI looks like while keeping that separate from the logic embedded in these classes.

Thursday, February 16, 2017

Templating Other Entities

Today's post will be long from a scrolling standpoint, but since there's not much meat to the template-code, it should go pretty quickly. The goal for today is to generate final templates/snippets for interface, abstract class, class and final class constructs that can be used as starting-points for defining those types in project-code without having to start from scratch every single time. If you cringed, or started to think out a non-Pythonic-themed comment when you saw final, stop right now, and read this first.

I'm going to tackle them in the order noted above.

The Interface Template

These cannot be instantiated, and must be extended into concrete implementations. In most languages that I'm aware of, an interface definition cannot have properties (not even abstract ones), and cannot have concrete method-implementations. Python doesn't have that constraint (it doesn't have a formal interface-construct, but allows for abstract property- and method-definition through the abc module, using its ABCMeta as a __metaclass__ specification to define a class that has some abstract members, and the @abstractmethod and @abstractproperty decorator-functions to define which members are abstract.

@describe.InitClass()
class InterfaceName( object ):
    """TODO: Document InterfaceName
Provides interface requirements and type-identity for objects that can 
REPRESENT SOMETHING"""

    #-----------------------------------#
    # Abstraction through abc.ABCMeta   #
    #-----------------------------------#
    __metaclass__ = abc.ABCMeta

    #-----------------------------------#
    # Static interface attributes (and  #
    # default values?)                  #
    #-----------------------------------#

    #-----------------------------------#
    # Abstract Properties               #
    #-----------------------------------#

#    PropertyName = abc.abstractproperty()

    #-----------------------------------#
    # Instance Initializer              #
    #-----------------------------------#
    @describe.AttachDocumentation()
    @describe.todo( 'Document __init__' )
    @describe.todo( 'Implement __init__' )
    def __init__( self ):
        """
Instance initializer"""
        # InterfaceName is intended to be an interface,
        # and is NOT intended to be instantiated. Alter at your own risk!
        if self.__class__ == InterfaceName:
            raise NotImplementedError( 'InterfaceName is '
                'intended to be an interface, NOT to be instantiated.' )
        # Call parent initializers, if applicable.

    #-----------------------------------#
    # Instance Garbage Collection       #
    #-----------------------------------#

    #-----------------------------------#
    # Abstract Instance Methods         #
    #-----------------------------------#

#    @abc.abstractmethod
#    def RequiredMethod( arg1, arg2=None, *args, **kwargs ):
#        raise NotImplementedError( '%s.RequiredMethod is not implemented as '
#            'required by InterfaceName' % self.__class__.__name__ )

    #-----------------------------------#
    # Abstract Class Methods            #
    #-----------------------------------#

    #-----------------------------------#
    # Static Class Methods              #
    #-----------------------------------#

#---------------------------------------#
# Append to __all__                     #
#---------------------------------------#
__all__.append( 'InterfaceName' )

Like the module- and package-templates, a lot of this is just structural, organizational comment-blocks, indicating where to put verious types of members. After the initial declaration of the __metaclass__ for the interface, there are seven major blocks:

  • A place to define static(-ish) interface-level attributes/constants;
  • A place to define abstract (required) properties;
  • A concrete __init__, more on that shortly;
  • A block for a __del__ method, if one needs to be required in derived classes;
  • A block for defining abstract instance-methods;
  • A block for defining abstract class-methods; and
  • A block for defining static methods.

The check for the InterfaceName is in place so that an interface could be made more-or-less operational on Python versions that predate the abc module (sometime in the 2.6.x versions). It'd be kinda tedious, but all that would have to be done would be to remove all of the abc.[whatever] calls and references, and the interface would still be non-instantiable. A similar pattern is also in place for the templated abstract-method for the same reason.

The Abstract Class Template

These also cannot be instantiated, but can contain concrete implementations of properties and methods both, as well as abstract requirements for properties and methods. Much of it is very similar to the Interface template, for much the same reasons, but there are a couple additional sections:

  • Blocks for collecting the (usually protected) property-getter, -setter and -deleter methods for any concrete property implementations; and
  • A concrete- and abstract-property location;

@describe.InitClass()
class AbstractClassName( object ):
    """TODO: Document AbstractClassName
Provides baseline functionality, interface requirements and type-identity for 
objects that can REPRESENT SOMETHING"""

    #-----------------------------------#
    # Abstraction through abc.ABCMeta   #
    #-----------------------------------#
    __metaclass__ = abc.ABCMeta

    #-----------------------------------#
    # Class attributes (and instance-   #
    # attribute default values)         #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-getter methods  #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-setter methods  #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-deleter methods #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Properties (abstract OR  #
    # concrete!)                        #
    #-----------------------------------#

#    PropertyName = abc.abstractproperty()
#    PropertyName = describe.makeProperty()

    #-----------------------------------#
    # Instance Initializer              #
    #-----------------------------------#
    @describe.AttachDocumentation()
    @describe.todo( 'Document __init__' )
    @describe.todo( 'Implement __init__' )
    def __init__( self ):
        """
Instance initializer"""
        # AbstractClassName is intended to be an abstract class,
        # and is NOT intended to be instantiated. Alter at your own risk!
        if self.__class__ == AbstractClassName:
            raise NotImplementedError( 'AbstractClassName is '
                'intended to be an abstract class, NOT to be instantiated.' )
        # Call parent initializers, if applicable.
        # Set default instance property-values with _Del... methods as needed.
        # Set instance property values from arguments if applicable.

    #-----------------------------------#
    # Instance Garbage Collection       #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Methods                  #
    #-----------------------------------#

#    @abc.abstractmethod
#    def RequiredMethod( arg1, arg2=None, *args, **kwargs ):
#        raise NotImplementedError( '%s.RequiredMethod is not implemented as '
#            'required by InterfaceName' % self.__class__.__name__ )

    #-----------------------------------#
    # Class Methods                     #
    #-----------------------------------#

    #-----------------------------------#
    # Static Class Methods              #
    #-----------------------------------#

#---------------------------------------#
# Append to __all__                     #
#---------------------------------------#
__all__.append( 'AbstractClassName' )

There are a few more prompt-comments in the __init__ method as well, keeping with my normal programming structure: I explicitly delete all of an instance's properties during initialization so that they will always have a value after instances are first created.

The Class Template

Classes are intended to be instantiated, and can be extended freely. This template doesn't have anything new, that I haven't shown before, so there's not much that can be said about it.

@describe.InitClass()
class ClassName( object ):
    """TODO: Document ClassName
Represents SOMETHING"""
    #-----------------------------------#
    # Class attributes (and instance-   #
    # attribute default values)         #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-getter methods  #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-setter methods  #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-deleter methods #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Properties               #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Initializer              #
    #-----------------------------------#
    @describe.AttachDocumentation()
    @describe.todo( 'Document __init__' )
    @describe.todo( 'Implement __init__' )
    def __init__( self ):
        """
Instance initializer"""
        # Call parent initializers, if applicable.
        # Set default instance property-values with _Del... methods as needed.
        # Set instance property values from arguments if applicable.
        pass # TODO: Remove this line after __init__ is implemented

    #-----------------------------------#
    # Instance Garbage Collection       #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Methods                  #
    #-----------------------------------#

    #-----------------------------------#
    # Class Methods                     #
    #-----------------------------------#

    #-----------------------------------#
    # Static Class Methods              #
    #-----------------------------------#

#---------------------------------------#
# Append to __all__                     #
#---------------------------------------#
__all__.append( 'ClassName' )

The Final Class Template

A final class is intended to be instantiated, but not extended. The only significant difference between this template and the Class template above is how the __init__ handles extension-attempts:

    #-----------------------------------#
    # Instance Initializer              #
    #-----------------------------------#
    @describe.AttachDocumentation()
    @describe.todo( 'Document __init__' )
    @describe.todo( 'Implement __init__' )
    def __init__( self ):
        """
Instance initializer"""
        # FinalClassName is intended to be a nominally-final class
        # and is NOT intended to be extended. Alter at your own risk!
        #---------------------------------------------------------------------#
        # TODO: Explain WHY it's nominally final!                             #
        #---------------------------------------------------------------------#
        if self.__class__ != FinalClassName:
            raise NotImplementedError( 'FinalClassName is '
                'intended to be a nominally-final class, NOT to be extended.' )
        # Call parent initializers, if applicable.
        # Set default instance property-values with _Del... methods as needed.
        # Set instance property values from arguments if applicable.

After taking some time to roll the doc_metadata.py file into the new standard template, I can (finally) make good on my promise to make it available for download, as well as the collection of template-files:

109.6kB

It's been fourteen posts now, and just over a month since I started in on the documentation-process. I'm not sure exactly where I want to go next on the Python side of things, though I have a couple of ideas, so I think for my next post, I'll play around a bit with implementing some design-patterns in JavaScript. If nothing else, that will give me some time to ponder what the next logical step is.