JavaScript Theory/Design Question

JavaScript and client side scripting.

Moderator: General Moderators

Post Reply
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

JavaScript Theory/Design Question

Post by Chris Corbyn »

I'm trying to take a MVC type of approach to designing a JavaScript application. This is responds to use commands (typed only) and displays various bits and pieces on the page.

The things that may change in the display are:
* The main window area (just a big DIV like a terminal window)
* The command prompt
* The line the user types on
* A status bar/box (notification of what's happening on the ajax side of things)

I've already broken my code into several classes on the client side like this:

* Input - exists as a single object, handles all user input, contains a buffer object
* Output - Handles the display (aspects shown above)
* Buffer - Keeps a log of command keyed by the user and allows iterating backwards and forward through them by pressing UP and DOWN (like a terminal)

I'm getting a bix mixed up with what's generating output however. For example, when an item is addded to the buffer, the buffer wants to update the diaply to remove the line of text the user typed so it generates a new Output(), sets the buffer contents empty then flushes it itself.

Now similarly, when the user presses UP or DOWN, Input() asks Buffer() to get the next or previous item and then Input() generates a new Output(), sets the buffer contents to the relevant item and calls flush().

Can I centralise my output any more than this? It seems I'm going to be genrating new Output() in several areas, although the specific logic for this output will be dealt with by the Output() class itself.

Input()

Code: Select all

/**
 * @fileoverview Contains the Input handler class for processing what is typed.
 * The input handler reads user input and either generate a particular output
 * directly, or makes a new Request() to send to the server.
 *
 * @author Chris Corbyn <chris@w3style.co.uk>
 * @version 0.1
 */

/**
 * Constructor for the Input handler
 * @class Input
 * @constructor
 */
function Input()
{
	/**
	 * The DOM element where the user is typing input
	 * @type Element
	 */
	var DOMElement = document.getElementById('phpmyajax_input');
	/**
	 * The buffer keeps a log of what the user types so they can press
	 * UP and DOWN to repeat previous commands.
	 * @type Buffer
	 */
	var buffer = new Buffer();
	/**
	 * Maps ascii key codes to callback functions
	 * @type Array
	 */
	var keyHandles = new Array();
	keyHandles[38] = bufferGoBack;		//UP arrow
	keyHandles[40] = bufferGoForward;	//DOWN arrow
	keyHandles[13] = executeCommand;	//ENTER key
	
	/**
	 * The type of object this is (used for identification)
	 * @return {string} type
	 */
	this.getType = function()
	{
		return 'Input';
	}
	/**
	 * Takes a string of user input and either stores it as a partially completed string,
	 * makes a new request, or instantly asks Output() to do something.
	 * @param {int} command
	 */
	function process(str)
	{
		if (str == '\\clear')
		{
			buffer.clear();
		}
		else if (str.length) buffer.store(str);
	}
	/**
	 * Reads every single keypress from the user and fires actions
	 * upon certain keypresses
	 * @param {event} keypress
	 */
	this.readKey = function(e)
	{
		e = e || window.event;
		
		if (typeof keyHandles[e.keyCode] != 'undefined')
		{
			callback = keyHandles[e.keyCode];
			callback();
		}
	}
	/**
	 * Work backwords through the buffer and get the previous command
	 * @throws Output
	 */
	function bufferGoBack()
	{
		if (!buffer.length) return;
		
		if (buffer.prev())
		{
			var output = new Output();
			output.setItem(buffer.getItem(), buffer.getType());
			output.flush();
		}
	}
	/**
	 * Work backwords through the buffer and get the previous command
	 * @throws Output
	 */
	function bufferGoForward()
	{
		if (!buffer.length) return;
		
		if (buffer.next())
		{
			var output = new Output();
			output.setItem(buffer.getItem(), buffer.getType());
			output.flush();
		}
	}
	/**
	 * Trigger the process() method
	 */
	function executeCommand()
	{
		process(DOMElement.value);
	}
}
Buffer()

Code: Select all

/**
 * @fileoverview Contains the command buffer class.
 * The command buffer tracks what the user has typed and offers iterators to
 * move backwards and forwards through the entries.
 *
 * @author Chris Corbyn <chris@w3style.co.uk>
 * @version 0.1
 */

/**
 * Constructor for the Buffer
 * @class Buffer
 * @constructor
 */
function Buffer()
{
	/**
	 * Stack of entries in the buffer
	 * @type Array
	 */
	var items = new Array();
	/**
	 * The last thing that was stored in the buffer
	 * @type string
	 */
	this.lastItem = null;
	/**
	 * The current position in the entries
	 * @type int
	 */
	this.position = 0;
	/**
	 * The size of the list of entries
	 * @type int
	 */
	this.length = 0;
	
	/**
	 * Used for identification
	 * @return {string} type
	 */
	this.getType = function()
	{
		return 'Buffer';
	}
	/**
	 * Log an entry into the buffer
	 * @param {string} entry
	 */
	this.store = function(str)
	{
		if (str != this.lastItem && str.charAt(0) != '\\')
		{
			items[this.length] = str;
			this.lastItem = str;
			this.length++;
			this.position = this.length; //Back to end
		}
		var output = new Output();
		output.setItem('', this.getType());
		output.flush();
	}
	/**
	 * Empty the buffer contents
	 */
	this.clear = function()
	{
		for (var i in items)
		{
			delete items[i];
		}
		this.lastItem = null;
		this.length = 0;
		this.position = 0;
		var output = new Output();
		output.setItem('', this.getType());
		output.flush();
	}
	/**
	 * Retreive the item from the buffer at the current position
	 * @return {string} entry
	 */
	this.getItem = function()
	{
		return items[this.position];
	}
	/**
	 * Iterate forwards
	 */
	this.next = function()
	{
		if (this.position < this.length-1)
		{
			this.position++;
			return true;
		}
		return false;
	}
	/**
	 * Iterate backwards
	 */
	this.prev = function()
	{
		if (this.position > 0)
		{
			this.position--;
			return true;
		}
		return false;
	}
}
Output() -- so far

Code: Select all

/**
 * @fileoverview Contains the Output handler class for displaying data.
 * The output handler is passed an item or several items and displays them
 * when flush() is called.
 *
 * @author Chris Corbyn <chris@w3style.co.uk>
 * @version 0.1
 */

/**
 * Constructor for the Input handler
 * @class Output
 * @constructor
 */
function Output()
{
	/**
	 * There are certain types of output (screen, input box, status bar, prompt)
	 * @type array
	 */
	var items = new Array();
	/**
	 * The node where the user is typing
	 * @type element
	 */
	var DOMInputElement = document.getElementById('phpmyajax_input');
	
	/**
	 * Add something to output
	 * @param {string} item
	 * @param {string} item type
	 */
	this.setItem = function(strItem, type)
	{
		items[type] = strItem;
	}
	/**
	 * Flush the output to the display
	 */
	this.flush = function()
	{
		for (var t in items)
		{
			switch(t)
			{
				case 'Buffer':
				updateBuffer(items[t]);
				break;
			}
		}
	}
	/**
	 * Change the contents of the input buffer
	 * @param {string}
	 */
	function updateBuffer(str)
	{
		DOMInputElement.value = str;
		DOMInputElement.focus();
	}
}
Thoughts?

EDIT | There's a test page up at http://www.w3style.co.uk/~d11wtq/PhpMyAjax/ if you're wondering what on earth I'm talking about. I'm only creating the skeleton for it at the moment, once the terminal functions I can do Request/Response.
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Post by Ambush Commander »

I'm not that well versed in JavaScript, but I'll try to comment, if only in abstract.

Javascript introduces the ability for multiple views to depend on the same data and the need for updates to that data to be pushed out dynamically. Patterns that had little relevance in PHP now are extremely applicable, in particular, the Controller in MVC (not your Front Controller, mind you) and the Observer pattern.

I'd have to take a look more carefully at the code to comment more... my Javascript really isn't up to snuff. :-/
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Ambush Commander wrote:I'm not that well versed in JavaScript, but I'll try to comment, if only in abstract.

Javascript introduces the ability for multiple views to depend on the same data and the need for updates to that data to be pushed out dynamically. Patterns that had little relevance in PHP now are extremely applicable, in particular, the Controller in MVC (not your Front Controller, mind you) and the Observer pattern.

I'd have to take a look more carefully at the code to comment more... my Javascript really isn't up to snuff. :-/
I did consider making output a singleton then passing it to Buffer(), Request() and Response() as an observer but it didn't feel quite as clean as simply generating new Output() on-the-fly.

The way the Output() object works is that you set as many items of set types into it and nothing happens until flush() is run.

For example, you might want to update the input buffer (the area where the user types) so you call this:

Code: Select all

var output = new Output();
output.setItem('New contents of buffer', 'Buffer');
//The above appears as this in my code: output.setItem(buffer.getItem(), buffer.getType());
You might also want to update the prompt from "prompt> " to "Password: " if we're asking for login for example:

Code: Select all

output.setItem('Password:', 'Prompt');
Then when you've finished setting everything that needs to change in the view you simply:

Code: Select all

output.flush();
I'm even contemplating making a Prompt() class which observes Input() and deals with what the prompts says itself.... but again, Prompt() would need to call Output() itself.

Obersvers do seem to play very nicely in stuff like this and because it's not like PHP where the script runs from top to bottom then finishes you actually see the observers doing cool things as you interact with the page.
User avatar
Weirdan
Moderator
Posts: 5978
Joined: Mon Nov 03, 2003 6:13 pm
Location: Odessa, Ukraine

Post by Weirdan »

Making new output every once in a while just seems... weird :) Your UI elements remains the same, why re-create Output objects?
User avatar
Weirdan
Moderator
Posts: 5978
Joined: Mon Nov 03, 2003 6:13 pm
Location: Odessa, Ukraine

