Proudly ProcrasDonating

Technology Thoughts

Super Submenu Abstraction

Keeping in line with my last post on our testing sequentializer, another neat code snippet I’d like to pluck for attention is how we generate submenus on extension pages.

Extension pages are web pages that are constructed directly by the extension on a user’s computer. For example, My Progress pages have four tabs:

  1. Gauges
  2. Sites
  3. Visits
  4. Trends

If a user without our extension goes to the My Progress page, http://procrasdonate.com/my_progress/ , then a single error page is generated by our server. However, a user with our extension is able to view multiple pages at that same URL simply by clicking items in a submenu.

Below are two screenshots of the My Progress pages. In the first, the “Gauges” tab is shown. In the second, the “Trends” tab is shown. Note that the submenu is the same on all My Progress tabs.

my_progress_screenshot

Gauges tab on My Progress page

Trends tab on My Progress page

Trends tab on My Progress page

Below are two screens of other extension pages. The My Impact pages all have a single table with four tabs for displaying different tables. The Registration pages are a series of forms for setting up ProcrasDonate for the first time. Both My Impact and Registration use a submenu to generate content.

my_impact_screenshot

Show All tab on My Impact page

Charities tab on Registration page

Charities tab on Registration page

Submenu Abstraction

You’re probably already smiling with how cool this situation is. Let’s break it down to savor the moment.

  1. We have a single submenu concept: a list of submenu pages plus metadata such as tab name and tab content. The data depends on the page set, eg My Progress, My Impact or Registration, but the data structure is the same.
  2. Submenus are displayed differently on each page set, but the functionality—click on menu item to change page content—remains the same.

The second abstract in which we display the same kind of data differently can be handled by creating a different html template for each submenu. The template for displaying the My Impact table tables has different HTML than the template for displaying My Progress images and tab names.

The super awesome part is that the structure of the submenu data is the same regardless of the page set. This allows the view to re-use submenu creation and event handling.

Submenu Data Structure

Each time a view is called to render the contents of a tab, it must generate a submenu data structure. This data structure is a list of submenu items. Each item contains the following properties:

  • substate (useful as id)
  • tab name (user visible)
  • list of properties (eg: prev, selected)

Observe that this structure is keenly linked to the display abstraction. Each menu item includes properties relevant to display and user events. However, actual display implementations are left to the templates.

This data structure is generated by the view based on the user’s state and the submenu specifications.

Submenu Specification

The submenu specification is listed as constants in the extension’s settings file.

The specifications include the tab names, substate names, submenu images if desired, content insertion functions (aka views) and process functions if desired (useful on registration forms to detect whether errors need to be resolved before continuing).

The My Progress submenu data structure looks like this:

constants.DEFAULT_PROGRESS_STATE = "gauges"; constants.PROGRESS_STATE_ENUM = [         "gauges", "classifications", "visits", "trends", /*"stacks", "averages", "ratios"*/ ]; constants.PROGRESS_STATE_TAB_NAMES = [         "Gauges", "Sites", "Visits", "Trends", /*"Stacks", "Averages", "Ratios"*/ ]; constants.PROGRESS_STATE_INSERTS = [         "insert_progress_gauges",         "insert_progress_classifications",         "insert_progress_visits",         "insert_progress_trends",         /*"insert_progress_stacks",         "insert_progress_averages",         "insert_progress_ratios"*/ ];

I included the commented out submenu items because they illustrate why this abstraction is so cool. Once the abstraction is written, adding submenu items or even whole submenus and pagesets, becomes easier and more importantly, less tedious. The more real coding instead of infrastructure copy/pasting that I do, the more productive and happier I am, and the less likely the code contains stupid bugs.

Submenu Activation and Processing

All extension views share a common process:

  1. Set substate if necessary
  2. Generate the submenu
  3. Retrieve content data structures
  4. Render template and insert into page
  5. Activate html
  6. Activate submenu html

Activation means adding event handlers to the inserted html. Activating the submenu specifically means adding click events for each submenu item.

When a user clicks a submenu item, the appropriate view needs to be called. If a content processor function is specified, then that needs to be called first to ensure that the user doesn’t leave a page with erroneous inputs.

I don’t want to go into our view system too much, but it should be clear how the submenu framework fits cleanly into the view model. Our main goal is to make writing views actually equal writing the data retrieval, template html and html activation if there is any. All the other bullet items could be automated by the view framework.

(Whether everything is abstracted and squirreled away into a frameworked is one of those engineering decisions based on trade-offs. Even when the scope of the problem is well known so that a framework is an obvious non-constraining, yes-extensibility decision, some costs are so low that it’s better to just got on with the coding and do a little copy and paste. We can’t write programs to write all our programs for us. Not until the singularity, anyway.)

