Class: UWA.Class.View

UWA/Class/View. UWA.Class.View

new UWA.Class.View(options)

The constructor of Views takes an options hash as argument. There are several special options that, if passed, will be attached directly to the view: model, collection, container, id, className, tagName, attributes and domEvents. They become properties of the view instance itself. Other non-special (e.g. 'foo' in the example above) options only show up in the options property of your instance of view.

Also, if the view defines an setup function, it will be called when the view is first created.

Most of the time, it is not necessary and it is not recommended to override this method for the construction of an instance of a view. If you want to run some initialization code when an instance of your view is constructed, you should rather implement the setup method, which will be invoked when the collection is created.

Though there are some cases where you might want to override the default constructor logic described above. An example is given below where we want to forbid some properties like container, id, className, tagName, attributes or domEvents to be overridden at construction time.

Example
var myModel, MyProtectedView;

myModel = new UWA.Class.Model();

MyProtectedView = UWA.Class.View.extend({
    // My protected view class is named, necessary because
    // I use UWA.Controls.Abstract.getClassNames below ... :
    name: 'my-protected-view',

    // Here protect your view from users that may want to change
    // its root container or events bindings when instanciated :
    init: function (options) {
        // don't want anybody to redefine some properties
        // id, className, tagName, attributes and domEvents
        // at construction time !
        // (note: this could also be done with UWA.merge)

        // Let's shallow-copy the passed options so that
        // we don't mutate them :
        var sanitizedOptions = UWA.clone(options, false);

        [   'id',
            'className',
            'tagName',
            'attributes',
            'domEvents'].forEach(function (propToDelete) {
                delete sanitizedOptions[propToDelete];
            });

        // don't forget to call the parent class ctor
        // because it does many useful other things that are essential
        // to your instance of view!
        this._parent(sanitizedOptions);
    },

    id: function () {
        return this.model.cid; // i want model cids for unique identifiers
    },

    className: function () {
        return this.getClassNames('-container');
    },

    tagName: 'div',

    attributes: {
        text: 'some placeholder'
    },

    domEvents: {
        'click' : '_changeColor'
    },

    // Do the rest of the initialization in your implementation
    // of setup :
    setup: function (options) {
        this.myCustomViewOption = options.myCustomViewOption || 'long-profile';
        this.listenTo(this.model, "onChange:someAttribute", '_updateAPieceOfUI');
    },

    _updateAPieceOfUI: function (model, options) {
        // ...
    },

    _changeColor: function (clickEvt) {
        this.container.toggleClassName('red');
    }
});

UWA.Event.onDomReady(function () {

    var instanceOfProtectedView;

    // All options passed will have no effect except
    // model and myCustomViewOption !
    // (userOption will be available as a property
    // in instanceOfProtectedView.options)
    instanceOfProtectedView = new MyProtectedView({
        model: myModel,
        id: 1,
        className: 'foo',
        tagName: 'bar',
        attributes: {
            text: 'my custom text'
        },
        domEvents: {
            'click' : function (clickEvt) {
                alert('Hello!');
            }
        },
        myCustomViewOption: 'short-profile',
        userOption: 'zoo'
    });

    UWA.log(instanceOfProtectedView.container.className); // This outputs " my-protected-view-container".
    UWA.log(instanceOfProtectedView.container.tagName); // This outputs "DIV".
    UWA.log(instanceOfProtectedView.container.id); // This outputs "c8".
    UWA.log(instanceOfProtectedView.container.textContent); // This outputs "some placeholder".
    UWA.log(instanceOfProtectedView.myCustomViewOption); // This outputs "short-profile".
    UWA.log(instanceOfProtectedView.options); // This outputs undefined.

});
Parameters
Name Type Argument Description
options Object <optional>