Post by Weirdan »

Commenting as I read your code:
  • As of now, your Output class is hardcoded to use your phpmyajax_input input box
  • instead of re-creating output your could inplement something like Output.clear() method
  • Currently, your Buffer is just command history, why not name it CommandHistory or something like that?
User avatar
Weirdan
Moderator
Posts: 5978
Joined: Mon Nov 03, 2003 6:13 pm
Location: Odessa, Ukraine

Post by Weirdan »

The questionable idea I have:
you can split the controller part from your Input into standalone object and integrate Output functionality into Input object. This way you would encapsulate all the operations involving phpmyajax_input control into one object. The Output then would be responsible for displaying data on your main div (console). Controller would be observer to events happening with your Input then.

what do you think?
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

w00t! Some feedback :)

You're right on with the output thing. What I've done with Output() now is what should have happened in the first place. When it flushes it also now deletes what was set... that's how an output flush should work right? The contents of the display are buffered to properties, flush() runs, they get dumped to screen then discarded from the buffer. No need to have new Output() objects everywhere now. Two fixed streams "Input" and "Output"... much better cos I can now do things like this :)

Code: Select all

output.setItem(prompt, Prompt.getType());
output.setItem(buffer.getItem(), Buffer.getType());
output.flush();

buffer.prev();
output.setItem(buffer.getItem(), Buffer.getType());
output.flush();
I love the way Output() reads where the content was provided from and decides how it should be displayed on that premise so I think I'm going to keep the Input/Buffer/Request/Response objects separate like this and it seems to work extremely well.

Output() basically sees it like this:
* If output was provided by Buffer() it's for the input buffer to be updated
* If output was provided by Input() it's for displaying the user's command in the display area
* If output was provided by ErrorMsg() it's for showing an error
* If output was provided by Request() it's for showing the user the current status of the HTTP request (in the title bar)
* If output was provided by Response() it's for listing results in the display area

The hard-coded names are going to be moved into a settings/config file... I just have them there while I was playing around with the theory :)

I think I've pretty much get the basic readline/terminal emulation working so I'm moving on to the AJAX part now...

Merging output functionality into Input() feels a bit wrong since that sort of what I was trying to avoid. I could register the DOM elements somewhere and have all objects refer to the regsitered nodes rather than using DOM to retreive the themselves... might be a bit of an overjill though for the sake of one duplicated getElementById().

Thanks for the input. I'll probably have more question regarding the Request/Response objects on the client-side... :)
Post Reply