diff --git a/labs/architecture-examples/maria/lib/maria/maria.js b/labs/architecture-examples/maria/lib/maria/maria.js index 2eeb7ef1d1..aa6a8fa4ad 100644 --- a/labs/architecture-examples/maria/lib/maria/maria.js +++ b/labs/architecture-examples/maria/lib/maria/maria.js @@ -223,8 +223,9 @@ et.dispatchEvent({type:'change', extraData:'abc'}); } if (hasOwnProperty(this, '_evento_parents') && !evt._propagationStopped) { - for (var i=0, ilen=this._evento_parents.length; i' + + 'hello, ' + + 'world' + + ''; + }; + +When an element view is created and its HTML template is rendered as +a DOM element, the view will automatically start listening to the DOM +element or its children for the events specified in the map returned +by the getUIActions method. This map is empty by default but you can +redefine or override as necessary and supply the necessary handler +functions which usually delegate to the controller. + + maria.ElementView.prototype.getUIActions = function() { + return { + 'mouseover .greeting': 'onMouseoverGreeting', + 'click .name' : 'onClickName' + }; + }; + + maria.ElementView.prototype.onMouseoverGreeting = function(evt) { + this.getController().onMouseoverGreeting(evt); + }; + + maria.ElementView.prototype.onClickName = function(evt) { + this.getController().onClickName(evt); + }; + +Only a few simple CSS selectors are allowed in the keys of the UI +action map. An id can be used like "#alpha" but this is not +recommended. A class name like ".greeting", a tag name like "div", or +a combination of tag name and class name like "div.greeting" are +acceptable. In almost all cases, a single class name is sufficient and +recommended as the best practice. (If you need more complex selectors +you can use a different query library to replace the Grail library +used by default in Maria.) + +You can find an element or multiple elements in a view using the +element view's find and findAll methods. + + elementView.find('.name'); // returns a DOM element + elementView.findAll('span'); // returns an array + +Because maria.View objects are composite views, so are +maria.ElementView objects. This means that sub-element-view objects can +be added to an element view. By default the sub-element-view object's +root DOM element will be added to the parent element view's root +DOM element. You can change the element to which they are added by +redefining or overridding the getContainerEl function. + + maria.ElementView.prototype.getContainerEl = function() { + return this.find('.name'); + }; + +A particularly useful pattern is using maria.ElementView as the +"superclass" of your application's element views. The following example +shows how this can be done at a low level for a to-do application. See +maria.ElementView.subclass for a much more compact way to accomplish +the same. + + checkit.TodoView = function() { + maria.ElementView.apply(this, arguments); + }; + checkit.TodoView.prototype = new maria.ElementView(); + checkit.TodoView.prototype.constructor = checkit.TodoView; + checkit.TodoView.prototype.getDefaultControllerConstructor = function() { + return checkit.TodoController; + }; + checkit.TodoView.prototype.getTemplate = function() { + return checkit.TodoTemplate; + }; + checkit.TodoView.prototype.getUIActions = function() { + return { + 'click .check' : 'onClickCheck' , + 'dblclick .todo-content': 'onDblclickDisplay', + 'keyup .todo-input' : 'onKeyupInput' , + 'keypress .todo-input' : 'onKeypressInput' , + 'blur .todo-input' : 'onBlurInput' + }; + }; + checkit.TodoView.prototype.onClickCheck = function(evt) { + this.getController().onClickCheck(evt); + }; + checkit.TodoView.prototype.onDblclickDisplay = function(evt) { + this.getController().onDblclickDisplay(evt); + }; + checkit.TodoView.prototype.onKeyupInput = function(evt) { + this.getController().onKeyupInput(evt); + }; + checkit.TodoView.prototype.onKeypressInput = function(evt) { + this.getController().onKeypressInput(evt); + }; + checkit.TodoView.prototype.onBlurInput = function(evt) { + this.getController().onBlurInput(evt); + }; + checkit.TodoView.prototype.buildData = function() { + var model = this.getModel(); + var content = model.getContent(); + this.find('.todo-content').innerHTML = + content.replace('&', '&').replace('<', '<'); + this.find('.check').checked = model.isDone(); + aristocrat[model.isDone() ? 'addClass' : 'removeClass'](this.find('.todo'), 'done'); + }; + checkit.TodoView.prototype.update = function() { + this.buildData(); + }; + checkit.TodoView.prototype.showEdit = function() { + var input = this.find('.todo-input'); + input.value = this.getModel().getContent(); + aristocrat.addClass(this.find('.todo'), 'editing'); + input.select(); + }; + checkit.TodoView.prototype.showDisplay = function() { + aristocrat.removeClass(this.find('.todo'), 'editing'); + }; + checkit.TodoView.prototype.getInputValue = function() { + return this.find('.todo-input').value; + }; + checkit.TodoView.prototype.showToolTip = function() { + this.find('.ui-tooltip-top').style.display = 'block'; + }; + checkit.TodoView.prototype.hideToolTip = function() { + this.find('.ui-tooltip-top').style.display = 'none'; + }; + +*/ maria.ElementView = function(model, controller, doc) { this._doc = doc || document; maria.View.call(this, model, controller); @@ -2182,65 +2491,132 @@ maria.ElementView.prototype.getUIActions = function() { return {}; }; +maria.ElementView.prototype.build = function() { + if (!this._rootEl) { + this.buildTemplate(); + this.buildUIActions(); + this.buildData(); + this.buildChildViews(); + } + return this._rootEl; +}; + +maria.ElementView.prototype.buildTemplate = function() { + // parseHTML returns a DocumentFragment so take firstChild as the rootEl + this._rootEl = maria.parseHTML(this.getTemplate(), this._doc).firstChild; +}; + (function() { var actionRegExp = /^(\S+)\s*(.*)$/; - maria.ElementView.prototype.getRootEl = function() { - if (!this._rootEl) { - // parseHTML returns a DocumentFragment so take firstChild as the rootEl - var rootEl = this._rootEl = maria.parseHTML(this.getTemplate(), this._doc).firstChild; - - var uiActions = this.getUIActions(); - for (var key in uiActions) { - if (Object.prototype.hasOwnProperty.call(uiActions, key)) { - var matches = key.match(actionRegExp), - eventType = matches[1], - selector = matches[2], - methodName = uiActions[key], - elements = maria.findAll(selector, this._rootEl); - for (var i = 0, ilen = elements.length; i < ilen; i++) { - evento.addEventListener(elements[i], eventType, this, methodName); - } + maria.ElementView.prototype.buildUIActions = function() { + var uiActions = this.getUIActions(); + for (var key in uiActions) { + if (Object.prototype.hasOwnProperty.call(uiActions, key)) { + var matches = key.match(actionRegExp), + eventType = matches[1], + selector = matches[2], + methodName = uiActions[key], + elements = maria.findAll(selector, this._rootEl); + for (var i = 0, ilen = elements.length; i < ilen; i++) { + maria.addEventListener(elements[i], eventType, this, methodName); } } - - var childViews = this.childNodes; - for (var i = 0, ilen = childViews.length; i < ilen; i++) { - this.getContainerEl().appendChild(childViews[i].getRootEl()); - } - - this.update(); } - return this._rootEl; }; }()); +maria.ElementView.prototype.buildData = function() { + // to be overridden by concrete ElementView subclasses +}; + +maria.ElementView.prototype.buildChildViews = function() { + var childViews = this.childNodes; + for (var i = 0, ilen = childViews.length; i < ilen; i++) { + this.getContainerEl().appendChild(childViews[i].build()); + } +}; + +maria.ElementView.prototype.update = function() { + // to be overridden by concrete ElementView subclasses +}; + maria.ElementView.prototype.getContainerEl = function() { - return this.getRootEl(); + return this.build(); }; maria.ElementView.prototype.insertBefore = function(newChild, oldChild) { maria.View.prototype.insertBefore.call(this, newChild, oldChild); if (this._rootEl) { - this.getContainerEl().insertBefore(newChild.getRootEl(), oldChild ? oldChild.getRootEl() : null); + this.getContainerEl().insertBefore(newChild.build(), oldChild ? oldChild.build() : null); } }; maria.ElementView.prototype.removeChild = function(oldChild) { maria.View.prototype.removeChild.call(this, oldChild); if (this._rootEl) { - this.getContainerEl().removeChild(oldChild.getRootEl()); + this.getContainerEl().removeChild(oldChild.build()); } }; maria.ElementView.prototype.find = function(selector) { - return maria.find(selector, this.getRootEl()); + return maria.find(selector, this.build()); }; maria.ElementView.prototype.findAll = function(selector) { - return maria.findAll(selector, this.getRootEl()); + return maria.findAll(selector, this.build()); }; +/** + +@property maria.SetView + +@parameter model {Object} Optional + +@parameter controller {Object} Optional + +@parameter document {Document} Optional + +@description + +A constructor function to create new set view objects. + + var setView = new maria.SetView(); + +maria.SetView inherits from maria.ElementView and the documentation of +maria.ElementView will tell you most of what you need to know when +working with a maria.SetView. + +A maria.SetView is intended to be a view for a maria.SetModel. The set +view will take care child views when elements are added or deleted from +the set model. + +When an element is added to the set, the set view need to know what +kind of view to make. Your application will redefine or likely override +the set view's createChildView method. + + maria.SetView.prototype.createChildView = function(model) { + return new maria.ElementView(model); + }; + +A particularly useful pattern is using maria.SetView as the +"superclass" of your application's set views. The following example +shows how this can be done at a low level for a to-do application. See +maria.SetView.subclass for a more compact way to accomplish the same. + + checkit.TodosListView = function() { + maria.SetView.apply(this, arguments); + }; + checkit.TodosListView.prototype = new maria.SetView(); + checkit.TodosListView.prototype.constructor = checkit.TodosListView; + checkit.TodosListView.prototype.getTemplate = function() { + return checkit.TodosListTemplate; + }; + checkit.TodosListView.prototype.createChildView = function(todoModel) { + return new checkit.TodoView(todoModel); + }; + +*/ maria.SetView = function() { maria.ElementView.apply(this, arguments); }; @@ -2248,19 +2624,10 @@ maria.SetView = function() { maria.SetView.prototype = new maria.ElementView(); maria.SetView.prototype.constructor = maria.SetView; -maria.SetView.prototype.setModel = function(model) { - if (this.getModel() !== model) { - maria.ElementView.prototype.setModel.call(this, model); - - var childViews = this.childNodes.slice(0); - for (var i = 0, ilen = childViews.length; i < ilen; i++) { - this.removeChild(childViews[i]); - } - - var childModels = this.getModel().toArray(); - for (var i = 0, ilen = childModels.length; i < ilen; i++) { - this.appendChild(this.createChildView(childModels[i])); - } +maria.SetView.prototype.buildChildViews = function() { + var childModels = this.getModel().toArray(); + for (var i = 0, ilen = childModels.length; i < ilen; i++) { + this.appendChild(this.createChildView(childModels[i])); } }; @@ -2269,9 +2636,8 @@ maria.SetView.prototype.createChildView = function(model) { }; maria.SetView.prototype.update = function(evt) { - // Check if there is an event as this method is also called - // at the end of building the view. - if (evt) { + // Don't update for bubbling events. + if (evt.target === this.getModel()) { if (evt.addedTargets && evt.addedTargets.length) { this.handleAdd(evt); } @@ -2297,6 +2663,7 @@ maria.SetView.prototype.handleDelete = function(evt) { var childView = childViews[j]; if (childView.getModel() === childModel) { this.removeChild(childView); + childView.destroy(); break; } } @@ -2518,14 +2885,56 @@ for maria.SetModel. */ maria.SetModel.subclass = maria.Model.subclass; +/** + +@property maria.View.subclass + +@description + +A function that makes subclassing maria.View more compact. + +The following example creates a myapp.MyView constructor function +equivalent to the more verbose example shown in the documentation +for maria.View. + + maria.View.subclass(myapp, 'MyView', { + modelActions: { + 'squashed': 'onSquashed', + 'squished': 'onSquished' + }, + properties: { + anotherMethod: function() { + alert('another method'); + } + } + }); + +This subclassing function implements options following the +"convention over configuration" philosophy. The myapp.MyView will, +by convention, use the myapp.MyController constructor. +This can be configured. + + maria.View.subclass(myapp, 'MyView', { + controllerConstructor: myapp.MyController, + modelActions: { + ... + +Alternately you can use late binding by supplying a string name of +an object in the application's namespace object (i.e. the myapp object +in this example). + + maria.View.subclass(myapp, 'MyView', { + controllerConstructorName: 'MyController', + modelActions: { + ... + +*/ maria.View.subclass = function(namespace, name, options) { options = options || {}; - var modelConstructor = options.modelConstructor; - var modelConstructorName = options.modelConstructorName || name.replace(/(View|)$/, 'Model'); var controllerConstructor = options.controllerConstructor; var controllerConstructorName = options.controllerConstructorName || name.replace(/(View|)$/, 'Controller'); var modelActions = options.modelActions; - var properties = options.properties || (option.properties = {}); + var properties = options.properties || (options.properties = {}); if (!Object.prototype.hasOwnProperty.call(properties, 'getDefaultControllerConstructor')) { properties.getDefaultControllerConstructor = function() { return controllerConstructor || namespace[controllerConstructorName]; @@ -2536,16 +2945,86 @@ maria.View.subclass = function(namespace, name, options) { return modelActions; }; } - if (!Object.prototype.hasOwnProperty.call(properties, 'initialize')) { - properties.initialize = function() { - if (!this.getModel()) { - var mc = modelConstructor || namespace[modelConstructorName]; - this.setModel(new mc()); - } - }; - } maria.subclass.call(this, namespace, name, options); }; +/** + +@property maria.ElementView.subclass + +@description + +A function that makes subclassing maria.ElementView more compact. + +The following example creates a checkit.TodoView constructor function +equivalent to the more verbose example shown in the documentation +for maria.ElementView. + + maria.ElementView.subclass(checkit, 'TodoView', { + uiActions: { + 'click .check' : 'onClickCheck' , + 'dblclick .todo-content': 'onDblclickDisplay', + 'keyup .todo-input' : 'onKeyupInput' , + 'keypress .todo-input' : 'onKeypressInput' , + 'blur .todo-input' : 'onBlurInput' + }, + properties: { + buildData: function() { + var model = this.getModel(); + var content = model.getContent(); + this.find('.todo-content').innerHTML = + content.replace('&', '&').replace('<', '<'); + this.find('.check').checked = model.isDone(); + aristocrat[model.isDone() ? 'addClass' : 'removeClass'](this.find('.todo'), 'done'); + }, + update: function() { + this.buildData(); + }, + showEdit: function() { + var input = this.find('.todo-input'); + input.value = this.getModel().getContent(); + aristocrat.addClass(this.find('.todo'), 'editing'); + input.select(); + }, + showDisplay: function() { + aristocrat.removeClass(this.find('.todo'), 'editing'); + }, + getInputValue: function() { + return this.find('.todo-input').value; + }, + showToolTip: function() { + this.find('.ui-tooltip-top').style.display = 'block'; + }, + hideToolTip: function() { + this.find('.ui-tooltip-top').style.display = 'none'; + } + } + }); + +This subclassing function implements options following the +"convention over configuration" philosophy. The checkit.TodoView will, +by convention, use the checkit.TodoModel, checkit.TodoController +and checkit.TodoTemplate objects. All of these can be configured +explicitely if these conventions do not match your view's needs. + + maria.ElementView.subclass(checkit, 'TodoView', { + modelConstructor : checkit.TodoModel , + controllerConstructor: checkit.TodoController, + template : checkit.TodoTemplate , + uiActions: { + ... + +Alternately you can use late binding by supplying string names of +objects in the application's namespace object (i.e. the checkit object +in this example). + +maria.ElementView.subclass(checkit, 'TodoView', { + modelConstructorName : 'TodoModel' , + controllerConstructorName: 'TodoController', + templateName : 'TodoTemplate' , + uiActions: { + ... + +*/ maria.ElementView.subclass = function(namespace, name, options) { options = options || {}; var template = options.template; @@ -2585,6 +3064,30 @@ maria.ElementView.subclass = function(namespace, name, options) { } maria.View.subclass.call(this, namespace, name, options); }; +/** + +@property maria.SetView.subclass + +@description + +The same as maria.ElementView. + +You will likely want to specify a createChildView method. + +The following example creates a checkit.TodosListView constructor +function equivalent to the more verbose example shown in the +documentation for maria.SetView. + + maria.SetView.subclass(checkit, 'TodosListView', { + modelConstructor: checkit.TodosModel, + properties: { + createChildView: function(todoModel) { + return new checkit.TodoView(todoModel); + } + } + }); + +*/ maria.SetView.subclass = maria.ElementView.subclass; /** diff --git a/labs/architecture-examples/maria/src/js/bootstrap.js b/labs/architecture-examples/maria/src/js/bootstrap.js index 21d31d0b69..34215fe66c 100644 --- a/labs/architecture-examples/maria/src/js/bootstrap.js +++ b/labs/architecture-examples/maria/src/js/bootstrap.js @@ -2,15 +2,19 @@ maria.addEventListener(window, 'load', function() { var loading = document.getElementById('loading'); loading.parentNode.removeChild(loading); + var model; if ((typeof localStorage === 'object') && (typeof JSON === 'object')) { var store = localStorage.getItem('todos-maria'); - var model = store ? checkit.TodosModel.fromJSON(JSON.parse(store)) : - new checkit.TodosModel(); + model = store ? checkit.TodosModel.fromJSON(JSON.parse(store)) : + new checkit.TodosModel(); evento.addEventListener(model, 'change', function() { localStorage.setItem('todos-maria', JSON.stringify(model.toJSON())); }); } + else { + model = new checkit.TodosModel(); + } - var app = new checkit.TodosAppView(model); - document.body.appendChild(app.getRootEl()); + var view = new checkit.TodosAppView(model); + document.body.appendChild(view.build()); }); diff --git a/labs/architecture-examples/maria/src/js/views/TodoView.js b/labs/architecture-examples/maria/src/js/views/TodoView.js index 538b4aacf9..4b1854ae6f 100644 --- a/labs/architecture-examples/maria/src/js/views/TodoView.js +++ b/labs/architecture-examples/maria/src/js/views/TodoView.js @@ -10,22 +10,25 @@ maria.ElementView.subclass(checkit, 'TodoView', { 'blur .todo-input' : 'onBlurInput' }, properties: { - update: function() { + buildData: function() { var model = this.getModel(); var content = model.getContent(); this.find('.todo-content').innerHTML = content.replace('&', '&').replace('<', '<'); this.find('.check').checked = model.isDone(); - aristocrat[model.isDone() ? 'addClass' : 'removeClass'](this.getRootEl(), 'done'); + aristocrat[model.isDone() ? 'addClass' : 'removeClass'](this.find('.todo'), 'done'); + }, + update: function() { + this.buildData(); }, showEdit: function() { var input = this.find('.todo-input'); input.value = this.getModel().getContent(); - aristocrat.addClass(this.getRootEl(), 'editing'); + aristocrat.addClass(this.find('.todo'), 'editing'); input.select(); }, showDisplay: function() { - aristocrat.removeClass(this.getRootEl(), 'editing'); + aristocrat.removeClass(this.find('.todo'), 'editing'); }, getInputValue: function() { return this.find('.todo-input').value; diff --git a/labs/architecture-examples/maria/src/js/views/TodosAppView.js b/labs/architecture-examples/maria/src/js/views/TodosAppView.js index c47d5f4d5d..1cd4868388 100644 --- a/labs/architecture-examples/maria/src/js/views/TodosAppView.js +++ b/labs/architecture-examples/maria/src/js/views/TodosAppView.js @@ -8,10 +8,6 @@ maria.ElementView.subclass(checkit, 'TodosAppView', { }, initialize: function() { var model = this.getModel(); - if (!model) { - model = new checkit.TodosModel(); - this.setModel(model); - } this.appendChild(new checkit.TodosInputView(model)); this.appendChild(new checkit.TodosToolbarView(model)); this.appendChild(new checkit.TodosListView(model)); diff --git a/labs/architecture-examples/maria/src/js/views/TodosInputView.js b/labs/architecture-examples/maria/src/js/views/TodosInputView.js index 6d4b83d410..79218a6ba6 100644 --- a/labs/architecture-examples/maria/src/js/views/TodosInputView.js +++ b/labs/architecture-examples/maria/src/js/views/TodosInputView.js @@ -1,5 +1,4 @@ maria.ElementView.subclass(checkit, 'TodosInputView', { - modelConstructor: checkit.TodosModel, uiActions: { 'focus .new-todo': 'onFocusInput' , 'blur .new-todo': 'onBlurInput' , diff --git a/labs/architecture-examples/maria/src/js/views/TodosListView.js b/labs/architecture-examples/maria/src/js/views/TodosListView.js index f491216720..8ad1635546 100644 --- a/labs/architecture-examples/maria/src/js/views/TodosListView.js +++ b/labs/architecture-examples/maria/src/js/views/TodosListView.js @@ -1,5 +1,4 @@ maria.SetView.subclass(checkit, 'TodosListView', { - modelConstructor: checkit.TodosModel, properties: { createChildView: function(todoModel) { return new checkit.TodoView(todoModel); diff --git a/labs/architecture-examples/maria/src/js/views/TodosStatsView.js b/labs/architecture-examples/maria/src/js/views/TodosStatsView.js index 918d95e646..8f455260c0 100644 --- a/labs/architecture-examples/maria/src/js/views/TodosStatsView.js +++ b/labs/architecture-examples/maria/src/js/views/TodosStatsView.js @@ -1,8 +1,10 @@ maria.ElementView.subclass(checkit, 'TodosStatsView', { - modelConstructor: checkit.TodosModel, properties: { - update: function() { + buildData: function() { this.find('.todos-count').innerHTML = this.getModel().length; + }, + update: function() { + this.buildData(); } } }); diff --git a/labs/architecture-examples/maria/src/js/views/TodosToolbarView.js b/labs/architecture-examples/maria/src/js/views/TodosToolbarView.js index 4a38856fdd..54737ccf10 100644 --- a/labs/architecture-examples/maria/src/js/views/TodosToolbarView.js +++ b/labs/architecture-examples/maria/src/js/views/TodosToolbarView.js @@ -1,5 +1,4 @@ maria.ElementView.subclass(checkit, 'TodosToolbarView', { - modelConstructor: checkit.TodosModel, uiActions: { 'click .allCheckbox' : 'onClickAllCheckbox' , 'click .markallDone' : 'onClickMarkAllDone' , @@ -7,11 +6,14 @@ maria.ElementView.subclass(checkit, 'TodosToolbarView', { 'click .deleteComplete': 'onClickDeleteDone' }, properties: { - update: function() { + buildData: function() { var model = this.getModel(); var checkbox = this.find('.allCheckbox'); checkbox.checked = model.isAllDone(); checkbox.disabled = model.isEmpty(); + }, + update: function() { + this.buildData(); } } });