an hash of options, where you can configure the model, collection, container, {@link module:UWA/Class/View.UWA.Class.View#id|id}, className, tagName, attributes and domEvents options that, if provided, are directly attached as properties to the view. Note that this options hash is also attached in the view.options property. Non-special options consequently only show up in this 'options' property of your instance of view.

Index

Members

id :String|Function

Define the id of the View's container element.

By default, a View's container element is created without any id attribute.

You can choose what will be the id of your View's container element by setting this property with a String but it may also be defined as a function, if you want to wait to define the id until runtime/creation time (for example to wait until the DOM is ready).

Type
  • String | Function
Example
var MyView;

// when instanciated, this view will have a div
// element identified with 'the_model_cid' as a root container:
MyView = UWA.Class.View.extend({
    id: function () {
        return this.model.cid; // i want model cids for unique identifiers in the DOM
                               // (which is not always a good idea)
    }
});

container :Element|Function

Element/function: The root DOM element of the view.

All views have a root DOM element at all times (the container property), whether they have already been inserted into the page or not. In this fashion, views can be rendered at any time (by calling render), and inserted/injected (see UWA.Controls.Abstract.inject for example) into the DOM all at once, in order to get high-performance UI rendering with as few reflows and repaints as possible.

In the vast majority of situations in your application, you shouldn’t assign this container property. Actually, in a perfect application (less tied to HTML), you would never assign to container. You should let the View create its container element for you, from its tagName, {@link module:UWA/Class/View.UWA.Class.View#className|className}, {@link module:UWA/Class/View.UWA.Class.View#id|id} and {@link module:UWA/Class/View.UWA.Class.View#attributes|attributes} properties, if specified (if not, container is an empty div) and then inject the View to an existing HTML. This :

  • reduces the coupling between your JavaScript and your HTML, allowing you more flexibility and making it easier to update what is going where, without having to tear things apart before putting them back together.
  • provides a nice encapsulation : the view is only responsible for rendering itself; whoever created/instanciated it is responsible for placing it where it needs to go in the existing HTML.

Though, having said that, there are exceptions to this, like progressive enhancement, when you need to take existing HTML and attach JavaScript functionality to it. So, if you are in this situation and you do need to assign an existing DOM element to the container property of your View, here are several clean ways to do it (the behind-the-scenes rule being that this container property must always be evaluated when the DOM is ready).

First of all, you can define the container property as a function returning a DOM element :

var MyView, myView;

MyView = UWA.Class.View.extend({
    // this must be defined as a function so that it does not get
    // evaluated before the DOM is ready!
    container: function () {
        return widget.getElement('#someElement');
    },

    domEvents: {
        "click #someButton": "clicked"
    },

    clicked: function (domEvent) {
        UWA.log("I was clicked!");
    }
});

UWA.Event.onDomReady(function () {
    myView = new MyView();
});

You can also use the setup method of your View to query the DOM by yourself to find your element that will be the container

var MyView, myView;

MyView = UWA.Class.View.extend({
    domEvents: {
        "click #someButton": "clicked"
    },

    setup: function () {
        this.container = widget.getElement("#someElement");
    }

    clicked: function (domEvent) {
        UWA.log("I was clicked!");
    }
});

UWA.Event.onDomReady(function () {
    myView = new MyView();
});

In the two examples above, the container element is specified in the definition of your MyView class, meaning that --unless explicitely overridden at instanciation time-- all instances of MyView will by default share the same DOM element as their container. Just keep this in mind!

There are times when it does not make sense to specify the target DOM element that will be the container directly in your view definition. Let's not go into all of the details on when, because that is also largely a question of style and preferences. Though you will fairly regularly find yourself in situations where you want to decouple the view definition from the knowledge of its container. In these situations all you need to do is specify a container when you instantiate the view:

var MyView, myView;

// The MyView class itself does not define a 'container'...
MyView = UWA.Class.View.extend({
    domEvents: {
        "click #someButton": "clicked"
    },

    clicked: function (domEvent) {
        UWA.log("I was clicked!");
    }
});

// If you were to instantiate a MyView without passing any options
// to its constructor, you would get a 'container' generated for you
// and end up with a <div> by default ...
// However, by passing in the 'container' option, you are overriding
// the 'container' for that specific instance of the view ! :
UWA.Event.onDomReady(function () {
    myView = new MyView({
        container: widget.getElement('#someElement')
    });
});

Please note that :

  • you can pass either a native DOM element or an UWA-extended UWA.Element for this container property, in all cases it will be internally extended to an UWA.Element.
  • as explained above, this container property may be defined as a function, if you want to wait to define it until runtime.
Type
  • Element | Function

attributes :Object|Function

Define the attributes of the View's container element.

You can set some attributes on your View's container element by setting this property with an hash of { attributeName: 'attrValue' } pairs but it may also be defined as a function, if you want to wait to define what will be these attributes and their values until runtime/creation time (for example to wait until the DOM is ready).

Type
  • Object | Function

domEvents :Object|Function

Define the mapping between DOM events in the view and callbacks.

domEvents is an hash mapping DOM events happening within the scope of your view's container (that acts as a delegate to handle these events) with callbacks.

These mappings are written in the format {"event selector": "callback"}. The callback may be either the name of a method on the view, or a direct function body. Omitting the selector causes the DOM event to be bound to the view's root element (this.container).

The domEvents property may also be defined as a function that returns an events hash, to make it easier to programmatically define your events, as well as inherit them from parent views.

Type
  • Object | Function
Example
// this example demonstrates the two forms domEvents can take
// (hash or function), and how to cleanly inheritate domEvents from a
// parent view domEvents, no matter how it is defined (as a hash or a
// function)

var ParentView = UWA.Class.View.extend({
    // domEvent defined as a hash :
    domEvents: {
        'click button.maximize' : 'onMaximize'
    },
    onMaximize: function (evt) {
        UWA.log('Maximum r&b');
    }
});

var ChildView = ParentView.extend({
    // domEvents defined as a function :
    domEvents: function () {
        var parentEvents = ParentView.prototype.domEvents;
        if (UWA.is(parentEvents, 'function')) {
            parentEvents = parentEvents();
        }
        // In any case, clone what we got from the parent :
        parentEvents = UWA.clone(parentEvents, false);
        return UWA.extend(parentEvents || {}, {
              'blur' : 'onBlur'
        });
    },
    onBlur: function (e) {
        UWA.log('I got my head checked by a jumbo jet');
    }
});

tagName :String|Function

Define the tagName of the View's container element.

The default {@link module:UWA/Class/View.UWA.Class.View#tagName|tagName} of a View's container element is <div>.

You can change the {@link module:UWA/Class/View.UWA.Class.View#tagName|tagName} of your View's container element by setting this property with a String but it may also be defined as a function, if you want to wait to define the {@link module:UWA/Class/View.UWA.Class.View#tagName|tagName} until runtime (for example to wait until the DOM is ready).

Type
  • String | Function

className :String|Function

Define the CSS className of the View's container element.

By default, a View's container element is created without any class.

You can choose what will be the className of your View's container element by setting this property with a String but it may also be defined as a function, if you want to wait to define the className until runtime (for example to wait until the DOM is ready).

Type
  • String | Function
Example
var View1, View2;

// when instanciated, this view will have a div element with a class
// "foo" as a root container:
view1 = UWA.Class.View.extend({
    className: 'foo'
});

// Another way to define the className with a function :
view2 = UWA.Class.View.extend({
    // the view class is named, necessary because
    // I use UWA.Controls.Abstract.getClassNames below ... :
    name: 'view2',

    className: function () {
        return this.getClassNames('-container');
    }
});

<readonly> cid :String

A special property of views, the cid or client id is a unique identifier automatically generated and assigned to all instances of views when they're first created. Can be used to prefix ids in the DOM, (see {@link module:UWA/Class/View.UWA.Class.View#id|id} property below) or create unique CSS classnames. Note that you also have UWA.Controls.Abstract.getClassNames to do the later. Do not modify this property, it is read only.

Type
  • String

Methods

setup()

Override this method to initialize a newly created instance of view.

Notes:

  • The default implementation does nothing.
  • It is passed the same arguments that were passed to the constructor init method.
  • setup is automatically called by constructors of views. So do not directly call it in your application code. It is internally used and consequently only meant to be overridden.
  • Overriding it with your own initialization logic is optional; however it is a good practice to implement it when sub-classing a view, because the setup method is an ideal place to house logic that needs to run when view instances are created. Typically, you should register there to events emitted by your observed model or collection, using listenTo, implemented by instances of views.
Example
var ProfileView;

// Definition of a ProfileView, meant to display data about a User :
ProfileView = UWA.Class.View.extend({

    className: function () {
        return this.getClassNames('-container');
    },

    setup: function (options) {
        var that = this;
        that._type = options.type || 'detailed';
        that.listenTo(that.model, {
            'onChange:firstName' : that._redrawFirstName,
            'onChange:lastName' : that._redrawLastName,
            'onChange:picUrl' : that._updatePicture
        });
    },

    destroy: function () {
        // stop listening to our observed model, this is very important
        // so that our view can be effectively removed by the GC (otherwise
        // it is held by the model, as a registered observer).
        this.stopListening(this.model);
        this.model = null;

        // Eventually call this._parent() so that our container element
        // is destroyed and removed from the DOM.
        this._parent();
    },

    _redrawFirstName : function () {
        // ...
    },

    _redrawLastName : function () {
        // ...
    },

    _updatePicture : function () {
        // ...
    }
});

destroy() → {this}

Override this method to destroy your view.

destroy is generally called by parent views, consequently a view must not destroy itself.

Please also note that the view and its child views should not be used after destroy has been invoked/called. (A way to enforce the detection of such illegal postmortem usages is to set the container reference to null in your implementation of destroy)

See also UWA.Controls.Abstract.destroy for a description of the default implementation.

You must override this destroy method to perform the following tasks (when applicable) in the given order:

  • 1) stop listening to your child views
  • 2) destroy and dereference your child views
  • 3) stop listening to your observed models, this is very important so that your view can be effectively removed by the garbage collector. If you don't do that, your view (and its DOM elements) is held by the observed models, as an observable).
  • 4) call this._parent() in your overridden implementation so that UWA.Controls.Abstract.destroy is eventually called. This will remove all delegated events on the container and remove the container and children elements from the document.
  • 5) you can eventually set the container reference to null (also other references to other DOM elements), so that any code dealing with the container called after destroy has been called would fail. This can help detecting some illegal access to zombie views.

