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
anddata-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.
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 Job
s, 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 Job
s 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 Job
s 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 Job
s 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 )
andremoveDisplay( 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 theJob
s that theJobManager
is keeping track of. addJob( job, updateNow )
andremoveJob( job, updateNow )
- Provide a mechanism for adding or removing a single
Job
from the collection ofJob
s that theJobManager
is keeping track of. TheupdateNow
argument (which defaults to atrue
value) allows a developer to write code that adds a series ofJob
s to the instance, then explicitly calling for an update (withjobsChanged()
. That process, minus the explicit call tojobsChanged()
, 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 beJob
instances, but frequently are), returningtrue
if both objects provided have the same values for theircontact
,name
andnumber
properties. In practice, I've found that one of the two objects being compared is frequently not aJob
instance, though in those cases, it has always been a generic object with the same properties as aJob
. getSortedJobs( sortKey, sortOrder )
- Returns a sequence of
Job
objects, sorted by thesortKey
property-values of those objects, in thesortOrder
direction. This was needed because each display-object in the codebase can have its own sort-criteria:JobContactList
will always sort bycontact
, ascending;JobNameList
will always sort byname
, ascending; andJobListView
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:
No comments:
Post a Comment