UNPKG

thywill

Version:

A Node.js clustered framework for single page web applications based on asynchronous messaging.

306 lines (276 loc) 8.95 kB
/*global Handlebars: false, Thywill: false */ /** * @fileOverview * Client Javascript for the Calculations application. */ (function () { 'use strict'; // ------------------------------------------ // Define an Echo application class. // ------------------------------------------ /** * @class * An implementation of Thywill.ApplicationInterface for the Calculations * application. * * @see Thywill.ApplicationInterface */ function CalculationsApplication (applicationId) { Thywill.RpcCapableApplicationInterface.call(this, applicationId); // For storing Handlebars.js templates. this.templates = {}; // Data for the various operations shown on the page. this.operations = { multiplyByTwo: { operationText: '* 2', rpc: { name: 'multiplicative.multiplyByTwo', hasCallback: false } }, divideByTwo: { operationText: '/ 2', rpc: { name: 'multiplicative.divideByTwo', hasCallback: true } }, square: { operationText: '^ 2', rpc: { name: 'powers.square', hasCallback: false } }, squareRoot: { operationText: '^ 0.5', rpc: { name: 'powers.squareRoot', hasCallback: true } } }; } Thywill.inherits(CalculationsApplication, Thywill.RpcCapableApplicationInterface); var p = CalculationsApplication.prototype; // ------------------------------------------ // User Interface Methods // ------------------------------------------ /** * Create the application user interface and its event listeners. */ p.uiSetup = function () { var self = this; // The user interface is contained in this one template. this.templates.uiTemplate = Handlebars.compile(jQuery('#{{{uiTemplateId}}}').html()); // We'll need the operations definitions for rendering. var operations = Object.keys(this.operations).map(function (key, index, array) { self.operations[key].id = key; return self.operations[key]; }); // Render and display the template. jQuery('body').append(this.templates.uiTemplate({ operations: operations })); // Add references to elements to the operations definitions. for (var key in this.operations) { this.operations[key].inputElement = jQuery('#' + key + ' input'); this.operations[key].resultElement = jQuery('#' + key + ' .result'); } }; /** * Make the UI disabled - no operations can be carried out. */ p.uiDisable = function () { jQuery('input').attr('disabled', true).removeClass('enabled').off('keyup'); }; /** * Make the UI enabled and allow operations. */ p.uiEnable = function () { var self = this; // Enable the operations on data changing in the inputs. jQuery('input').removeAttr('disabled').addClass('enabled').on('keyup', function (e) { var elem = jQuery(this); self.inputValueChanged(elem.attr('operation'), elem.val()); }); }; /** * Display an error associated with an operation. */ p.uiError = function (id, error) { var operation = this.operations[id]; var speed = 100; switch (error) { case this.rpcErrors.DISCONNECTED: error = 'Not connected.'; break; case this.rpcErrors.NO_FUNCTION: error = 'Missing server function.'; break; case this.rpcErrors.NO_PERMISSION: error = 'Access forbidden.'; break; case this.rpcErrors.TIMED_OUT: error = 'Timed out.'; break; default: } operation.resultElement.fadeOut(speed, function () { operation.resultElement.addClass('result-error').html(error).fadeIn(speed); }); }; /** * Update the display of an operation result. * * @param {string} id * Operation ID. * @param {string|number} result * The result to display. */ p.uiUpdateResult = function (id, result) { // NaN and Infinity come through the JSON as null. If this was a more // complete example, it might deal with complex numbers and so forth. // But it doesn't. if (result === null) { this.uiError(id, 'Invalid calculation.'); return; } if (typeof result === 'number') { if (result % 1 === 0) { result = result.toFixed(0); } else { result = result.toFixed(4).replace(/0+$/, ''); } } var operation = this.operations[id]; var speed = 100; operation.resultElement.fadeOut(speed, function () { operation.resultElement.removeClass('result-error').html(result).fadeIn(speed); }); }; /** * Change the status message. */ p.uiStatus = function (text, className) { var status = jQuery('#status'); var speed = 100; status.fadeOut(speed, function () { status.html('[ ' + text + ' ]') .removeClass('connecting connected disconnected') .addClass(className) .fadeIn(speed); }); }; // ------------------------------------------ // Other Methods // ------------------------------------------ /** * An input value changed. Delay, then send an RPC to the server. * * @param {string} id * The ID of the operation. * @param {string} value * The current input value. */ p.inputValueChanged = function (id, value) { var self = this; var operation = this.operations[id]; // Using interval to set up a delay that we keep extending with each new // value change that happens before the delay completes. if (operation.intervalId) { clearInterval(operation.intervalId); } operation.intervalId = setInterval(function () { // Clear the interval - we only want the one function call. clearInterval(operation.intervalId); delete operation.intervalId; // Stash the value in an array of values. Do this to avoid oddball timing // issues relating to responses returning out of order. operation.pending = operation.pending || []; operation.pending.push(value); // Send the RPC. var data = { name: operation.rpc.name, hasCallback: operation.rpc.hasCallback, args: [parseFloat(value, 10)] }; self.rpc(data, function (error, result) { if (error) { self.uiError(id, error); } else { // Only display this result if the value parameter is still the last submitted value parameter. var display = (operation.pending.length && operation.pending[operation.pending.length - 1] === value); // Remove from the pending list. operation.pending = operation.pending.filter(function (element, index, array) { return value !== element; }); if (display) { self.uiUpdateResult(id, result); } } }); }, 200); }; /** * Rudimentary logging. * * @param {string} logThis * String to log. */ p.log = function (logThis) { console.log(logThis); }; /** * @see Thywill.ApplicationInterface#received */ p.received = function (message) { // This application only uses RPC, so no other messages // should arrive here. }; /** * @see Thywill.ApplicationInterface#connecting */ p.connecting = function () { this.uiStatus('Connecting...', 'connecting'); this.log('Client attempting to connect.'); }; /** * @see Thywill.ApplicationInterface#connected */ p.connected = function () { this.uiStatus('Connected', 'connected'); this.uiEnable(); this.log('Client connected.'); }; /** * @see Thywill.ApplicationInterface#connectionFailure */ p.connectionFailure = function () { this.uiStatus('Disconnected', 'disconnected'); this.uiDisable(); this.log('Client failed to connect.'); }; /** * @see Thywill.ApplicationInterface#disconnected */ p.disconnected = function () { this.uiStatus('Disconnected', 'disconnected'); this.uiDisable(); this.log('Client disconnected.'); }; // ---------------------------------------------------------- // Create an application instance and set it up. // ---------------------------------------------------------- // Create the application instance. The application ID will be populated // by the backend via the Handlebars template engine when this Javascript // file is prepared as a resource. var app = new CalculationsApplication('{{{applicationId}}}'); // Initial UI setup. jQuery(document).ready(function () { app.uiSetup(); }); })();