Notes:

You don't have to take care in destroy of not listening anymore (aka detaching) the events defined in domEvents.

Example
var MyView = UWA.Class.View.extend({

    // the UWA class name, useful to generate CSS classNames:
    name: 'AppView',

    className: function () {
        return this.getClassNames();
    },

    setup: function (options) {

        // listen to events emitted by the model :
        this.listenTo(this.model, 'onChange:someAttr', this._updateAPieceOfUI);

        // a child view (created but not yet rendered) :
        this._childView = new AnotherView();

        // Listen to some events of my subViews :
        this.listenTo(this._childView, 'onButtonClicked', this._updateAnotherPieceOfUI);
    },

    destroy: function () {
        // 1) stop listening to child view ...
        this.stopListening(this._childView);

        // 2) destroy and dereference child view :
        this._childView.destroy();
        this._childView = null;

        // 3) stop listening to the events emitted by the model :
        this.stopListening(this.model);
        this.model = null;

        // 4) call implementation of destroy by my parent class.
        // This will remove all delegated events on the container
        // and remove the container and children elements from the
        // document.
        this._parent();
    },

    _updateAPieceOfUI: function (domEvent) {
        // ...
    },

    _updateAnotherPieceOfUI: function () {
        // ...
    }
});
Returns

Current UWA.Class.View instance.

