-
229 |
230 |
├── api ├── deleteContact.json.txt ├── getContact.json.txt ├── saveContact.json.txt └── getContacts.json.txt ├── linked ├── css │ ├── images │ │ ├── site_mvc_breakdown.jpg │ │ ├── site_cormvc_billboard.jpg │ │ ├── site_footer_background.jpg │ │ └── site_header_background.jpg │ ├── standard.css │ ├── reset.css │ ├── content.css │ ├── data_form.css │ ├── contacts.css │ └── structure.css └── js │ ├── model │ ├── contact.js │ ├── contact_service_mock.js │ └── contact_service.js │ ├── controller │ ├── contacts.js │ └── content.js │ ├── view │ ├── contact_form.js │ └── contact_list.js │ └── app │ └── corMVC.js ├── ie_back_button.htm ├── LICENSE.md ├── README.md └── index.htm /api/deleteContact.json.txt: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "errors": [], 4 | "data": 1 5 | } -------------------------------------------------------------------------------- /linked/css/images/site_mvc_breakdown.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bennadel/CorMVC/HEAD/linked/css/images/site_mvc_breakdown.jpg -------------------------------------------------------------------------------- /linked/css/images/site_cormvc_billboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bennadel/CorMVC/HEAD/linked/css/images/site_cormvc_billboard.jpg -------------------------------------------------------------------------------- /linked/css/images/site_footer_background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bennadel/CorMVC/HEAD/linked/css/images/site_footer_background.jpg -------------------------------------------------------------------------------- /linked/css/images/site_header_background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bennadel/CorMVC/HEAD/linked/css/images/site_header_background.jpg -------------------------------------------------------------------------------- /api/getContact.json.txt: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "errors": [], 4 | "data": { "id": 1, "name": "Ben Nadel", "email": "ben@corMVC.com", "phone": "212-555-1111" } 5 | } -------------------------------------------------------------------------------- /api/saveContact.json.txt: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "errors": [], 4 | "data": { "id": 1, "name": "Ben Nadel", "email": "ben@corMVC.com", "phone": "212-555-1111" } 5 | } -------------------------------------------------------------------------------- /linked/css/standard.css: -------------------------------------------------------------------------------- 1 | 2 | @import url( "./reset.css" ); 3 | @import url( "./content.css" ); 4 | @import url( "./structure.css" ); 5 | @import url( "./data_form.css" ); 6 | @import url( "./contacts.css" ); 7 | -------------------------------------------------------------------------------- /linked/css/reset.css: -------------------------------------------------------------------------------- 1 | 2 | h1, 3 | h2, 4 | h3, 5 | h4, 6 | h5, 7 | p, 8 | ul, 9 | ol, 10 | li, 11 | table { 12 | line-height: 1.6em ; 13 | margin: 0px 0px 1.6em 0px ; 14 | padding: 0px 0px 0px 0px ; 15 | } 16 | 17 | form { 18 | margin: 0px 0px 0px 0px ; 19 | padding: 0px 0px 0px 0px ; 20 | } 21 | 22 | -------------------------------------------------------------------------------- /ie_back_button.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /linked/css/content.css: -------------------------------------------------------------------------------- 1 | 2 | /* From jQuery UI CSS Framework. */ 3 | .clear:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } 4 | .clear { display: inline-block; } 5 | /* required comment for clearfix to work in Opera \*/ 6 | * html .clear { height:1%; } 7 | .clear { display:block; } 8 | 9 | h1, 10 | h2, 11 | h3, 12 | h4, 13 | h5 { 14 | font-family: arial, helvetica ; 15 | font-weight: 400 ; 16 | } 17 | 18 | h1 { 19 | font-size: 180% ; 20 | margin-bottom: .5em ; 21 | } 22 | 23 | h2 { 24 | font-size: 150% ; 25 | margin-bottom: .5em ; 26 | } 27 | 28 | ul, 29 | ol { 30 | margin-left: 30px ; 31 | } 32 | 33 | li { 34 | margin-bottom: 5px ; 35 | } 36 | 37 | a { 38 | color: #9E0B0F ; 39 | } -------------------------------------------------------------------------------- /linked/js/model/contact.js: -------------------------------------------------------------------------------- 1 | 2 | // I represent a contact object. 3 | 4 | // Add model to the application. 5 | window.application.addModel((function( $, application ){ 6 | 7 | // I am the contact class. 8 | function Contact( id, name, phone, email ){ 9 | this.id = (id || 0); 10 | this.name = (name || ""); 11 | this.phone = (phone || ""); 12 | this.email = (email || ""); 13 | }; 14 | 15 | // I validate the contact instance. 16 | Contact.prototype.validate = function(){ 17 | // Not using this right now. 18 | return( [] ); 19 | }; 20 | 21 | 22 | // ----------------------------------------------------------------------- // 23 | // ----------------------------------------------------------------------- // 24 | 25 | // Return a new model class. 26 | return( Contact ); 27 | 28 | })( jQuery, window.application )); 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | # The MIT License (MIT) 3 | 4 | Copyright (c) 2013 [Ben Nadel][1] 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | [1]: http://www.bennadel.com -------------------------------------------------------------------------------- /linked/css/data_form.css: -------------------------------------------------------------------------------- 1 | 2 | div.form-errors { 3 | background-color: #FEF1F1 ; 4 | border: 1px dotted #9E0B0F ; 5 | color: #9E0B0F ; 6 | margin-bottom: 1.5em ; 7 | padding: 10px 10px 10px 10px ; 8 | } 9 | 10 | div.form-errors h3 { 11 | font-size: 100% ; 12 | font-weight: bold ; 13 | margin-bottom: .6em ; 14 | } 15 | 16 | div.form-errors ul { 17 | margin-bottom: 0px ; 18 | } 19 | 20 | div.form-errors li { 21 | margin-bottom: 3px ; 22 | } 23 | 24 | input.branded-input { 25 | border: 1px solid #333333 ; 26 | border-bottom-color: #999999 ; 27 | border-right-color: #999999 ; 28 | font-size: 120% ; 29 | padding: 3px 6px 3px 6px ; 30 | } 31 | 32 | input.large { 33 | width: 300px ; 34 | } 35 | 36 | input.xlarge { 37 | width: 600px ; 38 | } 39 | 40 | form.data-form { 41 | margin-bottom: 1.5em ; 42 | } 43 | 44 | form.data-form fieldset { 45 | border-width: 0px 0px 0px 0px ; 46 | margin: 0px 0px 0px 0px ; 47 | padding: 0px 0px 0px 0px ; 48 | } 49 | 50 | form.data-form div.entry { 51 | margin-bottom: 15px ; 52 | } 53 | 54 | form.data-form div.entry label { 55 | color: #666666 ; 56 | display: block ; 57 | font-weight: bold ; 58 | margin-bottom: 2px ; 59 | } -------------------------------------------------------------------------------- /api/getContacts.json.txt: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "errors": [], 4 | "data": [ 5 | { "id": 1, "name": "Ben Nadel", "phone": "212-555-1234", "email": "ben.nadel@corMVC.com" }, 6 | { "id": 2, "name": "Rey Bango", "phone": "212-555-2345", "email": "rey.bango@corMVC.com" }, 7 | { "id": 3, "name": "Cody Lindley", "phone": "212-555-0393", "email": "cody.lindley@corMVC.com" }, 8 | { "id": 4, "name": "Scott Gonzalez", "phone": "212-555-0908", "email": "scott.gonzalez@corMVC.com" }, 9 | { "id": 5, "name": "Rebecca Murphey", "phone": "212-555-1433", "email": "rebecca.murphey@corMVC.com" }, 10 | { "id": 6, "name": "Adam Sontag", "phone": "212-555-3948", "email": "adam.sontag@corMVC.com" }, 11 | { "id": 7, "name": "Karl Swedberg", "phone": "212-555-9983", "email": "karl.swedberg@corMVC.com" }, 12 | { "id": 8, "name": "Dan Wellman", "phone": "212-555-3434", "email": "dan.wellman@corMVC.com" }, 13 | { "id": 9, "name": "John Resig", "phone": "212-555-7890", "email": "john.resig@corMVC.com" }, 14 | { "id": 10, "name": "Paul Irish", "phone": "212-555-4311", "email": "paul.irish@corMVC.com" }, 15 | { "id": 11, "name": "Ben Alman", "phone": "212-555-7829", "email": "ben.alman@corMVC.com" }, 16 | { "id": 12, "name": "Bob Bonifield", "phone": "212-555-1248", "email": "bob.bonifield@corMVC.com" }, 17 | { "id": 13, "name": "Vlad Filippov", "phone": "212-555-8282", "email": "vlad.filippov@corMVC.com" }, 18 | { "id": 14, "name": "Jon Snook", "phone": "212-555-7894", "email": "jon.snook@corMVC.com" }, 19 | { "id": 15, "name": "Jon Clark", "phone": "212-555-4561", "email": "jon.clark@corMVC.com" }, 20 | { "id": 16, "name": "Elijah Manor", "phone": "212-555-4566", "email": "elijah.manor@corMVC.com" }, 21 | { "id": 17, "name": "James Padolsey", "phone": "212-555-1591", "email": "james.padolsey@corMVC.com" }, 22 | { "id": 18, "name": "Alex Sexton", "phone": "212-555-1236", "email": "alex.sexton@corMVC.com" } 23 | ] 24 | } -------------------------------------------------------------------------------- /linked/css/contacts.css: -------------------------------------------------------------------------------- 1 | 2 | #contact-list-header { 3 | background-color: #2B1812 ; 4 | color: #F5BE44 ; 5 | font-size: 150% ; 6 | height: 50px ; 7 | line-height: 48px ; 8 | margin-bottom: 1em ; 9 | padding: 0px 20px 0px 20px ; 10 | } 11 | 12 | #contact-list-header form { 13 | float: left ; 14 | } 15 | 16 | #contact-list-header input { 17 | background-color: #482C21 ; 18 | border: 1px solid #663E2F ; 19 | color: #FFFFFF ; 20 | font-size: 100% ; 21 | margin-top: -2px ; 22 | padding: 0px 0px 1px 5px ; 23 | width: 440px ; 24 | } 25 | 26 | #contact-list-header a { 27 | color: #F5BE44 ; 28 | float: right ; 29 | text-decoration: none ; 30 | } 31 | 32 | #contact-list-header span.contrast { 33 | color: #FFFFFF ; 34 | } 35 | 36 | #contact-list { 37 | color: #333333 ; 38 | font-size: 110% ; 39 | list-style-type: none ; 40 | margin: 0px 0px 0px 0px ; 41 | padding: 0px 0px 0px 0px ; 42 | } 43 | 44 | #contact-list li { 45 | margin: 0px 0px 15px 0px ; 46 | padding: 0px 0px 0px 0px ; 47 | } 48 | 49 | #contact-list li.contact { 50 | background-color: #DBDAC1 ; 51 | padding: 7px 10px 7px 10px ; 52 | width: 712px ; 53 | } 54 | 55 | #contact-list li.last-contact { 56 | margin-bottom: 0px ; 57 | } 58 | 59 | #contact-list li.contact a { 60 | color: #333333 ; 61 | } 62 | 63 | #contact-list div.summary { 64 | float: left ; 65 | font-weight: bold ; 66 | } 67 | 68 | #contact-list div.summary a.name { 69 | text-decoration: none ; 70 | } 71 | 72 | #contact-list div.actions { 73 | float: right ; 74 | font-weight: bold ; 75 | } 76 | 77 | #contact-list dl.details { 78 | clear: both ; 79 | display: none ; 80 | margin: 0px 0px 0px 0px ; 81 | padding: 10px 0px 5px 0px ; 82 | width: 712px ; 83 | } 84 | 85 | #contact-list dl.details dt { 86 | clear: both ; 87 | color: #797746 ; 88 | float: left ; 89 | font-weight: bold ; 90 | margin: 0px 0px 0px 0px ; 91 | padding: 0px 0px 0px 0px ; 92 | text-align: right ; 93 | width: 70px ; 94 | } 95 | 96 | #contact-list dl.details dd { 97 | float: left ; 98 | margin: 0px 0px 0px 0px ; 99 | padding: 0px 0px 0px 10px ; 100 | width: 620px ; 101 | } 102 | 103 | #contact-form {} 104 | 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # CorMVC - Client-Side jQuery And JavaScript Framework 3 | 4 | by [Ben Nadel][1] (on [Google+][2]) 5 | 6 | __*NOTE: This is a really old project that has not been updated in years. I 7 | have personally started to use Google's AngularJS and highly recommend it.*__ 8 | 9 | __Demo:__ [View the online demo][4] 10 | 11 | CorMVC is a jQuery-powered Model-View-Controller (MVC) framework that can 12 | aide in the development of single-page jQuery applications. It evolved out of 13 | my recent presentation, [Building Single-Page Applications Using jQuery And ColdFusion][3], 14 | and will continue to evolve as I think more deeply about this type of 15 | application architecture. 16 | 17 | CorMVC stands for: Client-Only-Required Model-View-Controller, and is my 18 | laboratory experiment in application architecture held completely seperate 19 | from server-side technologies. 20 | 21 | ## CorMVC Philosophy 22 | 23 | Building frameworks in jQuery (or any other language for that matter) is very 24 | new to me; I don't claim to be any good at it. In fact, when I started looking 25 | into jQuery-based frameworks, I had no intention of creating my own. As I 26 | started to do my research, however, I quickly encountered two major problems 27 | with what was avilable: 28 | 29 | * Most examples were so small that I could not see how they might be applied 30 | to the kind of software I build. 31 | * Most frameworks were enormous and required command line utilities and some 32 | additional server-side technology (like Ruby On Rails) just to experiment with. 33 | 34 | I didn't even know how to begin learning. So, rather than wade through what was 35 | available, I decided to try and create something from scratch. What I came up 36 | with is corMVC. The philosophies that I put into the corMVC framework are those 37 | that were hopefully a remedy to the problems I encoutered above: 38 | 39 | * __A large sample application__. This whole demo site (including the contacts 40 | section) runs off of corMVC as a single-page application. 41 | * __No server required__. This demo application does not require any additional 42 | server-side technologies. If you have a web browser, you can download and run 43 | this application immediately. 44 | * __No building required__. This framework does not require you to build the 45 | application using scaffolding or any other command-line executables. You just 46 | download it and open it up in a browser. 47 | * __Small Framework__. This framework is very small (and excessively 48 | commented). It doesn't do anything more than it is supposed to. 49 | 50 | While I want to keep the corMVC framework as small as possible, I am sure that 51 | as I begin to more fully understand the various needs of single-page 52 | applications, the framework will have to evolve as necessary. In the end 53 | though, I want the corMVC framework to be an aide and not a constraint- 54 | affording the programmer the freedom to pile their own jQuery magic on top of 55 | this foundation. 56 | 57 | 58 | [1]: http://www.bennadel.com 59 | [2]: https://plus.google.com/108976367067760160494?rel=author 60 | [3]: http://www.bennadel.com/blog/1730-Building-Single-Page-Applications-Using-jQuery-And-ColdFusion-With-Ben-Nadel-Video-Presentation-.htm 61 | [4]: http://bennadel.github.io/CorMVC -------------------------------------------------------------------------------- /linked/js/controller/contacts.js: -------------------------------------------------------------------------------- 1 | 2 | // I control the events within the Contacts section of the application. 3 | 4 | // Add a controller to the application. 5 | window.application.addController((function( $, application ){ 6 | 7 | // I am the controller class. 8 | function Controller(){ 9 | // Route URL events to the controller's event handlers. 10 | this.route( "/contacts/", this.index ); 11 | this.route( "/contacts/add/", this.addContact ); 12 | this.route( "/contacts/edit/:id", this.editContact ); 13 | this.route( "/contacts/delete/:id", this.deleteContact ); 14 | 15 | // Set default properties. 16 | this.currentView = null; 17 | this.contactListView = null; 18 | this.contactFormView = null; 19 | }; 20 | 21 | // Extend the core application controller (REQUIRED). 22 | Controller.prototype = new application.Controller(); 23 | 24 | 25 | // I initialize the controller. I get called once the application starts 26 | // running (or when the controller is registered - if the application is 27 | // already running). At that point, the DOM is available and all the other 28 | // model and view classes will have been added to the system. 29 | Controller.prototype.init = function(){ 30 | this.contactListView = application.getView( "ContactList" ); 31 | this.contactFormView = application.getView( "ContactForm" ); 32 | }; 33 | 34 | 35 | // ----------------------------------------------------------------------- // 36 | // ----------------------------------------------------------------------- // 37 | 38 | 39 | // I am the add event for this controller. 40 | Controller.prototype.addContact = function( event ){ 41 | // Show the form view. 42 | this.showView( this.contactFormView, event ); 43 | }; 44 | 45 | 46 | // I am the edit event for this controller. 47 | Controller.prototype.editContact = function( event, id ){ 48 | // Show the form view. 49 | this.showView( this.contactFormView, event ); 50 | }; 51 | 52 | 53 | // I am the delete event for this controller. 54 | Controller.prototype.deleteContact = function( event, id ){ 55 | // Delete the contact. 56 | application.getModel( "ContactService" ).deleteContact( 57 | id, 58 | function(){ 59 | application.relocateTo( "contacts" ); 60 | } 61 | ); 62 | }; 63 | 64 | 65 | // I am the default event for this controller. 66 | Controller.prototype.index = function( event ){ 67 | // Show the list view. 68 | this.showView( this.contactListView, event ); 69 | }; 70 | 71 | 72 | // I show the given view; but first, I hide any existing view. 73 | Controller.prototype.showView = function( view, event ){ 74 | // Check to see if there is a current view. If so, then hide it. 75 | if (this.currentView && this.currentView.hideView){ 76 | this.currentView.hideView(); 77 | } 78 | 79 | // Show the given view. 80 | view.showView( event.parameters ); 81 | 82 | // Store the given view as the current view. 83 | this.currentView = view; 84 | }; 85 | 86 | 87 | // ----------------------------------------------------------------------- // 88 | // ----------------------------------------------------------------------- // 89 | 90 | // Return a new contoller singleton instance. 91 | return( new Controller() ); 92 | 93 | })( jQuery, window.application )); 94 | -------------------------------------------------------------------------------- /linked/css/structure.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | background-color: #1F1F1F ; 4 | font-family: verdana, arial ; 5 | font-size: 62.5% ; 6 | margin: 0px 0px 0px 0px ; 7 | padding: 0px 0px 0px 0px ; 8 | } 9 | 10 | 11 | #site-wrapper {} 12 | 13 | #site-header { 14 | background-color: #2B1812 ; 15 | background-image: url( "./images/site_header_background.jpg" ) ; 16 | background-position: left top ; 17 | background-repeat: repeat-x ; 18 | height: 471px ; 19 | overflow: hidden ; 20 | } 21 | 22 | #site-header-content { 23 | margin: 0px auto 0px auto ; 24 | width: 775px ; 25 | } 26 | 27 | #site-navigation { 28 | border-bottom: 4px solid #482C21 ; 29 | font-size: 14px ; 30 | height: 38px ; 31 | list-style-type: none ; 32 | margin: 0px 0px 8px 0px ; 33 | padding: 29px 0px 0px 0px ; 34 | } 35 | 36 | #site-navigation li { 37 | float: left ; 38 | height: 30px ; 39 | line-height: 30px ; 40 | margin: 0px 15px 0px 0px ; 41 | } 42 | 43 | #site-navigation a { 44 | color: #FFFFFF ; 45 | float: left ; 46 | padding: 0px 12px 0px 12px ; 47 | text-decoration: none ; 48 | text-transform: uppercase ; 49 | zoom: 1 ; 50 | } 51 | 52 | #site-navigation a:active, 53 | #site-navigation a:focus { 54 | outline-width: 0px 0px 0px 0px ; 55 | outline: none ; 56 | } 57 | 58 | #site-navigation a:hover { 59 | text-decoration: underline ; 60 | } 61 | 62 | #site-navigation li.bennadel { 63 | float: right ; 64 | margin-right: 0px ; 65 | } 66 | 67 | #site-navigation li.on a { 68 | background-color: #9E0B0F ; 69 | } 70 | 71 | #site-navigation li.on a:hover { 72 | text-decoration: none ; 73 | } 74 | 75 | #site-cormvc-billboard { 76 | background-color: #FFFFFF ; 77 | background-image: url( "./images/site_cormvc_billboard.jpg" ) ; 78 | background-position: center center ; 79 | background-repeat: no-repeat ; 80 | height: 260px ; 81 | overflow: hidden ; 82 | padding: 11px 11px 11px 11px ; 83 | text-indent: 9999px ; 84 | width: 753px ; 85 | white-space: nowrap ; 86 | } 87 | 88 | #site-mvc-breakdown { 89 | background-image: url( "./images/site_mvc_breakdown.jpg" ) ; 90 | background-position: center center ; 91 | background-repeat: no-repeat ; 92 | height: 110px ; 93 | overflow: hidden ; 94 | padding: 0px 11px 0px 11px ; 95 | text-indent: 9999px ; 96 | width: 753px ; 97 | white-space: nowrap ; 98 | } 99 | 100 | #site-body { 101 | background-color: #FFFFFF ; 102 | } 103 | 104 | #site-body-content { 105 | font-size: 1.2em ; 106 | margin: 0px auto 0px auto ; 107 | padding: 0px 0px 50px 0px ; 108 | width: 732px ; 109 | } 110 | 111 | #primary-content-stages { 112 | list-style-type: none ; 113 | margin: 0px 0px 0px 0px ; 114 | padding: 0px 0px 0px 0px ; 115 | } 116 | 117 | #primary-content-stages li.primary-content-stage { 118 | display: none ; 119 | margin: 0px 0px 0px 0px ; 120 | padding: 0px 0px 0px 0px ; 121 | } 122 | 123 | #primary-content-stages li.current-primary-content-stage { 124 | display: block ; 125 | } 126 | 127 | ul.primary-content-views { 128 | list-style-type: none ; 129 | margin: 0px 0px 0px 0px ; 130 | padding: 0px 0px 0px 0px ; 131 | } 132 | 133 | ul.primary-content-views li.primary-content-view { 134 | display: none ; 135 | margin: 0px 0px 0px 0px ; 136 | padding: 0px 0px 0px 0px ; 137 | } 138 | 139 | ul.primary-content-views li.current-primary-content-view { 140 | display: block ; 141 | } 142 | 143 | #site-footer { 144 | background-image: url( "./images/site_footer_background.jpg" ) ; 145 | background-position: left top ; 146 | background-repeat: repeat-x ; 147 | } 148 | 149 | #site-footer-content { 150 | color: #666666 ; 151 | font-size: 1.1em ; 152 | margin: 0px auto 0px auto ; 153 | padding: 15px 10px 100px 10px ; 154 | width: 755px ; 155 | } 156 | 157 | #site-footer a { 158 | color: #666666 ; 159 | } 160 | -------------------------------------------------------------------------------- /linked/js/controller/content.js: -------------------------------------------------------------------------------- 1 | 2 | // I control the primary navigation and the corresponding view of content that 3 | // is displayed on the page. I do not control the content that is displayed within 4 | // the primary content view (that is delegated to the other controllers). 5 | 6 | // Add a controller to the application. 7 | window.application.addController((function( $, application ){ 8 | 9 | // I am the controller class. 10 | function Controller(){ 11 | // Route URL events to the controller's event handlers. 12 | this.route( "/", this.index ); 13 | this.route( "/home", this.index ); 14 | this.route( "/contacts.*", this.contacts ); 15 | this.route( "/documentation", this.documentation ); 16 | this.route( "/resources", this.resources ); 17 | this.route( "/404", this.pageNotFound ); 18 | 19 | // Set default properties. 20 | this.navigation = null; 21 | this.navigationItems = null; 22 | this.views = null; 23 | this.homeView = null; 24 | this.contactsView = null; 25 | this.documentationView = null; 26 | this.resourcesView = null; 27 | this.pageNotFoundView = null; 28 | }; 29 | 30 | // Extend the core application controller (REQUIRED). 31 | Controller.prototype = new application.Controller(); 32 | 33 | 34 | // I initialize the controller. I get called once the application starts 35 | // running (or when the controller is registered - if the application is 36 | // already running). At that point, the DOM is available and all the other 37 | // model and view classes will have been added to the system. 38 | Controller.prototype.init = function(){ 39 | this.navigation = $( "#site-navigation" ); 40 | this.navigationItems = this.navigation.find( "> li:has(a[rel])" ); 41 | this.views = $( "#primary-content-stages > li" ); 42 | this.homeView = this.views.filter( "[ rel = 'home' ]" ); 43 | this.contactsView = this.views.filter( "[ rel = 'contacts' ]" ); 44 | this.documentationView = this.views.filter( "[ rel = 'documentation' ]" ); 45 | this.resourcesView = this.views.filter( "[ rel = 'resources' ]" ); 46 | this.pageNotFoundView = this.views.filter( "[ rel = '404' ]" ); 47 | 48 | // Update the copyright year. 49 | $( "#site-copyright span.copyright-year" ).text( 50 | (new Date()).getFullYear() 51 | ); 52 | }; 53 | 54 | 55 | // ----------------------------------------------------------------------- // 56 | // ----------------------------------------------------------------------- // 57 | 58 | 59 | // I show the contacts view. 60 | Controller.prototype.contacts = function( event ){ 61 | this.showView( this.contactsView ); 62 | }; 63 | 64 | 65 | // I show the documentation view. 66 | Controller.prototype.documentation = function( event ){ 67 | this.showView( this.documentationView ); 68 | }; 69 | 70 | 71 | // I am the default event for this controller. 72 | Controller.prototype.index = function( event ){ 73 | this.showView( this.homeView ); 74 | }; 75 | 76 | 77 | // I show the page not found view. 78 | Controller.prototype.pageNotFound = function( event ){ 79 | this.showView( this.pageNotFoundView ); 80 | }; 81 | 82 | 83 | // I show the resources view. 84 | Controller.prototype.resources = function( event ){ 85 | this.showView( this.resourcesView ); 86 | }; 87 | 88 | 89 | // I show the given view; but first, I hide any existing view. 90 | Controller.prototype.showView = function( view ){ 91 | // Turn off the primary navigation. 92 | this.navigationItems.removeClass( "on" ); 93 | 94 | // Remove the current view class. 95 | this.views.removeClass( "current-primary-content-stage" ); 96 | 97 | // Turn on the correct navigation. Match the REL attribute of the given 98 | // view to the REL attribute of the anchor inside the navigation item. 99 | if (view.attr( "rel" )){ 100 | this.navigationItems.filter( ":has(a[rel = '" + view.attr( "rel" ) + "' ])" ).addClass( "on" ); 101 | } 102 | 103 | // Add the primary content view class. 104 | view.addClass( "current-primary-content-stage" ); 105 | }; 106 | 107 | 108 | // ----------------------------------------------------------------------- // 109 | // ----------------------------------------------------------------------- // 110 | 111 | // Return a new contoller singleton instance. 112 | return( new Controller() ); 113 | 114 | })( jQuery, window.application )); 115 | -------------------------------------------------------------------------------- /linked/js/model/contact_service_mock.js: -------------------------------------------------------------------------------- 1 | 2 | // I am the gateway to the contacts collection within the system. For this 3 | // demo, there is communication with the "server", but only to invoke hard-coded 4 | // JSON files in attempt to show you how client-server communication might happen. 5 | 6 | // Add model to the application. 7 | window.application.addModel((function( $, application ){ 8 | 9 | // I am the contacts service class. 10 | function ContactService(){ 11 | // ... Nothing to do here ... 12 | }; 13 | 14 | 15 | // I initialize the model. I get called once the application starts 16 | // running (or when the model is registered - if the application is 17 | // already running). At that point, the DOM is available and all the other 18 | // model and view classes will have been added to the system. 19 | ContactService.prototype.init = function(){ 20 | // ... nothing needed here ... 21 | }; 22 | 23 | 24 | // ----------------------------------------------------------------------- // 25 | // ----------------------------------------------------------------------- // 26 | 27 | 28 | // I delete the contact at the given ID. 29 | ContactService.prototype.deleteContact = function( id, onSuccess, onError ){ 30 | var self = this; 31 | 32 | // Delete the contact on the server and return any error messages. 33 | application.ajax({ 34 | url: "api/deleteContact.json.txt", 35 | data: { 36 | method: "deleteContact", 37 | id: id 38 | }, 39 | normalizeJSON: true, 40 | success: function( response ){ 41 | // Check to see if the request was successful. 42 | if (response.success){ 43 | // The delete was successful - pass back the ID to the callback. 44 | onSuccess( id ); 45 | } else if (onError){ 46 | // The call was not successful - call the error function. 47 | onError( respsonse.errors ); 48 | } 49 | } 50 | }); 51 | }; 52 | 53 | 54 | // I get the contact at the given ID. 55 | ContactService.prototype.getContact = function( id, onSuccess, onError ){ 56 | var self = this; 57 | 58 | // Get the contacts from the server. 59 | application.ajax({ 60 | url: "api/getContact.json.txt", 61 | data: { 62 | method: "getContact", 63 | id: id 64 | }, 65 | normalizeJSON: true, 66 | success: function( response ){ 67 | // Check to see if the request was successful. 68 | if (response.success){ 69 | // Create contacts based on the response data and pass the contact 70 | // off to the callback. 71 | onSuccess( self.populateContactsFromResponse( response.data ) ); 72 | } else if (onError){ 73 | // The call was not successful - call the error function. 74 | onError( respsonse.errors ); 75 | } 76 | } 77 | }); 78 | }; 79 | 80 | 81 | // I get the contacts. 82 | ContactService.prototype.getContacts = function( onSuccess, onError ){ 83 | var self = this; 84 | 85 | // Get the contacts from the server. 86 | application.ajax({ 87 | url: "api/getContacts.json.txt", 88 | data: { 89 | method: "getContacts" 90 | }, 91 | normalizeJSON: true, 92 | success: function( response ){ 93 | // Check to see if the request was successful. 94 | if (response.success){ 95 | // Create contacts based on the response data and pass the contacts 96 | // collection off to the callback. 97 | onSuccess( self.populateContactsFromResponse( response.data ) ); 98 | } else if (onError){ 99 | // The call was not successful - call the error function. 100 | onError( respsonse.errors ); 101 | } 102 | } 103 | }); 104 | }; 105 | 106 | 107 | // I populate real contact objects based on the raw respons data. 108 | ContactService.prototype.populateContactsFromResponse = function( responseData ){ 109 | // Check to see if the given response object is an array. If so, 110 | // then we will populate an array and return it. If not, then we will 111 | // populate a single object and return it. 112 | if ($.isArray( responseData )){ 113 | 114 | // We are populating an array of contacts. 115 | var contacts = []; 116 | 117 | // Loop over each response item and conver it to a real contact. 118 | $.each( 119 | responseData, 120 | function( index, contactData ){ 121 | contacts.push( 122 | application.getModel( "Contact", [ contactData.id, contactData.name, contactData.phone, contactData.email ] ) 123 | ); 124 | } 125 | ); 126 | 127 | // Return the populated contacts collection. 128 | return( contacts ); 129 | 130 | } else { 131 | 132 | // We are populating a single contact. 133 | return( 134 | application.getModel( "Contact", [ responseData.id, responseData.name, responseData.phone, responseData.email ] ) 135 | ); 136 | 137 | } 138 | }; 139 | 140 | 141 | // I save the given contact. 142 | ContactService.prototype.saveContact = function( id, name, phone, email, onSuccess, onError ){ 143 | var self = this; 144 | 145 | // Save the contact on the server and return any error messages. 146 | application.ajax({ 147 | url: "api/saveContact.json.txt", 148 | data: { 149 | method: "saveContact", 150 | id: id, 151 | name: name, 152 | phone: phone, 153 | email: email 154 | }, 155 | normalizeJSON: true, 156 | success: function( response ){ 157 | // Check to see if the request was successful. 158 | if (response.success){ 159 | // The save was successful - return the saved contact ID. 160 | onSuccess( response.data ); 161 | } else { 162 | // The save was not successful - return the errors. 163 | onError( response.errors ); 164 | } 165 | } 166 | }); 167 | }; 168 | 169 | 170 | // ----------------------------------------------------------------------- // 171 | // ----------------------------------------------------------------------- // 172 | 173 | // Return a new model class singleton instance. 174 | return( new ContactService() ); 175 | 176 | })( jQuery, window.application )); 177 | -------------------------------------------------------------------------------- /linked/js/model/contact_service.js: -------------------------------------------------------------------------------- 1 | 2 | // I am the gateway to the contacts collection within the system. For this 3 | // demo, there is no communication with the server - all contacts are stored 4 | // locally and internally to this service object. 5 | 6 | // Add model to the application. 7 | window.application.addModel((function( $, application ){ 8 | 9 | // I am the contacts service class. 10 | function ContactService(){ 11 | this.contacts = []; 12 | this.contactsIndex = {}; 13 | this.uuid = 0; 14 | }; 15 | 16 | 17 | // I initialize the model. I get called once the application starts 18 | // running (or when the model is registered - if the application is 19 | // already running). At that point, the DOM is available and all the other 20 | // model and view classes will have been added to the system. 21 | ContactService.prototype.init = function(){ 22 | // ... nothing needed here ... 23 | }; 24 | 25 | 26 | // ----------------------------------------------------------------------- // 27 | // ----------------------------------------------------------------------- // 28 | 29 | 30 | // I delete the contact at the given ID. 31 | ContactService.prototype.deleteContact = function( id, onSuccess, onError ){ 32 | // Loop over the array looking for the given contact. 33 | for (var i = 0 ; i < this.contacts.length ; i++){ 34 | 35 | // Check to see if this is the target contact. 36 | if (this.contacts[ i ].id == id){ 37 | 38 | // Delete this contact from the collection. 39 | this.contacts.splice( i, 1 ); 40 | 41 | // Delete this contact from the index. 42 | this.contactsIndex[ id ] = null; 43 | 44 | // Break out of the loop. 45 | break; 46 | 47 | } 48 | 49 | } 50 | 51 | // Call the callback with the given id. 52 | onSuccess( id ); 53 | }; 54 | 55 | 56 | // I get the contact at the given ID. 57 | ContactService.prototype.getContact = function( id, onSuccess, onError ){ 58 | // Check to see if the contact exists. 59 | if (this.contactsIndex[ id ]){ 60 | 61 | // Return the given contact. 62 | onSuccess( this.contactsIndex[ id ] ); 63 | 64 | } else { 65 | 66 | // The contact could not be found - just return a new contact. 67 | onSuccess( 68 | application.getModel( "Contact", [ 0 ] ) 69 | ); 70 | 71 | } 72 | }; 73 | 74 | 75 | // I get the contacts. 76 | ContactService.prototype.getContacts = function( onSuccess, onError ){ 77 | // Return the contacts collection. 78 | onSuccess( this.contacts ); 79 | }; 80 | 81 | 82 | // I save the given contact. 83 | ContactService.prototype.saveContact = function( id, name, phone, email, onSuccess, onError ){ 84 | // Create an array to hold the validation errors. 85 | var errors = []; 86 | 87 | // Validate the name. 88 | if ($.trim( name ).length == 0){ 89 | 90 | // Add a new error. 91 | errors.push({ 92 | type: "MissingData", 93 | message: "[NAME] is required", 94 | parameter: "name" 95 | }); 96 | 97 | } 98 | 99 | // Valide the email. 100 | if ( 101 | email.length && 102 | !(new RegExp( "^[^@]+@([^.]+\\.)+[^.]{2,}$", "i" )).test( email ) 103 | ){ 104 | 105 | // Add a new error. 106 | errors.push({ 107 | type: "InvalidValue", 108 | message: "[EMAIL] is invalid", 109 | parameter: "email" 110 | }); 111 | 112 | } 113 | 114 | // Check to see if there any errors. 115 | if (errors.length){ 116 | 117 | // Return errors. 118 | onError( errors ); 119 | 120 | } else { 121 | 122 | // The contact validated, save it. 123 | 124 | // Check to see if this contact exists. 125 | if (this.contactsIndex[ id ]){ 126 | 127 | // Simply update the contact information. 128 | this.contactsIndex[ id ].name = name; 129 | this.contactsIndex[ id ].phone = phone; 130 | this.contactsIndex[ id ].email = email; 131 | 132 | } else { 133 | 134 | // Create a new contact. 135 | var contact = application.getModel( "Contact", [ ++this.uuid, name, phone, email ] ); 136 | 137 | // Add the contact to the collection. 138 | this.contacts.push( contact ); 139 | 140 | // Add the contact to the index. 141 | this.contactsIndex[ contact.id ] = contact; 142 | 143 | } 144 | 145 | // Sort the contacts collection. 146 | this.contacts.sort( 147 | function( a, b ){ 148 | if (a.name < b.name){ 149 | return( -1 ); 150 | } else { 151 | return( 1 ); 152 | } 153 | } 154 | ); 155 | 156 | // Return the saved contact id. 157 | onSuccess( id ); 158 | 159 | } 160 | }; 161 | 162 | 163 | // ----------------------------------------------------------------------- // 164 | // ----------------------------------------------------------------------- // 165 | 166 | // Return a new model class singleton instance. 167 | return( new ContactService() ); 168 | 169 | })( jQuery, window.application )); 170 | 171 | 172 | 173 | // ----------------------------------------------------------------------- // 174 | // FOR THE DEMO ONLY!!! // 175 | // ----------------------------------------------------------------------- // 176 | 177 | // When the DOM is ready, pre-populate some sample data. 178 | jQuery(function( $ ){ 179 | 180 | // Get the contact service. 181 | var contactService = window.application.getModel( "ContactService" ); 182 | 183 | var onSuccess = function(){}; 184 | 185 | // Add some test data to the database. 186 | contactService.saveContact( 0, "Ben Nadel", "212-555-1234", "ben.nadel@corMVC.com", onSuccess ); 187 | contactService.saveContact( 0, "Rey Bango", "212-555-2345", "rey.bango@corMVC.com", onSuccess ); 188 | contactService.saveContact( 0, "Cody Lindley", "212-555-0393", "cody.lindley@corMVC.com", onSuccess ); 189 | contactService.saveContact( 0, "Scott Gonzalez", "212-555-0908", "scott.gonzalez@corMVC.com", onSuccess ); 190 | contactService.saveContact( 0, "Rebecca Murphey", "212-555-1433", "rebecca.murphey@corMVC.com", onSuccess ); 191 | contactService.saveContact( 0, "Adam Sontag", "212-555-3948", "adam.sontag@corMVC.com", onSuccess ); 192 | contactService.saveContact( 0, "Karl Swedberg", "212-555-9983", "karl.swedberg@corMVC.com", onSuccess ); 193 | contactService.saveContact( 0, "Dan Wellman", "212-555-3434", "dan.wellman@corMVC.com", onSuccess ); 194 | contactService.saveContact( 0, "John Resig", "212-555-7890", "john.resig@corMVC.com", onSuccess ); 195 | contactService.saveContact( 0, "Paul Irish", "212-555-4311", "paul.irish@corMVC.com", onSuccess ); 196 | contactService.saveContact( 0, "Ben Alman", "212-555-7829", "ben.alman@corMVC.com", onSuccess ); 197 | contactService.saveContact( 0, "Bob Bonifield", "212-555-1248", "bob.bonifield@corMVC.com", onSuccess ); 198 | contactService.saveContact( 0, "Vlad Filippov", "212-555-8282", "vlad.filippov@corMVC.com", onSuccess ); 199 | contactService.saveContact( 0, "Jon Snook", "212-555-7894", "jon.snook@corMVC.com", onSuccess ); 200 | contactService.saveContact( 0, "Jon Clark", "212-555-4561", "jon.clark@corMVC.com", onSuccess ); 201 | contactService.saveContact( 0, "Elijah Manor", "212-555-4566", "elijah.manor@corMVC.com", onSuccess ); 202 | contactService.saveContact( 0, "James Padolsey", "212-555-1591", "james.padolsey@corMVC.com", onSuccess ); 203 | contactService.saveContact( 0, "Alex Sexton", "212-555-1236", "alex.sexton@corMVC.com", onSuccess ); 204 | 205 | }); 206 | -------------------------------------------------------------------------------- /linked/js/view/contact_form.js: -------------------------------------------------------------------------------- 1 | 2 | // I am the view helper for the Contact Add / Edit form. I bind the 3 | // appropriate event handlers and translate the UI events into actions 4 | // within the application. 5 | 6 | // Add view to the application. 7 | window.application.addView((function( $, application ){ 8 | 9 | // I am the contact form view class. 10 | function ContactForm(){ 11 | this.view = null; 12 | this.backLink = null; 13 | this.form = null; 14 | this.errors = null; 15 | this.fields = { 16 | id: null, 17 | name: null, 18 | phone: null, 19 | email: null 20 | }; 21 | this.cancelLink = null; 22 | }; 23 | 24 | 25 | // I initialize the view. I get called once the application starts 26 | // running (or when the view is registered - if the application is 27 | // already running). At that point, the DOM is available and all the other 28 | // model and view classes will have been added to the system. 29 | ContactForm.prototype.init = function(){ 30 | var self = this; 31 | 32 | // Initialize properties. 33 | this.view = $( "#contact-edit-view" ); 34 | this.form = $( "#contact-form" ); 35 | this.errors = this.form.find( "div.form-errors" ); 36 | this.fields.id = this.form.find( ":input[ name = 'id' ]" ); 37 | this.fields.name = this.form.find( ":input[ name = 'name' ]" ); 38 | this.fields.phone = this.form.find( ":input[ name = 'phone' ]" ); 39 | this.fields.email = this.form.find( ":input[ name = 'email' ]" ); 40 | this.cancelLink = this.form.find( "div.actions a.cancel" ); 41 | 42 | // Bind the submit handler. 43 | this.form.submit( 44 | function( event ){ 45 | // Submit the form. 46 | self.submitForm(); 47 | 48 | // Cancel default event. 49 | return( false ); 50 | } 51 | ); 52 | 53 | // Bind the cancel link. 54 | this.cancelLink.click( 55 | function( event ){ 56 | // Confirm the cancel link. 57 | return( confirm( "Are you sure you want to cancel?" ) ); 58 | } 59 | ); 60 | }; 61 | 62 | 63 | // ----------------------------------------------------------------------- // 64 | // ----------------------------------------------------------------------- // 65 | 66 | 67 | // I apply the given submission errors to the form. This involves translating the 68 | // paramters-based errors into user-friendly errors messages. 69 | ContactForm.prototype.applyErrors = function( errors ){ 70 | var self = this; 71 | 72 | // Clear any existing errors. 73 | this.clearErrors(); 74 | 75 | // Get the list of error messages. 76 | var errorList = this.errors.find( "> ul" ); 77 | 78 | // Loop over the errors to translate them. 79 | $.each( 80 | errors, 81 | function( index, error ){ 82 | var message = ""; 83 | 84 | // Check to see which type of error this is. 85 | switch (error.parameter){ 86 | case "name": 87 | message = "Please enter a name."; 88 | break; 89 | case "phone": 90 | message = "Please enter a valid phone number."; 91 | break; 92 | case "email": 93 | message = "Please enter a valid email."; 94 | break; 95 | default: 96 | message = ("An unknown error occurred: " + error.message + "."); 97 | break; 98 | }; 99 | 100 | // Add the error to the form. 101 | errorList.append( "98 | CorMVC is a jQuery-powered Model-View-Controller (MVC) framework that 99 | can aide in the development of single-page jQuery applications. It evolved out of 100 | my recent presentation, 101 | Building Single-Page Applications Using jQuery And ColdFusion, 102 | and will continue to evolve as I think more deeply about this type of application 103 | architecture. 104 |
105 | 106 |107 | CorMVC stands for: 108 | Client-Only-Required 109 | Model-View-Controller, 110 | and is my laboratory experiment in application architecture held completely 111 | seperate from server-side technologies. 112 |
113 | 114 |119 | Building frameworks in jQuery (or any other language for that matter) is 120 | very new to me; I don't claim to be any good at it. In fact, when I started 121 | looking into jQuery-based frameworks, I had no intention of creating my own. 122 | As I started to do my research, however, I quickly encountered two major problems 123 | with what was avilable: 124 |
125 | 126 |138 | I didn't even know how to begin learning. So, rather than wade through what was 139 | available, I decided to try and create something from scratch. What I came up with 140 | is corMVC. The philosophies that I put into the corMVC framework 141 | are those that were hopefully a remedy to the problems I encoutered above: 142 |
143 | 144 |166 | While I want to keep the corMVC framework as small as possible, I am sure that as I begin 167 | to more fully understand the various needs of single-page applications, the framework 168 | will have to evolve as necessary. In the end though, I want the corMVC framework to be 169 | an aide and not a constraint- affording the programmer the freedom to pile their 170 | own jQuery magic on top of this foundation. 171 |
172 | 173 |178 | When dealing with Javascript and "progressive enhancement enthusiast," someone always asks 179 | how this should work / degrade if the end user does not have Javascript enabled. 180 | While this might be a contentious answer, if your target user does not have Javascript 181 | enabled, then I don't think they should be using a single-page application. That's like 182 | asking how a user should navigate a FLEX application if they don't have Flash installed; 183 | it doesn't make sense. I'm not talking about building "pamphlet" sites here - 184 | I'm talking about building complex, user-experience-oriented applications. 185 |
186 | 187 | 188 |207 | The contacts section is meant to demonstrate more complex interactions that 208 | involve the application's service layer (part of the Model) and potentially 209 | (thought not required in this demo) communication with the server. 210 |
211 | 212 | 213 |289 | Please fill out the contact information below. 290 |
291 | 292 | 339 | 340 | 341 |360 | This is the documentation page. 361 |
362 | 363 |364 | Coming soon.... 365 |
366 | 367 | 368 |380 | This is the resources page. 381 |
382 | 383 |384 | Coming soon.... 385 |
386 | 387 | 388 |400 | Sorry, but the page you requested could not be found. 401 |
402 | 403 |404 | Try going back to the home page. 405 |
406 | 407 | 408 |" + value.toString() + "
" ); 343 | } 344 | ); 345 | 346 | } 347 | }; 348 | 349 | 350 | // I normalize a hash value for comparison. 351 | Application.prototype.normalizeHash = function( hash ){ 352 | // Strip off front hash and slashses as well as trailing slash. This will 353 | // convert hash values like "#/section/" into "section". 354 | return( 355 | hash.replace( new RegExp( "^[#/]+|/$", "g" ), "" ) 356 | ); 357 | }; 358 | 359 | 360 | // I normalize a JSON response from an AJAX call. This is because some languages 361 | // (such as ColdFusion) are not case sensitive and do not have proper casing 362 | // on their JSON translations. I will lowercase all keys. 363 | Application.prototype.normalizeJSON = function( object ){ 364 | var self = this; 365 | 366 | // Check to see if this is an object that can be normalized. 367 | if ( 368 | (typeof( object ) == "boolean") || 369 | (typeof( object ) == "string") || 370 | (typeof( object ) == "number") || 371 | $.isFunction( object ) 372 | ){ 373 | 374 | // This is a non-object, just return it's value. 375 | return( object ); 376 | } 377 | 378 | // Check to see if this is an array. 379 | if ($.isArray( object )){ 380 | 381 | // Create an array into which the normalized data will be stored. 382 | var normalizedObject = []; 383 | 384 | // Loop over the array value and moralize it's value. 385 | $.each( 386 | object, 387 | function( index, value ){ 388 | normalizedObject[ index ] = self.normalizeJSON( value ); 389 | } 390 | ); 391 | 392 | } else { 393 | 394 | // Create an object into which the normalized data will be stored. 395 | var normalizedObject = {}; 396 | 397 | // Loop over the object key and moralize it's key and value. 398 | $.each( 399 | object, 400 | function( key, value ){ 401 | normalizedObject[ key.toLowerCase() ] = self.normalizeJSON( value ); 402 | } 403 | ); 404 | 405 | } 406 | 407 | // Return the normalized object. 408 | return( normalizedObject ); 409 | }; 410 | 411 | 412 | // I handle the location changes. 413 | Application.prototype.onLocationChange = function( locationChangeEvent ){ 414 | var self = this; 415 | 416 | // I am used to determine if the application should continue routing the request. 417 | // Depending on the return value of a given event handler, the current routing 418 | // can be cancelled. 419 | var keepRouting = true; 420 | 421 | // I am used to determine if a route was found for the given even. If not, a 422 | // 404 - page not found route will be executed. 423 | var routeFound = false; 424 | 425 | // Turn off monitoring while we route the location. We are doing this 426 | // to allow the application time to process the route without being 427 | // interupted. This will prevent someone clicking rappidly around the 428 | // application from causing unexpected effects. 429 | this.stopLocationMonitor(); 430 | 431 | // Iterate over the route mappings. 432 | $.each( 433 | this.routeMappings, 434 | function( index, mapping ){ 435 | var matches = null; 436 | 437 | // Check to see if routing has been cancelled. 438 | if (!keepRouting){ 439 | return; 440 | } 441 | 442 | // Define the default event context. 443 | var eventContext = { 444 | application: self, 445 | fromLocation: locationChangeEvent.fromLocation, 446 | toLocation: locationChangeEvent.toLocation, 447 | parameters: $.extend( {}, locationChangeEvent.parameters ) 448 | }; 449 | 450 | // Get the matches from the location (if the route mapping does 451 | // not match, this will return null) and check to see if this route 452 | // mapping applies to the current location (if no matches are returned, 453 | // matches array will be null). 454 | if (matches = locationChangeEvent.toLocation.match( mapping.test )){ 455 | 456 | // The route mapping will handle this location change. Now, we 457 | // need to prepare the event context and invoke the route handler. 458 | 459 | // Remove the first array (the entire location match). This is 460 | // irrelevant information. What we want are the captured groups 461 | // which will be in the subsequent indices. 462 | matches.shift(); 463 | 464 | // Map the captured group matches to the ordered parameters defined 465 | // in the route mapping. 466 | $.each( 467 | matches, 468 | function( index, value ){ 469 | eventContext.parameters[ mapping.parameters[ index ] ] = value; 470 | } 471 | ); 472 | 473 | // Check to see if this controller has a pre-handler. 474 | if (mapping.controller.preHandler){ 475 | // Execute the pre-handler. 476 | mapping.controller.preHandler( eventContext ); 477 | } 478 | 479 | // Execute the handler in the context of the controller. Even though the 480 | // event parameteres are getting passed as part of the event context, I 481 | // am going to pass them through as part of the argument collection for 482 | // conveinence in the handler's method signature. 483 | var result = mapping.handler.apply( 484 | mapping.controller, 485 | [ eventContext ].concat( matches ) 486 | ); 487 | 488 | // Check to see if this controller has a post-handler. 489 | if (mapping.controller.postHandler){ 490 | // Execute the post-handler. 491 | mapping.controller.postHandler( eventContext ); 492 | } 493 | 494 | // Check the controller handler's result value to see if we need to stop 495 | // routing. If the controller returns false, we are going to stop routing. 496 | if ( 497 | (typeof( result ) == "boolean") && 498 | !result 499 | ){ 500 | // Cancel routing. 501 | keepRouting = false; 502 | } 503 | 504 | // Flag that a route was found. 505 | routeFound = true; 506 | } 507 | } 508 | ); 509 | 510 | // Turn monitoring back on now that the routing has completed. 511 | this.startLocationMonitor(); 512 | 513 | // Check to see if a route was found. If not, then we need to trigger a 404 event. 514 | if (!routeFound){ 515 | 516 | // Trigger a 404 location. 517 | // NOTE: This will currently break the ability to use the back button on the 518 | // browser (since it will keep trying to relocate to the 404 on back). Will need 519 | // to rethink the way this is handled later. 520 | this.relocateTo( "404" ); 521 | 522 | } 523 | }; 524 | 525 | 526 | // I create a proxy for the callback so that given callback executes in the 527 | // context of the application object, overriding any context provided by the 528 | // calling context. 529 | Application.prototype.proxyCallback = function( callback ){ 530 | var self = this; 531 | 532 | // Return a proxy that will apply the callback in the THIS context. 533 | return( 534 | function(){ 535 | return( callback.apply( self, arguments ) ); 536 | } 537 | ); 538 | } 539 | 540 | 541 | // I relocate the application to the given location. You can also pass through 542 | // a hash of any additional parameters that you wanted added to the location 543 | // event that gets triggers. 544 | Application.prototype.relocateTo = function( newLocation, parameters ){ 545 | this.setLocation( newLocation, parameters ); 546 | }; 547 | 548 | 549 | // I start the application. 550 | Application.prototype.run = function(){ 551 | // Initialize the model. 552 | this.initModels(); 553 | 554 | // Initialize the views. 555 | this.initViews(); 556 | 557 | // Initialize the controllers. 558 | this.initControllers(); 559 | 560 | // Initialize the location monitor. 561 | this.initLocationMonitor(); 562 | 563 | // Turn on location monitor. 564 | this.startLocationMonitor(); 565 | 566 | // Flag that the application is running. 567 | this.isRunning = true; 568 | }; 569 | 570 | 571 | // I set the location of the application. I don't do anything explicitly - 572 | // I let the location monitoring handle this change implicitly. You can also 573 | // pass through additional parameters that will be added to the location event 574 | // that gets triggered. 575 | Application.prototype.setLocation = function( location, parameters ){ 576 | // Clearn the location. 577 | location = this.normalizeHash( location ); 578 | 579 | // Create variables to hold the new and old hashes. 580 | var oldLocation = this.currentLocation; 581 | var newLocation = location; 582 | 583 | // Store the new location. 584 | this.currentLocation = location; 585 | 586 | // Make sure the hash is the same as the location (this way, we don't 587 | // get circular logic as the monitor keeps pinging the hash). 588 | window.location.hash = ("#/" + location ); 589 | 590 | // The location has changed - trigger the change event on the application 591 | // object so that anyone monitoring it can react. 592 | $( this ).trigger({ 593 | type: "locationchange", 594 | fromLocation: oldLocation, 595 | toLocation: newLocation, 596 | parameters: parameters 597 | }); 598 | }; 599 | 600 | 601 | // I turn on the location monitoring. 602 | Application.prototype.startLocationMonitor = function(){ 603 | var self = this; 604 | 605 | // Create the interval function. 606 | this.locationMonitorInterval = setInterval( 607 | function(){ 608 | self.checkLocationForChange(); 609 | }, 610 | this.locationMonitorDelay 611 | ); 612 | }; 613 | 614 | 615 | // I turn off the location monitoring. 616 | Application.prototype.stopLocationMonitor = function(){ 617 | clearInterval( this.locationMonitorInterval ); 618 | }; 619 | 620 | 621 | // ----------------------------------------------------------------------- // 622 | // ----------------------------------------------------------------------- // 623 | 624 | 625 | // I am the prototype for the application controllers. This is so they 626 | // can leverage some binding magic without the overhead of the implimentation. 627 | Application.prototype.Controller = function(){ 628 | // ... 629 | }; 630 | 631 | 632 | // I am the prototype for the Controller prototype. 633 | Application.prototype.Controller.prototype = { 634 | 635 | // I route the given pseudo location to the given controller method. 636 | route: function( path, handler ){ 637 | // Strip of any trailing and leading slashes. 638 | path = application.normalizeHash( path ); 639 | 640 | // We will need to extract the parameters into an array - these will be used 641 | // to create the event object when the location changes get routed. 642 | var parameters = []; 643 | 644 | // Extract the parameters and replace with capturing groups at the same 645 | // time (such that when the pattern is tested, we can map the captured 646 | // groups to the ordered paramters above. 647 | var pattern = path.replace( 648 | new RegExp( "(/):([^/]+)", "gi" ), 649 | function( $0, $1, $2 ){ 650 | // Add the named parameter. 651 | parameters.push( $2 ); 652 | 653 | // Replace with a capturing group. This captured group will be used 654 | // to create a named parameter if this route gets matched. 655 | return( $1 + "([^/]+)" ); 656 | } 657 | ); 658 | 659 | // Now that we have our parameters and our test pattern, we can create our 660 | // route mapping (which will be used by the application to match location 661 | // changes to controllers). 662 | application.routeMappings.push({ 663 | controller: this, 664 | parameters: parameters, 665 | test: new RegExp( ("^" + pattern + "$"), "i" ), 666 | handler : handler 667 | }); 668 | } 669 | 670 | }; 671 | 672 | 673 | // ----------------------------------------------------------------------- // 674 | // ----------------------------------------------------------------------- // 675 | 676 | 677 | // Create a new instance of the application and store it in the window. 678 | window.application = new Application(); 679 | 680 | // When the DOM is ready, run the application. 681 | $(function(){ 682 | window.application.run(); 683 | }); 684 | 685 | // Return a new application instance. 686 | return( window.application ); 687 | 688 | })( jQuery ); 689 | --------------------------------------------------------------------------------