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
The resulting
interface/members that they share (barring implementation details) are:
Job
s in the JobManager
.
- Properties
-
_jobManager
[private]- A reference to the
JobManager
that provides the collection ofJob
s 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 sortJob
s for display in the UI that the instance controls. sortOrder
- The sort-order (
up
ordown
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 aJobListDisplay
, but not the wrapper for the separateJobContactList
orJobNameList
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 ofJob
displays, retrieved bygetSortedJobs
and formatted bygetJobDisplay
. getJobDisplay( job )
- Generates DOM-elements using the template markup of
_listItemTemplate
, and populating the placeholders therein with data from the suppliedjob
. 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
.
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:
Job Number,
Job Titleand
Job Contactheaders) to allow the user to sort by those values, and to toggle the sort-order between
upand
downsorts. 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 specifiedfield
as the sort-key. setSortCriteria( field, order )
[scope]- Sets the
sortField
andsortOrder
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 itsouterHTML
(which would include the tag itself) is passed toJobListView.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 thedata-sort
attribute as thefield
that the element will sort by/on.
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:
Job
s 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:
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 Job
s. It lives in the header
of the job-display list (the bold, red part below):
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 ofJob
s 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 Job
s. That, plus a functional demo of the whole UI is
coming next...