Type
this

render() → {this}

Override this method to house the logic required to construct the view.

render is the core method that your view should override, in order to populate its root container element, with the appropriate HTML. The convention is for render to always return this, this allows to chain view methods, like this very common chain of calls :

myInstanceOfView.render().inject(parentTargetDomElement);

Notes:

  • It is not a good practice in general to call render directly in your implementation of {@link module:UWA/Class/View.UWA.Class.View#setup|setup}. This leads to the view being immediately rendered when it is instanciated. It is better to decouple the instanciation/construction and the rendering of the view, this way you can choose the moment when you want to render the view.
  • A good coding pattern is to make your implementation of render call some other private methods dedicated to the (re-)rendering of some small sub parts of your view. Another very common coding pattern is to call the render methods of your sub-views in the implementation of render.
  • (this remark applies to any code in your view that is looking up DOM elements) To lookup some elements in your view in render, always use getElement and getElements methods, that are scoped to DOM elements within the current view! Never use global lookups like Widget.getElement for example!
  • In general, render is called by the parent/aggregating view.
Returns

Current UWA.Class.View instance.

Type
this

setContainer(element) → {this}

Change the view's {@link module:UWA/Class/View.UWA.Class.View#container|container}, including event re-delegation.

Use setContainer if you'd like to apply a view to a different DOM element. setContainer will also create the {@link module:UWA/Class/View.UWA.Class.View#container|container} reference and move the view's delegated events from the old element to the new one.

Example
var v = new UWA.Class.View();

v.render().inject(widget.body);

// example where we want to render the view somewhere else :
v.destroy();
v.setContainer(widget.getElement('#foo')).render();

// example where the view is rendered somewhere else without having
// deleted its previously rendered elements :
v.setContainer(widget.getElement('#bar')).render();

// example where the view is destroyed and re-rendered in a new
// container that is injected in the document's body :
v.destroy();
v.setContainer({
    tag: 'ul',
    'class': 'myElement items',
    html: [
        {
            tag: 'li',
            'class': 'item',
            text: 'My first item'
        },
        {
            tag: 'li',
            'class': 'item',
            text: 'My second item'
        }
    ]
}).render().inject(widget.body);
Parameters
Name Type Description
element Element

A DOM element or an hash of attributes (see example below)

Returns

Current UWA.Class.View instance.

Type
this

getElement(selector) → {UWA.Element}

Gets the first descendant element in the view that matches a CSS selector.

getElement is a UWA.Element.getElements delegate for element lookup, scoped to DOM elements within the current view. Within your view code, this must always be prefered to global lookups like document.getElementById('foo') or widget.getElement('#foo')!

Parameters
Name Type Description
selector String

The CSS selector to use (could be more than one rule).

Returns

UWA.Element matched in the view.

Type
UWA.Element

getElements(selector) → {Array}

Gets all descendants elements that match a CSS selector.

{@link module:UWA/Class/View.UWA.Class.View#getElements|getElements} is a UWA.Element.getElements delegate for element lookup, scoped to DOM elements within the current view. Within your view code, this must always be prefered to global lookups like document.getElementById('foo') or widget.getElement('#foo')!

Parameters
Name Type Description
selector String

The CSS selector to use (could be more than one rule).

Returns

Array of UWA.Element that matched in the view.

Type
Array

delegateDOMEvents(DOMEvents) → {this}

Attach callbacks to delegated DOM events given a mapping

By default, delegateDOMEvents is internally called when a view is instantiated, referencing the domEvents option object for any events that need to be setup. If, for whatever reason, the events attached to a view are removed (the view is destroyed or undelegateDOMEvents has been called), consider the delegateDOMEvents method the tool for refreshing/reattaching the events to the view.

To demonstrate the delegateDOMEvents method, in the example below, a view is rendered without setting up events during extending or instantiate. After the view is created, the view's domEvents property is updated thanks to delegateDOMEvents so that the appropriate callback is invoked when the button is clicked depending on the current period of time (morning, afternoon & evening).

When delegateDOMEvents is called, perhaps with a different events hash, all callbacks are removed and delegated afresh — useful for views which need to behave differently when in different modes.

Notes:

  • delegateDOMEvents can be called with no argument. In this case, the events map defined in {@link module:UWA/Class/View.UWA.Class.View#domEvents|domEvents} is simply re-used for re-attaching delegated events that would have been detached using undelegateDOMEvents.
  • You cannot refresh/reattach the view's DOM events using setOption or setOptions methods (from UWA.Class.Options mixin implemented by views), you have to use delegateDOMEvents.
Example
  var MyView, myViewInstance, today, idx,
      timeOutInMillisecs, greetingCallbackNames;

  MyView = UWA.Class.View.extend(UWA.Class.Debug, {
      render: function () {
          this.container.addContent({
              tag: 'button',
              text: 'Give greeting!'
          });
          return this;
      },

      // (factorization is not the point of the example here!)
      _sayGoodMorning: function () {
          this.log('Good Morning!');
      },
      _sayGoodAfternoon: function () {
          this.log('Good Afternoon!');
      },
      _sayGoodEvening: function () {
          this.log('Good Evening!');
      },
      _sayGoodNight: function () {
          this.log('Good night!');
      }
  });

  myViewInstance = new MyView(); // create view ...
  myViewInstance.debugMode = true;

  timeOutInMillisecs = 6 * 60 * 60 * 1000; // every 6 hours,
                                           // the events map is changed

  greetingCallbackNames = [
      '_sayGoodNight',
      '_sayGoodMorning',
      '_sayGoodAfternoon',
      '_sayGoodEvening'
  ];

  function bindGreetingCBtoClickEvent () {
      myViewInstance.delegateDOMEvents({
          'click button': greetingCallbackNames[(idx++)%4]
      });
      setTimeout(bindGreetingCBtoClickEvent, timeOutInMillisecs);
  };

  today = new Date();
  idx = Math.floor(today.getHours() / 6);

  bindGreetingCBtoClickEvent(); // call delegateDOMEvents because we setup the events after the view was created

  myViewInstance.render().inject(document.body); // ... render and inject it!
Parameters
Name Type Argument Description
DOMEvents Object <optional>

an hash mapping DOM events happening within the scope of your view's container (that acts as a delegate to handle these events) with names of callbacks of the view. See {@link module:UWA/Class/View.UWA.Class.View#domEvents|domEvents} for details.

Returns

Current UWA.Class.View instance.

Type
this

undelegateDOMEvents() → {this}

Detach callbacks from delegated DOM events

undelegateDOMEvents clears all callbacks previously bound to the view with delegateDOMEvents. You usually don't need to use this, but may wish to if you have multiple views attached to the same DOM element.

{@link module:UWA/Class/View.UWA.Class.View#undelegateDOMEvents|undelegateDOMEvents} is also useful if you want to disable a view from the DOM temporarily.

Returns

Current UWA.Class.View instance.

Type
this