A View represents a renderable piece of an application's user interface, and provides hooks for easily subscribing to and handling delegated DOM events on a view's container element.
Views provide a generic structure for template- or DOM-based rendering. Views are template-agnostic, meaning that there's no actual template language built in, so you're welcome to use any template language you want (or none at all).
A common practice is to associate a View instance with a Model instance so that the view is automatically re-rendered whenever the data in the model changes, but this relationship is not required. A view may also be used standalone, associated with a Model List, or may even contain nested views.
The Y.View
class is meant to be extended by a custom class that defines a custom render()
method and any necessary DOM event handlers.
To include the source files for View and its dependencies, first load the YUI seed file if you haven't already loaded it.
<script src="http://yui.yahooapis.com/3.18.1/build/yui/yui-min.js"></script>
Next, create a new YUI instance for your application and populate it with the
modules you need by specifying them as arguments to the YUI().use()
method.
YUI will automatically load any dependencies required by the modules you
specify.
<script> // Create a new YUI instance and populate it with the required modules. YUI().use('view', function (Y) { // View is available and ready for use. Add implementation // code here. }); </script>
For more information on creating YUI instances and on the
use()
method, see the
documentation for the YUI Global Object.
A beta version of View was first introduced in YUI 3.4.0. If you're using View in YUI 3.4.0 or 3.4.1, you'll need to make the following changes to your code when upgrading:
The container
, model
, and modelList
properties are now attributes. If you were accessing them as properties, update your code to access them as attributes instead. For example, var model = myView.model
becomes var model = myView.get('model')
, and myView.model = thing
becomes myView.set('model', thing);
.
The container
attribute now treats string values as CSS selectors. Previously, it assumed string values represented raw HTML. To get the same functionality as the old behavior, pass your HTML string through Y.Node.create()
before passing it to container
. For example, new Y.View({container: '<div/>'})
becomes new Y.View({container: Y.Node.create('<div/>')})
.
Destroying a view no longer also destroys the view's container node by default. To destroy a view's container node when destroying the view, pass {remove: true}
to the view's destroy()
method.
The most basic way to use View is to create an instance of the Y.View
class, pass in some configuration attributes, and override the render()
method at the instance level to define how your view will be rendered.
To demonstrate how to associate a Model with a View, this example uses an instance of the Y.PieModel
class that's used in the examples in the Model User Guide.
Jump to the View Attributes section below to learn more about the container
, model
, and template
attributes used in this example.
var pie = new Y.PieModel({type: 'apple'}), pieView = new Y.View({ // Override the default container attribute. container: Y.Node.create('<div class="pie"/>'), // Specify an optional model to associate with the view. model: pie, // Provide an optional template that will be used to render the view. The // template can be anything we want, but in this case we'll use a string // that will be processed with Y.Lang.sub(). template: '{slices} slice(s) of {type} pie remaining.' }); // Override the render() method to define how the view will be rendered. pieView.render = function () { var container = this.get('container'), html = Y.Lang.sub(this.template, this.get('model').toJSON()); // Render this view's HTML into the container element. container.setHTML(html); // Append the container element to the DOM if it's not on the page already. if (!container.inDoc()) { Y.one('body').append(container); } return this; }; // Render the view. pieView.render();
This renders the following HTML to the page:
<div class="pie"> 6 slice(s) of apple pie remaining. </div>
Creating an instance of the base Y.View
class like this makes it easy to get up and running with a simple view, but probably isn't the best way to create more complex views that need to handle DOM events, re-render automatically on model changes, etc.
For more complex use cases, it's usually better to create a custom View subclass by Extending Y.View
.
Y.View
The first step in creating a custom View class is to extend Y.View
. This allows you to override the render()
method and default View attributes to implement the desired behavior for your view, while also adding your own methods to handle DOM events and provide other custom view functionality.
If you want, you can establish a relationship between your view and a Model or Model List instance by attaching event handlers to them in a custom initializer()
method. The initializer is typically where you would subscribe to model change events to be notified when you need to re-render your view.
This example demonstrates how to create a Y.PieView
class that displays the current state of a Y.PieModel
instance like the one defined in the Model user guide. It's functionally similar to the example shown in Instantiating View, but adds the ability to handle a DOM event with a custom event handler, and automatically re-renders the view whenever the associated model changes.
// Create a new Y.PieView class that extends Y.View and renders the current // state of a Y.PieModel instance. Y.PieView = Y.Base.create('pieView', Y.View, [], { // Add prototype methods and properties for your View here if desired. These // will be available to all instances of your View. You may also override // existing default methods and properties of Y.View. // Specify delegated DOM events to attach to the container. events: { '.eat': {click: 'eatSlice'} }, // Provide a template that will be used to render the view. The template can // be anything we want, but in this case we'll use a string that will be // processed with Y.Lang.sub(). template: '{slices} slice(s) of {type} pie remaining. ' + '<button class="eat">Eat a Slice!</button>', // The initializer function will run when a view is instantiated. This is a // good time to subscribe to change events on a model instance. initializer: function () { var model = this.get('model'); // Re-render this view when the model changes, and destroy this view when // the model is destroyed. model.after('change', this.render, this); model.after('destroy', this.destroy, this); }, // The render function is responsible for rendering the view to the page. It // will be called whenever the model changes. render: function () { var container = this.get('container'), html = Y.Lang.sub(this.template, this.get('model').toJSON()); // Render this view's HTML into the container element. container.setHTML(html); // Append the container element to the DOM if it's not on the page already. if (!container.inDoc()) { Y.one('body').append(container); } return this; }, // The eatSlice function will handle click events on this view's "Eat a Slice" // button. eatSlice: function (e) { // Call the pie model's eatSlice() function. This will consume a slice of // pie (if there are any left) and update the model, thus causing the view // to re-render to reflect the new model data. this.get('model').eatSlice(); } }, { // Specify attributes and static properties for your View here. ATTRS: { // Override the default container attribute. container: { valueFn: function () { return Y.Node.create('<div class="pie"/>'); } } } });
After defining the Y.PieView
class and the Y.PieModel
class (see the Model user guide), we can instantiate a new PieView and associate it with a PieModel instance.
var pie = new Y.PieModel({type: 'apple'}), pieView = new Y.PieView({model: pie}); pieView.render();
This renders the following HTML to the page:
<div class="pie"> 6 slice(s) of apple pie remaining. <button class="eat">Eat a Slice!</button> </div>
If the user clicks the "Eat a Slice!" button, the model will be updated and the view will re-render itself:
<div class="pie"> 5 slice(s) of apple pie remaining. <button class="eat">Eat a Slice!</button> </div>
addTarget()
In Extending Y.View
, the view's initializer()
set two event listeners directly on the model:
var model = this.get('model'); model.after('change', this.render, this); model.after('destroy', this.destroy, this);
Alternatively, you can use addTarget() to create a bubbling chain. In the example below, the view automatically receives events from the model, which means you can now choose to set change event listeners on the view.
Y.PieView = Y.Base.create('pieView', Y.View, [], { ... initializer: function () { var model = this.get('model'); // If this view has a model, bubble model events to the view. model && model.addTarget(this); // If the model gets swapped out, reset targets accordingly. this.after('modelChange', function (ev) { ev.prevVal && ev.prevVal.removeTarget(this); ev.newVal && ev.newVal.addTarget(this); }); // Re-render this view when the model changes. this.after('*:change', this.render, this); }, ...
The modelChange
listener is not strictly necessary, but it does make your code more robust.
If you decide to swap in another model instance sometime after initialization,
this listener ensures that the new model gets wired up properly to the view.
The following properties are meaningful to View classes and subclasses.
Property | Default Value | Description |
---|---|---|
containerTemplate |
'<div/>' |
HTML template for this view's container element. This will be used to create the container if no custom container is specified when the view is created. |
events |
{} |
A map of CSS selectors to DOM events that should be handled by your view. Under the hood, event delegation is used so that the actual events are attached to the view's container element. This means you can freely re-render the contents of the container without having to worry about detaching and re-attaching events. See Handling DOM Events for more details. |
template |
'' |
Reference to a template for this view.
This is a convenience property that has no default behavior of its own. It's only provided as a convention to allow you to store whatever you wish to use as a template, whether that's an HTML string, a
How this template gets used is entirely up to you and your custom |
The View class uses both properties and attributes. What's the difference? In short, properties are best for storing data that might be useful to multiple instances of a View, whereas attributes are best for storing data that pertains only to a specific instance.
View classes and subclasses provide the following attributes.
Attribute | Default Value | Description |
---|---|---|
container |
<div> Node |
A DOM element,
If you specify a container element that isn't already on the page, then you'll need to append it to the page yourself. You can do this in the
Note that if you are extending a view and want to set a default value for the ATTRS: { container: { valueFn: function () { /* return a Y.Node */ } } }
The view's constructor will ignore any assignments using the |
Views also support ad-hoc attributes, meaning you can simply pass an object hash to a view's constructor and attributes will automatically be created for the keys you specify.
// Instantiate a view and setting some ad-hoc attributes. var view = new Y.View({foo: 'foo', bar: 'bar'}); view.get('foo'); // => "foo" view.get('bar'); // => "bar"
The events
property of a view class allows you to specify a mapping of CSS selectors to DOM events that should be handled by your view.
Under the hood, event delegation is used so that the actual events are attached to the view's container element. This means you can freely re-render the contents of the container without having to worry about detaching and re-attaching events.
Event handlers may be specified as functions or as strings that map to function names on the view instance or its prototype.
var Y.MyView = Y.Base.create('myView', Y.View, [], { events: { // Call `this.toggle()` whenever the element with the id "toggle-button" // is clicked. '#toggle-button': {click: 'toggle'}, // Call `this.hoverOn()` when the mouse moves over any element with the // "hoverable" class, and `this.hoverOff()` when the mouse moves out of // any element with the "hoverable" class. '.hoverable': { mouseover: 'hoverOn', mouseout : 'hoverOff' } }, hoverOff: function (e) { // ... handle the mouseout event ... }, hoverOn: function (e) { // ... handle the mouseover event ... }, toggle: function (e) { // ... handle the click event ... } });
The this
object in view event handlers will always refer to the current view instance. If you'd prefer this
to refer to something else, use Y.bind()
to bind a custom this
object.
At instantiation time, you can provide an events
config property to add or override event handlers for individual view instances.
// Overriding or adding event handlers on a per-instance basis. var myView = new Y.MyView({ events: { // Replace the #toggle-button click handler with a custom one. '#toggle-button': { click: function (e) { // ... custom click handler ... } }, // Add a handler for focus events on elements with the "focusable" class. '.focusable': { focus: function (e) { // ... custom focus handler ... } } } });
A view's default render()
method is completely empty. It's up to you to override this method and fill it with your own rendering logic.
The ultimate goal of your render()
function is to put some HTML into the view's container element and ensure that the container is on the page. How you choose to do this is entirely up to you.
You might choose to use plain old DOM manipulation to create the elements for your view, or you might store an HTML string in your view's template
property and then render that, or you might even store a full-blown template (perhaps using Handlebars or Mustache). The View component intentionally avoids dictating how you render things, so you're free to do whatever you like best.
Note: Ideally your render()
method should also return this
at the end to allow chaining, but that's up to you.
In general, it makes sense to associate a view with a Model or Model List instance so that you can update the view when related data changes. You can do this either by re-rendering the entire view (this is the easiest way) or by modifying only the parts of the view that have changed (harder, but possibly more performant).
Again, which route you choose to take is entirely up to you.
When instantiating a view, you may pass a model
attribute in the config object that references a Model instance you wish to associate with the view.
// Associate a PieModel instance with a PieView instance. var pie = new Y.PieModel({type: 'apple'}), pieView = new Y.PieView({model: pie});
This attribute is entirely optional. There's no requirement that views be associated with models, but if you do intend to associate your view with a model, then specifying that model instance at instantiation time will cause a reference to be stored for convenience.
There's no special magic under the hood that will link the model to your view; you'll still need to manually subscribe to any model events you want to handle in your view's initializer()
function (see the example in Extending Y.View
).
Instead of specifying a model association, you could also choose to associate your view with a Model List, or even with nothing at all. It's entirely up to you.
To associate a view with a Model List instead of a Model, use the modelList
config attribute. In your view's initializer, attach event listeners to list events to re-render the view when the list's contents change or when the data of one of the models in the list changes.
// Create a custom View subclass that's associated with a Model List. var Y.PieListView = Y.Base.create('pieListView', Y.View, [], { // ... other prototype properties and methods ... initializer: function () { var list = this.get('modelList'); // Re-render this view when a model is added to or removed from the model // list. list.after(['add', 'remove', 'reset'], this.render, this); // We'll also re-render the view whenever the data of one of the models in // the list changes. list.after('*:change', this.render, this); } // ... other prototype properties and methods ... });
Then pass in a Model List instance when instantiating your view.
var pies = new Y.PieList(), pieListView = new Y.PieListView({modelList: pies}); // When we add a pie to the list, the view will be re-rendered. pies.add({type: 'banana cream'});
The model
and modelList
attributes are really just ad-hoc attributes that are created on demand, so using "model" and "modelList" as the names is just a convention and not a baked-in requirement. Feel free to store your models and model lists under different attribute names if you want.
The NodeMap extension adds a static getByNode()
method to a View subclass that returns the View instance associated with a given Node instance (much like Y.Widget.getByNode()
does for widgets). The Node may be a View container or a child of a View container.
This functionality is provided by an optional extension because it requires you to manually call destroy()
on your views when you're done using them in order to avoid memory leaks due to long-lived internal references.
To use this extension, load the view-node-map
module and pass Y.View.NodeMap
in the extensions array when creating a View subclass.
<div id="container"> <div id="child"></div> </div> <script> YUI().use('view', 'view-node-map', function (Y) { // Create a custom View subclass that mixes in the Y.View.NodeMap extension. Y.PieView = Y.Base.create('pieView', Y.View, [Y.View.NodeMap]); // Create a new instance of the custom View. var pieView = new Y.PieView({container: '#container'}); // Look up the View instance by its container. Y.PieView.getByNode('#container'); // returns the pieView instance // ...or by a child of its container. Y.PieView.getByNode('#child'); // also returns the pieView instance }); </script>
While Y.View
and Y.Widget
may seem similar on the surface, they're intended for different purposes, even though they have some overlap.
In a nutshell: a view is meant to be used as an internal piece of a component or application, and is not intended to be exposed to external developers as an API or a reusable component itself. A widget, on the other hand, is meant to be a reusable component with a public API.
Views are well suited for rendering portions of web pages, whether large or small, since they're lightweight and can be easily associated with Models and Model Lists. A view may even be responsible for creating and rendering widgets on a page, but the view is an internal piece of an application or component and is not meant to be used externally.
Widgets are well suited for representing self-contained interactive controls or objects that behave like first-class HTML elements. A widget might actually use views to provide a visual representation and even handle some DOM events — but only as internal plumbing. The widget itself is responsible for providing a reusable public API.