All together

When we put this all together we get an extensible submenu system that reduces the coding of new pages and subpages to the heart of the matter—specifying data and writing the middle of the view (retrieve content data structures)—while leaving the tedious submenu data structure generation and activation to already written helper methods.

Some pseudocode and diagrams would probably make this clearer, but for now I hope this overview is still mildly informative.

For those of you starving for more details, I’ve appended some code snippets to the bottom of this post. You might be particularly interested in the non-trivial closure in the activation function.

Proudly ProcrasDonating,
Lucy.

activate_substate_menu_items: function(request, current_substate, enums, inserts, processors) {                 var self = this;                 _iterate(enums, function(key, substate, index) {                         if (processors) {                                 request.jQuery("#substate_tab_"+substate+", .substate_tab_"+substate).click(                                         self._process(current_substate, enums, processors, inserts[index], request)                                 );                         } else {                                 request.jQuery("#substate_tab_"+substate+", .substate_tab_"+substate).click(                                         self._proceed(inserts[index], request)                                 );                         }                 });         },                 _process: function(current_substate, enums, processors, event, request) {                 var self = this;                 return function() {                         _iterate(enums, function(key, substate, index) {                                 if (substate == current_substate) {                                         var success = self[processors[index]](request, event);                                         if (success) {                                                 self[event](request);                                         }                                 }                         });                 };         },                 /// necessary because when did direct closure         /// (function(event) { return event; })(self[event])         // .click(         //     (function(event) { return event; })(self[event])         // )         /// called functions had incorrect this         _proceed: function(event, request) {                 var self = this;                 return function() {                         //self[fnname].apply(self, args);                         self[event](request);                 };         },                 /*          * @param: images: may be undefined          * @return: dictionary of:          *              menu_items: list of (id, klasses, value) tuples for substate menu          *          id is "substate_tab_"+enums[index]          *          value is tab_names[index]          *          klasses is ["substate_tab", "current_tab"]<---if current tab          *      next: tuple of next tab or null          *      prev: tuple of prev tab or null          */         make_substate_menu_items: function(current_substate, enums, tab_names, images) {                 var menu_items = [];                 var prev = null;                 var next = null;                                 var _last = null;                 var _one_past_current = false;                 var _two_past_current = false; // because can't tell the difference bw nulls                 _iterate(tab_names, function(key, value, index) {                         // figure out menu item                         var klasses = ["substate_tab"];                         if (enums[index] == current_substate) {                                 klasses.push("current_tab");                         } else if (!_one_past_current) {                                 klasses.push("past");                         } else {                                 klasses.push("future");                         }                                                 var img = "";                         var bar = "";                         if (enums[index] == current_substate) {                                 if (images) {                                         if (images[index].selected) {                                                 img = constants.MEDIA_URL+"img/"+images[index].selected;                                         } else {                                                 img = constants.MEDIA_URL+"img/"+images[index].past;                                         }                                 } else {                                         img = constants.MEDIA_URL+"img/StepCircle"+(index+1)+"Done.png";                                 }                                 bar = constants.MEDIA_URL+"img/Dash.png";                                                         } else if (!_one_past_current) {                                 if (images) {                                         img = constants.MEDIA_URL+"img/"+images[index].past;                                 } else {                                         img = constants.MEDIA_URL+"img/StepCircle"+(index+1)+"Done.png";                                 }                                 bar = constants.MEDIA_URL+"img/Dash.png";                                                         } else {                                 if (images) {                                         if (images[index].future) {                                                 img = constants.MEDIA_URL+"img/"+images[index].future;                                         } else {                                                 img = constants.MEDIA_URL+"img/"+images[index].past;                                         }                                 } else {                                         img = constants.MEDIA_URL+"img/StepCircle"+(index+1)+".png";                                 }                                 bar = constants.MEDIA_URL+"img/DashGreen.png";                         }                                                         var menu_item = {                                 id: "substate_tab_"+enums[index],                                 klasses: klasses,                                 value: value,                                 img: img,                                 bar: bar                         }                         // set next                         if (_one_past_current && !_two_past_current) {                                 next = menu_item;                                 _two_past_current = true                         }                         // set prev                         if (enums[index] == current_substate) {                                 _one_past_current = true;                                 prev = _last;                         }                         // add to menu items                         if (value != "XXX" && value != "XXXX") {                                 menu_items.push(menu_item);                         }                         _last = menu_item;                 });                 return {                         menu_items: menu_items,                         next: next,                         prev: prev                 }         },

No comments yet »

Your comment

HTML-Tags:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>