The last piece of this OO JavaScript UI is the bit that handles the form for
adding jobs to the JobManager.
This is not a Real World Example...
In a real-world application scenario, this would probably be a lot
more complicated — The current popular approaches would probably have it
posting data to a web-service end-point that would return a
JSON result-set that
would be (or include) the validated job-data, possibly with some additional data
indicating the success or failure of the post-attempt. That job-data would then
be passed to the JobManager's addJob method, and the
various displays would update accordingly.
The POC-level approach here doesn't do
any of that, though there are a couple of places where that sort of connection to
a server-side process could be introduced. Me, I think I would drop it into
JobManager.addJob, but that's a knee-jerk thought, and other places
might well be better.
At any rate, here's the last piece of the puzzle...
The JobAdderForm class
JobAdderForm attaches to the job-entry form in the main UI:
It is responsible for accepting data for new jobs, building a data-structure that
can be submitted to JobManager.addJob, and submitting it. Its
implementation looks like this:
function JobAdderForm( wrapper, form )
{
/*
* Provides the functionality for adding a job to the JobManager
*
* wrapper ........... (element or string, required) The element
* (or the ID of the element) that wraps all
* of the UI.
*/
// Processing that needs to happen before definition, if any
if ( typeof wrapper == 'string' )
{ wrapper = document.getElementById( wrapper ); }
if ( typeof form == 'string' )
{ form = document.getElementById( form ); }
// Instance properties:
// + Private properties
var _cancelControl = null;
var _contactField = null;
var _createControl = null;
var _form = form;
var _nameField = null;
var _numberField = null;
var _startControl = null;
var _wrapper = wrapper;
var _jobManager = new JobManager();
// - Find the job-fields and -actions elements, and assign
// them as appropriate
allChildren = wrapper.getElementsByTagName( '*' );
for( ci=0; ci<allChildren.length; ci++ )
{
child = allChildren[ ci ];
// data-jobfield
jobField = child.getAttribute( 'data-jobfield' );
switch( jobField )
{
case 'contact':
_contactField = child;
break;
case 'name':
_nameField = child;
break;
case 'number':
_numberField = child;
break;
}
// data-jobaction
jobAction = child.getAttribute( 'data-jobaction' );
switch( jobAction )
{
case 'cancelJob':
_cancelControl = child;
break;
case 'createJob':
_createControl = child;
break;
case 'startJob':
_startControl = child;
break;
}
}
// TODO: This is where any validation that the REQUIRED fields
// and actions exist should be done.
// Initialize as needed based on incoming arguments
// Instance methods:
// + public methods
this.addJobToManager = function()
{
/*
* Collects the job-data from the form, generates a new
* data-object with all the job-data in it, and adds it
* to the JobManager.
*/
newJob = {
'contact':_contactField.value,
'name':_nameField.value,
'number':_numberField.value,
};
_jobManager.addJob( newJob );
this.clearForm();
}
this.clearForm = function()
{
/*
* Clears all of the fields in the job-adder form
*/
if ( _contactField )
{ _contactField.value = ''; }
if ( _nameField )
{ _nameField.value = ''; }
if ( _numberField )
{ _numberField.value = ''; }
}
this.hide = function()
{
/*
* Hides the form
*/
_form.style.display = 'none';
}
this.show = function()
{
/*
* Shows the form
*/
_form.style.display = 'block';
}
// Event-like methods of the instance
// Processing that needs to happen before returning the instance, if any
// - Attach events to controls
if ( _cancelControl )
{
_cancelControl.jobAdderForm = this;
_cancelControl.addEventListener( 'click', function( event )
{
this.jobAdderForm.clearForm();
this.jobAdderForm.hide();
}
);
}
if ( _createControl )
{
_createControl.jobAdderForm = this;
_createControl.addEventListener( 'click', function( event )
{
this.jobAdderForm.addJobToManager();
this.jobAdderForm.hide();
}
);
}
if ( _startControl )
{
_startControl.jobAdderForm = this;
_startControl.addEventListener( 'click', function( event )
{
this.jobAdderForm.show();
}
);
// If there is a start-control, it's responsible for showing
// the form, so hide the form...
this.hide();
}
// Return the instance
return this;
}
The interface:
Properties
_cancelControl[private]
The UI control (a button, link, or other element) that cancels
a job-submission.
_contactField[private]
The text-field that will contain the contact
property-value of the new job.
_createControl[private]
The UI control (a button, link, or other element) that
submits a job-submission.
_form[private]
The outermost element of the form that contains all of the
_*Field elements and the _*Control elements associated with the form. This probably won't include the _startControl (since it should reside outside the _form, lest it be hidden).
_jobManager[private]
The JobManager object that new job-submissions will
be sent to.
_nameField[private]
The text-field that will contain the name
property-value of the new job.
_numberField[private]
The text-field that will contain the number
property-value of the new job.
_startControl[private]
The UI control (a button, link, or other element) that starts
the job-submission process (by showing the form).
_wrapper[private]
The element that contains the entire UI for the
application, including the _form and all of the
_*Control elements.
Methods
addJobToManager()
Gathers the values from the _*Field elements, builds
a Job-compatible data-structure from them, and sends
that to JobManager.addJob.
clearForm()
Clears the values of all the _*Field
elements.
hide()
Hides the _form element.
show()
Shows the _form element.
JobAdderForm has no public properties — the wrapping
UI element, the form and all of the fields and controls are auto-assimilated using a
process much like the one built out for JobListView early in the
initialization process of the object's construction. While it wouldn't be too
difficult, I think, to allow items to be specified as part of a parameters
argument (as has been the pattern elsewhere), but my gut feeling was to auto-connect
everything involved. That's simpler from an integration standpoint (and it was less
code for me to write, to boot).
All of its method are pretty straightforward, too. The only potential caveat is
that as things stand right now, there is no checking performed with regards to whether
the various _*Field fields are found and associated. Again, since this
is pretty much a POC-level effort on my part, I simply didn't want to take the time
to implement that level of initialization-validation.
Putting it All Together
With all of the markup-template in place (with all of the necessary
data-* attributes), and the full object-library included, the only
thing remaining is to initialize the set of objects. That's a fairly small chunk
of code:
// Create some test jobs for inclusion into the initial view
testJobs = [
{ 'number':'OOK00001', 'name':'Job Number One', 'contact':'Joe Smith' },
{ 'number':'WOK00001', 'name':'Job Number Two', 'contact':'John Doe' },
{ 'number':'OOK00002', 'name':'The Third Job', 'contact':'Richard Roe' },
{ 'number':'WOK00002', 'name':'Fourth Is This Job', 'contact':'Pat Jones' },
{ 'number':'OOK00003', 'name':'The Fifth Job', 'contact':'Richard Roe' },
{ 'number':'WOK00003', 'name':'Sixth Is This Job', 'contact':'Pat Jones' },
];
// Create the main job-manager instance:
jobManager = new JobManager( testJobs );
// Initialize the main job-list view, with all the initial sort-settings
currentJobListParameters = {
'top':'currentJobList',
'listElement':'managedJobsList',
'sortField':'number',
'sortOrder':'up',
};
mainListView = new JobListView( currentJobListParameters );
// Create the job-contact-list view, with all the initial sort-settings
jobContactListParameters = {
'top':'jobContactList',
'listElement':'managedContactlist',
};
jobContactList = new JobContactList( jobContactListParameters );
// Create the job-name-list view, with all the initial sort-settings
jobNameListParameters = {
'top':'jobNameList',
'listElement':'managedNameList',
};
jobNameList = new JobNameList( jobNameListParameters );
// Set up the job-counter(s)
mainJobCounter = new JobCounter( 'jobCounter' );
// Set up the job-adder form
jobAdderForm = new JobAdderForm( 'ui-example', 'newJobForm' );
// After all the initialization is done, and all the objects' inter-relationships
// are established, start the job-update cycle across all of the displays:
jobManager.jobsChanged();
And here it is, live:
name
contact
Is This an MVC...?
Eh, maybe.
If it isn't a Model-View-Controller,
it certainly shares some of that pattern's structure. Consider:
JobManager is Model-like
It directly manages the data of the application;
It also directly manages all of the logic (such as there is) of
the application, and could be modified to manage any rules
around the data, if any were needed;
The display-objects (JobDisplayList,
JobContactList, JobNameList, and
JobCounter) are definitely Views
That's what they do: Displaying data from (or derived from)
the JobManager
Where it gets questionable, I think, is in the Controller part of the
pattern. There's no one, single controller-object in this mix, though the
JobAdderForm arguably comes closest. Any of the various sort-controls
in the JobDisplayList are also controller-like, and at some level,
so is the entire page that the UI resides in.
If you want to play with the code, it's available for download. It runs well
enough on my local environment as a local HTML page and JavaScript file (that's
in the current version of Google Chrome, your mileage may vary from browser to
browser):
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:
Job NumberJob TitleJob Contact
Job-Number #1Job-Title #1Job-Contact #1
Job-Number #2Job-Title #2Job-Contact #2
...
Job-Number #10Job-Title #10Job-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 firstdata-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.)
Alldata-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:
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:
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):
...
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...
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:
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 originalJobManager 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: