The YUI App Framework is a rollup of the App, Model, Model List, Router, and View components, combined to form a simple MVC-style framework for writing single-page JavaScript applications.
You can use these components separately or together to create anything from simple non-interactive views to rich applications with URL-based routing, data binding, and full client-server synchronization.
If you've used DocumentCloud's excellent Backbone.js framework, many of the classes and APIs provided by App Framework components will look familiar to you. There are important differences, though, and the App Framework takes full advantage of YUI's powerful component and event infrastructure under the hood.
To include the source files for App Framework 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('app', function (Y) { // App Framework 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.
The app
module is a rollup module consisting of the following individual components. These components may also be used individually if you don't need all of them at the same time.
Component | Module | Description |
---|---|---|
App | app-base |
Provides a top-level application component which manages navigation and views. This gives you a foundation and structure on which to build your application; it combines robust URL navigation with powerful routing and flexible view management. |
Model | model |
A lightweight |
Model List | model-list |
An array-like ordered list of |
Router | router |
Provides URL-based same-page routing using HTML5 history ( |
View | view |
Represents a piece of an application's user interface and is responsible for rendering content and handling DOM events. Views are commonly associated with Models or Model Lists and are re-rendered when they change, although this isn't required. |
App is a high-level component that builds upon other components in the App Framework. The App component is composed of Router and View, and also the Pjax utility. This combination creates a solid foundation and structure on which to build your application. It connects together robust URL navigation with powerful routing and flexible view management.
The goal of the App component is to provide you a place to organize and connect together the parts of your application. App implements infrastructural pieces which are common to all apps — such as managing views and the navigation between pages — allowing you to focus on the specifics of your app.
App will enable you to seamlessly enhance the user experience and performance of traditional client/server apps. It enables you to create richer interactions without compromising standard browser behavior, URLs, or search engine crawl-ability. The Routing Coordination with Server section of this guide contain details on accomplishing this.
You can also use the App component to build client-only apps for when there is no server, or the server is not capable of routing and handling requests. There are drawbacks to client-only apps which you need to be aware of and fully understand their implications. Be sure to read the Progressively-enhanced vs. Client-only Apps section which contains details on these drawbacks and best practices.
The remaining sections of this guide provide details on what you'll need to know to start working with the App component. Refer to the previous section for information about the other components of the App Framework.
Creating an App instance couldn't be any simpler:
var app = new Y.App();
Two instantiable classes are provided by the app-base
module: Y.App
, and Y.App.Base
. The difference between these is that Y.App.Base
provides the basic app functionality and will remain pure; whereas Y.App
(which extends Y.App.Base
) will have all of the app-component extensions automatically mixed-in when they are included in the YUI instance.
In the following example, we are including both the app-base
and app-transitions
modules. When the app-transitions
module is added to the YUI instance it will automatically mix itself into the Y.App
class, adding transitions to all Y.App
instances, but Y.App.Base
will remain unaffected by the inclusion of this module.
YUI().use('app-base', 'app-transitions', function (Y) { // This will create two YUI Apps, `basicApp` will not have transitions, // but `fancyApp` will have transitions support included and turn itself on. var basicApp = new Y.App.Base(), fancyApp = new Y.App({transitions: true}); });
While App instances are usable without any configuration, any non-trivial app will need to be configured. You might also want to extend Y.App
by mixing-in additional functionality, or even create a custom App class to implement the specific features of your application. The Extending Y.App
section explains how to do this.
When constructing a new Y.App
instance you can provide a config object with initial values for attributes along with values for Y.View
's properties and Y.Base
's "special" properties (used only during initialization). In addition to these, the following non-attribute properties have specific initialization logic applied and can be passed to the Y.App
constructor:
views
Hash of view-name to metadata used to declaratively describe an application's views and their relationship with the app and its other views. The views specified here will override any defaults provided by the views
object on the prototype
. Every Y.App
instance gets its own copy of a views
object so this object on the prototype will not be polluted.
See the App Properties and Declaring Views sections for more details.
Here's an example that defines some views
at instantiation time:
var app = new Y.App({ views: { home : {preserve: true}, users: {preserve: true}, user : {parent: 'users'} } });
The following properties are meaningful to App classes and subclasses. In addition to these, View's properties are also applicable Y.View
is part of App's composition.
Property | Default Value | Description |
---|---|---|
views |
{} |
Hash of view-name to metadata used to declaratively describe an application's views and their relationship with the app and its other views. See Declaring Views for more details. The view metadata is composed of Objects keyed to a view-name that can have any or all of the following properties:
|
transitions |
{} |
Default transitions to use when the
The following are types of changes for which transitions can be defined that correspond to the relationship between the new and old
|
The App class uses both properties and attributes. Properties are best when their stored data might be useful to multiple App instances, whereas attributes are best when the data being stored only pertains to a single instance.
App is composed of View, Router, and Pjax, all of which provide attributes that will be of interest to you — beyond these, App adds the following attributes:
Attribute | Default Value | Description |
---|---|---|
activeView |
null |
The application's active/visible view. This attribute is read-only, to set the See Switching the Active View for more details. |
serverRouting |
undefined |
Whether or not this application's server is capable of properly routing all requests and rendering the initial state in the HTML responses. This can have three different values, each having particular implications on how the app will handle routing and navigation:
See Routing Coordination with Server for more details. |
transitions |
false |
Whether or not this application should use view transitions, and if so then which ones or Note: Transitions are an opt-in feature and will only be used in browsers which support native CSS3 transitions.
See the Switching the Active View and |
viewContainer |
<div> Node |
The node into which this app's
The view container node serves as the container to hold the app's
The default view container is a
The app's See Rendering an App for more details. |
A few of App's inherited attributes are given new default values:
Attribute | Inherited From | New Default Value | Reason |
---|---|---|---|
container |
Y.View |
<body> Node |
Apps are considered to be full-page by default. |
html5 |
Y.Router |
auto |
This value is dependent on the value of |
linkSelector |
Y.PjaxBase |
"a" |
By default this selector should match all links on the page because full-page apps are the default. |
A primary feature of App is its powerful routing which enables you to enhance the user experience and performance of traditional client/server apps. Using Y.App
you can create more meaningful and richer interactions for a user navigating through your application. App's routing features center on progressive enhancement and do not compromise standard browser features (back/forward button support), URLs, or search engine craw-ability.
There are two attributes which are used to configure an app's URLs and how its routing should coordinate with the server:
root
An app's root
is the absolute path from which all routes should be evaluated, i.e. the path at which the app is mounted, usually "/".
See Setting the Root Path in Router's user guide for more information.
serverRouting
Models a higher level concept over the capabilities of, and interactions between the browser and server with respect to URLs and inter-page navigation. This attribute determines how your Y.App
instance will interact with the server: set this to true
if the app can be progressively enhanced, or false
if it's a client side-only app.
Note: The value of an app's html5
attribute depends on the value of its serverRouting
attribute. When serverRouting
is explicit set to false
(not just falsy), the default value for html5
will be set to false
for all browsers. When serverRouting
is true
or undefined
the returned value will be dependent on the browser's capability of using HTML5 history.
Looking for general information about routing? See the Routing section in Router's user guide.
This is the default behavior of how an app's routing will coordinate with the server. When serverRouting
is left undefined
, the app is given no information about the server's capabilities. In this case a balanced approach to URLs, routing, and navigation will be used:
Full-path URLs will only be used if the browser is capable of using HTML5 history, otherwise hash-based URLs will be used.
The Y.App
instance will handle all routing since it is unknown whether the server is capable of properly routing requests.
The server is required to handle full-path URLs which will be generated by browsers which use the HTML5 pushState()
history API. A server can handle these requests by either rendering the initial state in the HTML, or redirecting to a hash-based URL and letting the app dispatch to its route handlers.
This behavior has been chosen as the default because it provides the best path forward for a developer to upgrade a client side-only app to a progressively enhanced app (but you should always prefer to build an app using progressive enhancement from the start, that's the whole point).
The following is a simple Node.js Express server and very basic Y.App
instance which demonstrate how the client and server can coordinate routing and the fulfillment of a request when using URLs that conform to the capabilities of the browser, but the server is "dumb":
var express = require('express'), server = express(); // Handles requests to the root path ("/") by simply sending the "shell" page // which creates the `Y.App` instance. server.get('/', function (req, res) { res.sendfile('index.html'); }); // Handles all other requests by redirecting the browser back to the root path // and tacking on URL's path as the fragment; e.g. "/foo/" => "/#/foo/". server.get('*', function (req, res) { res.redirect('/#' + req.url); }); server.listen(process.env.PORT || 3000);
index.html
- The "shell" page for the Y.App
instance<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Example App</title> </head> <body> <h1></h1> <script src="http://yui.yahooapis.com/3.18.1/build/yui/yui-min.js"></script> <script> YUI().use('app-base', function (Y) { var app = new Y.App({ // Configure the app. }); // Handles requests for the root by updating the page heading. app.route('/', function () { Y.one('h1').set('text', 'Example App - Home'); }); // Handles all other requests by updating the page heading and // displaying current path in it. app.route('*', function (req) { Y.one('h1').set('text', 'Example App - ' + req.path); }); // Make sure to dispatch the current hash-based URL which was set by // the server to our route handlers. app.render().dispatch(); }); </script> </body> </html>
In the above example, the server handles full URLs by sending a redirect back to the root ("/") with a hash-based path and serves a "shell" HTML page to delegate control to a Y.App
instance. Once the app is loaded, it will dispatch the hash-based path to its route handlers, finishing the fulfillment of the request on the client.
You could also implement the server-side redirection logic using an Apache Mod Rewrite .htaccess
file:
RewriteEngine On RewriteRule ^(.+)$ /#/$1 [NE,L]
Full-path URLs are standard URLs which have matching routes based on the URL's path segments. The following are example full-path URLs you might expect to see in a photos application:
http://example.com/ http://example.com/photos/ http://example.com/photos/1 http://example.com/photos/2 http://example.com/albums/ http://example.com/albums/vacation
Setting an app's serverRouting
attribute to true
signals that the server is capable of properly handling requests to all full-path URLs the app can produce. This causes the app to always use full-path URLs, regardless of whether the browser can use HTML5 history or not. In this case the capabilities of the client and server are coordinated in the most optimal way, giving the app the following qualities:
Full-path URLs are always used, older browsers which can not use HTML5 history will have to do full page loads.
The Y.App
instance will only handle routing in browser which support HTML5 history, the other browser's "requests" will be handled completely by the server.
The server is required to handle full-path URLs which will be generated by the app. Ideally the server should render the application's initial state in the response HTML for the given URL.
This is the best option for progressive enhancement! All users of your app will have the same, full-path URLs, independent of the browser or device they are using. Configuring your app with serverRouting
set to true
means that the client and server components of your application are working together to provide the best overall experience for your users.
See Progressively-enhanced vs. Client-only Apps for more information about best practices.
Hash-based URLs have matching routes based on the URL's fragment identifier ("#"). The follow are example hash-based URLs you might expect to see in a photos application:
http://example.com/#/ http://example.com/#/photos/ http://example.com/#/photos/1 http://example.com/#/photos/2 http://example.com/#/albums/ http://example.com/#/albums/vacation
Notice how the above URLs actually all contain the same path segment ("/"), but use the fragment identifier ("#") to hold a pseudo-path value.
Setting an app's serverRouting
attribute to false
signals that the server is not capable of properly handling requests to full-path URLs. This causes the app to always use hash-based URLs, regardless of the browser's capabilities. In this case the app will assume that there is no server to rely on, giving the app the following qualities:
Be aware that all URLs will be hash-based, even in browsers which are capable of using HTML5 history.
The Y.App
instance will handle all routing since there is no server capable of properly routing handling requests.
The server is only expected to serve out a static HTML "shell" page with the required JavaScript to instantiate the Y.App
instance, which will then render the initial state of the application.
This is the best option if there is no guarantee that your app will be served by a capable server. This allows an app to run when hosted on the filesystem or static file server. You should be aware that using hash-based URLs means there will be information loss. The server will not receive the full URL because browsers do not send the fragment-part to the server, that is everything after and including the "#".
Please read Progressively-enhanced vs. Client-only Apps for more information and best practices.
App inherits both its container
attribute and render()
method from View. Unlike View's empty render()
implementation, App provides a default implementation which appends the activeView
(if there is one) to the viewContainer
node which itself is appended to the container
node.
The basic usage of your app's render()
method is to call it at least once, usually after you instantiate your app, this ensures the proper DOM structure is setup to handle rendering the app's views.
var app = new Y.App(); app.render();
This results in the HTML of the page looking like this:
<body class="yui3-app"> ... <div class="yui3-app-views"></div> </body>
By default, an app's container
node will be the <body>
element and its viewContainer
node will be a new <div>
.
Note: The "yui3-app"
and "yui3-app-views"
CSS classes are added to the container
and viewContainer
respectively — this happens when the app is rendered.
When constructing a new App instance you can specify values for the app's container
and viewContainer
attributes, and they can even reference the same node.
var app = new Y.App({ container : '#wrapper', viewContainer: '#wrapper' }); app.render();
Assuming that a <div id="wrapper">
node already exists on the page, this uses a CSS selector string to reference the node, assigns it to both containers, and results in the following HTML:
<div id="wrapper" class="yui3-app yui3-app-views"> ... </div>
If you specify a container
that is not already within the markup of the page, you'll need to manually append it to an element that is:
var app = new Y.App({ container: Y.Node.create('<div id="fancy-app" />') }); app.render().get('container').appendTo('body');
This results in the HTML of the page looking like this:
<body> ... <div id="fancy-app" class="yui3-app"> <div class="yui3-app-views"></div> </div> </body>
Refer to App's API Docs for more details about container
and viewContainer
attributes.
render()
You may override the render()
method to customize how the app renders itself, particularly if you are creating an App subclass.
Note: You should expect that the viewContainer
's contents will be modified by the app for the purpose of rendering the activeView
when it changes; and ideally your render()
method should also return this
at the end to allow chaining, but that's up to you.
The following provides a templates for how you could subclass Y.App
and implement a custom render()
method while still preserving its default behavior:
Y.CustomApp = Y.Base.create('customApp', Y.App, [], { render: function () { // This calls the superclass' (Y.App) implementation of the `render()` // method to preserve the default behavior. Y.CustomApp.superclass.render.apply(this, arguments); // Provide your custom rendering logic here. // Returns this `Y.CustomApp` instance to allow for chaining. return this; } });
Refer to App's API Docs for more details about the render()
method. See Subclassing for more details on extending Y.App
.
A primary feature of App is its flexible view management, which enables you to declare the primary views of your application and easily switch which one is active. This is very handy for defining your app's top-level "page" views, then switching between them as a user navigates through the application.
The views
property of an App class allows you to specify a mapping of view-names to view-metadata that should be registered to your app. This way you can specify information about your app's views — how they should be treated by the app, and how they relate to other views — up-front, in a declarative way that is self-documenting.
You can setup this views mapping on both App subclasses and instances. Every App instance will receive its own copy of a views
object, any views
metadata defined at the class-level will be used as defaults and merged with any views
specified during instantiation time.
The following example shows the creation of an App subclass, CustomApp
, which has a few default views defined, and an instance of CustomApp
which defines another view and overrides some of the defaults.
// Create a Y.CustomApp class that extends Y.App. Y.CustomApp = Y.Base.create('customApp', Y.App, [], { // Default registered views inherited by all CustomApp instances. views: { home : {preserve: true}, users: {preserve: true}, user : {parent: 'users'} } }); // Create a CustomApp instance that inherits the defaults and adds to them. var app = new Y.CustomApp({ // Additional view metadata to be merged with the defaults. views: { home : {preserve: false}, user : {preserve: false}, about: {preserve: true} } });
Using the getViewInfo()
method, we can see how the views
metadata from our CustomView
class and instance were merged together.
// Overwrote "home"'s default `preserve` value. Y.log(app.getViewInfo('home').preserve); // => false // Added `preserve` to "user" view, // and this did not overwrite the default `parent` value. Y.log(app.getViewInfo('user').parent); // => "home" Y.log(app.getViewInfo('user').preserve); // => false // The specified "about" view was registered. Y.log(app.getViewInfo('about').preserve); // => true
See the App Properties section above for more details on what metadata can be stored for each view in views
mapping.
When decomposing an application into discrete user-interfaces, it is natural to think of these as different "pages"—with each one serving a particular role and being the main content on the screen. With the App component, changing the main content/user-interface is done by updating an app's activeView
attribute via the showView()
method.
Working in concert with an app's registered views
, the showView()
method will take a specified view and make it the app's activeView
. This view will be "attached" to the app by rendering it inside the app's viewContainer
and any custom events fired by the view will bubble to the app. Any previously active view will be "detached" from the app, removed from the DOM, and either preserved for later use or properly destroyed.
The following example is the most basic "Hello World" app:
// Creates a new App and View instance. var app = new Y.App(), view = new Y.View(); // Overrides the view's `render()` method to render text into its `container`. view.render = function () { this.get('container').set('text', 'Hello World!'); return this; }; // Renders the `app` and `view`, then sets the view as the app's `activeView`. // We have to explicity tell the app to render the view because by default, if // the view instance already exists it won't be rendered automatically. app.render().showView(view, null, {render: true}); // Verify that `view` is now the `activeView`, and that the view's `container` // is now rendered within the app's `viewContainer`. Y.log(app.get('activeView') === view); // => true Y.log(app.get('viewContainer').contains(view.get('container'))); // => true
This results in the HTML of the page looking like this:
<body class="yui3-app"> ... <div class="yui3-app-views"> <div>Hello World!</div> </div> </body>
This example app can easily become dynamic and have the ability to say hello to someone by name. By creating a reusable HelloView
class the app can dynamically switch between outputting "Hello World!" and "Hello [name]!" where the "name" is a path segment in the URL.
// Creates a HelloView which can say hello to someone named, or to the World // if a name is not specified. Y.HelloView = Y.Base.create('helloView', Y.View, [], { render: function () { var name = this.get('name'); this.get('container').set('text', 'Hello ' + (name || 'World') + '!'); return this; } }); // Creates a new App instance and registers the HelloView. var app = new Y.App({ views: { hello: {type: 'HelloView'} } }); // Adds a route handler for "/" to show the HelloView. app.route('/', function (req) { // Sets the `activeView` to a new instance of a `Y.HelloView` by just // passing "hello", the name of the registered view. this.showView('hello'); }); // Adds a route handler for "/:name" to show the HelloView with a `name`. app.route('/:name', function (req) { // The name which we want to say hello to is specified on `req.params`. var name = req.params.name; // Sets the `activeView` to a new instance of a `Y.HelloView`, but here // we are also passing a config object which the new view instance will // be constructed with, and it contains the name which we'll say hello to. this.showView('hello', {name: name}); }); // Renders the app, then saves a new history entry for "/eric" which will // dispatch the "/:name" route handler. app.render().save('/eric');
This results in the URL being updated to either "/eric"
or "/#/eric"
depending on whether the browser is capable of HTML5 history, and the HTML of the page looking like this:
<body class="yui3-app"> ... <div class="yui3-app-views"> <div>Hello eric!</div> </div> </body>
showView()
Options
App's showView()
method has the following signature:
showView ( view [config] [options] [callback] )
As seen in the previous usage examples, view
can either be a view instance or the string-name of a view defined in the app's views
object. The optional config
object is used when creating a new view instance. The config
object can also be used to update an existing or preserved view
's attributes when options.update
is true
.
The following is the list of options
which affect how the app will switch its activeView
:
callback
Optional callback function to call after new activeView
is ready to use, the function will be passed a reference to the view.
Specifying a callback function is useful if you need access to the view instance which is created as a result of calling showView()
. Also, if an app is using transitions, the callback function will be called once the transitions have completed.
prepend
Whether the view
should be prepended instead of appended to the app's viewContainer
node.
This option is used internally to aid transitions by prepending the new activeView
if it has been defined as the parent
of the currently active view. This puts the view as the first child of the app's viewContainer
node which helps the sliding transitions.
render
Whether the view
should be rendered. Note: If no value is specified, a view instance will only be rendered if it's newly created by this method.
Sometimes you may need greater control over when a view is rendered. Under certain circumstances you may wish to re-render a preserved view, you can use this option to accomplish this.
transition
Optional transition override. A transition can be specified which will override the default, or false
for no transition.
When an app is using transitions, this option allows you to override the default transition that will occur per the app's transitions
configuration — enabling finer-grained control over how views transition.
update
Whether an existing view should have its attributes updated by passing the config
object to its setAttrs()
method. Note: This option does not have an effect if the view
instance is created as a result of calling this method.
This option would usually be used to update a preserved view with new data and can be used in conjunction with the render
option to force a view to update and re-render.
Refer to App's API docs for more details about the showView()
method.
There are certain situations where an app needs to show a view which is not rendered in the browser, instead the content is rendered server side. The App component provides a few features for working with pre-rendered views:
showContent()
Method which provides an easy way to view-ify HTML content which should be shown as an app's active/visible view.
loadContent()
Route middleware which load content from a server. This makes an Ajax request for the requested URL, parses the returned content and puts it on the route's response object.
Y.App.Content.route
A stack of middleware which forms a pjax-style content route. This provides a standard convention which uses loadContent()
and showContent()
to load and show server rendered content as views for an app.
When a person first accesses an app, the initial rendering of the UI might be done by the server; in this situation, when the app "boots up" in the browser, the showContent()
method can be used to view-ify this pre-rendered content and make it the app's activeView
. Using the server to render the initial state of the app in the HTML it sends to the browser is a general best practice. See Progressively-enhanced vs. Client-only Apps for more details.
The following is a basic example of using showContent()
during the initialization of an app:
Assume the page contains the following HTML:
... <body> <div id="wrapper"> <div class="view"> <h1>Some Page</h1> <p>With some content.</p> </div> </div> </body> ...
The Y.App
instance can be setup to use this initial content as its activeView
:
var app = new Y.App(); // Renders the app and shows the content which matches the specified selector. app.render().showContent('#wrapper > .view'); Y.log(Y.one('#wrapper > .view') === app.get('activeView').get('container')); // => true
When calling the showContent()
method here, a new Y.View
instance will be created and the <div class="view">
node will be its container
; and we can see this by checking the app's activeView
.
The showContent()
method becomes more powerful when it's called with options
. The following is a list of options in addition to any showView()
options:
view
The name of a view defined in this app's views
, or an object with the following properties:
name
The name of a view defined in this app's views
.
config
Optional configuration to use when creating the new view instance. This config object can also be used to update an existing or preserved view's attributes when options.update
is true
. Note: If a container
is specified, it will be overridden by the content specified in the first argument of showContent()
.
Let's look at a more interesting example which specifies a view
name registered with the app:
Assume the page contains the following HTML:
... <body> <div id="wrapper"> <div class="view" data-view="foo"> <h1>Foo Page</h1> <p>With some content.</p> <p> <button>Close</button> </p> </div> </div> </body> ...
This time, the Y.App
instance will be setup to use a Y.FooView
with the initial content as its activeView
:
Y.FooView = Y.Base.create('foo', Y.View, [], { events: { 'button': {click: 'handleButtonClick'} }, handleButtonClick: function (e) { alert('You click the button, and alerts are annoying.'); } }); var app, content; app = new Y.App({ // The example assumes there's support for rendering on the server. That's // the whole point with progressive enhancement. serverRouting: true, views: { foo: {type: Y.FooView} } }); content = Y.one('#wrapper > .view'); // Renders the app and shows the `content` node _and_ specifies which view // should be created. app.render().showContent(content, {view: content.getData('view')}); Y.log(app.get('activeView') instanceof Y.FooView); // => true
Refer to App's API Docs for more details about the showContent()
method.
Another common use of server rendered views is static content pages. While the interesting parts of an app are using models, views, and templates to render content dynamically in the browser, the app might also contain static content pages; e.g. "About Us". Instead of giving the client side of the app the smarts to render this type of static content, Y.App.Content.route
—which is a stack of middleware that forms a pjax-style content route—can be used. This route uses a convention of loading HTML from the server via an XHR, and making that content the app's activeView
by calling into the showContent()
method.
The following example uses both the Y.App.Content.route
for the "/about/"
page, and the loadContent()
middleware for the "/about/people/"
, allowing for more control over which view is used:
Y.PeopleView = Y.Base.create('people', Y.View, [], { events: { '.person': {click: 'showPersonOverlay'} }, showPersonOverlay: function (e) { // Show an overlay with the all info about the person... } }); var app = new Y.App({ views: { people: {type: Y.PeopleView} } }); // Uses the convenient content route to load the "/about/" pjax-style. app.route('/about/', Y.App.Content.route); // Uses the `loadContent()` middleware to load the HTML from the server, but // specifies a custom route callback to control which view is created. app.route('/about/people/', 'loadContent', function (req, res, next) { Y.config.doc.title = res.content.title; this.showContent(res.content.node, {view: 'people'}); }); // Render the app and dispatch to the route handlers. app.render().dispatch();
Refer to App's API Docs for more details about the loadContent()
middleware, and the Y.App.Content.route
route.
A key feature of Y.App
is its robust URL navigation management. You can simply use standard HTML links within your app and when the user clicks a link, Y.App
will handle it if there’s a matching route for that URL. With the navigation-related config options you have full control over your app's navigation behavior and experience, plus your app will automatically use the best available navigation method based on these settings, the browser’s capabilities, and the actions of the user.
This enables an app to fulfill a "page request" by using data it already has stored in models or loading new data, then composing and showing a view which represents the app's current state for this URL — all without requiring a full page load.
Beyond handling navigation via link clicks, Y.App
also exposes a programmatic way to navigate the user though your app via the navigate()
method.
App's navigate()
method implements a higher level concept of "browsing" over the save()
and replace()
methods. It will manage a user's navigation history like a browser, and is the recommended method to use when programmatically navigating the user to URLs within your app.
The navigate()
method will do the right thing (what the browser would do) when navigating the user to the same URL they are currently on — it will replace the history entry — or to an in-page fragment identifier — which only when configured will navigate. The following demonstrates some of these behaviors:
var app = new Y.App(); app.route('*', function (req, res, next) { // Handle all URLs. }); // Save a new history entry for "/", or replaces the current entry if we're // already on "/". app.navigate('/'); // => true // Does not navigate even though there's a matching route handler. app.navigate('#top'); // => false // Enable navigation on hash-only changes to the URL. app.set('navigateOnHash', true); // Does navigate because `navigateOnHash` was enabled. app.navigate('#top'); // => true
Refer to App's API Docs for more details about the navigate()
method.
The navigation features of Y.App
are built on the base pjax functionality. This is what enables users to navigate to the different sections or "pages" of an app while avoiding full page loads.
The following are configuration attributes which define an app’s navigation behavior:
Attribute | Defined in | Default Value | Description |
---|---|---|---|
linkSelector |
Y.PjaxBase |
"a" |
CSS selector string used to filter link click events so that only the links which match it will have the enhanced-navigation behavior of pjax applied.
When a link is clicked and that link matches this selector, navigating to the link's By default this selector will match all links on the page. |
navigateOnHash |
Y.PjaxBase |
false |
Whether navigating to a hash-fragment identifier on the current page should be enhanced and cause the
By default Pjax allows the browser to perform its default action when a user is navigating within a page by clicking in-page links (e.g. |
scrollToTop |
Y.PjaxBase |
true |
Whether the page should be scrolled to the top after navigating to a URL. When the user clicks the browser's back button, the previous scroll position will be maintained. |
serverRouting |
Y.App.Base |
undefined |
Whether or not this application's server is capable of properly routing all requests and rendering the initial state in the HTML responses. See App Attributes and Routing Coordination with Server for more details. |
navigate
Event
When the user is navigating to a URL for which the app has a route handler, the navigate
event will fire. The default action of this event updates the browser’s address bar to reflect the new URL, causing the app to dispatch to the matching route handlers.
Listening to your app's navigate
event is a useful way to indicate to the user that something is loading while they wait for the app to fully handle the new URL, possibly loading data from a remote server.
var app = new Y.App({ views: { users: {} }, users: new Y.ModelList() }); app.route('/users/', function () { var users = app.get('users'); // Load users data from a remote server. users.load(function () { app.showView('users', {users: users}); // Removes the "loading" class from the app's `container` node. app.get('container').removeClass('loading'); }); }); // Listen for the app's `navigate` event. app.on('navigate', function (e) { // Adds the "loading" class to the app's `container` node. app.get('container').addClass('loading'); }); // Navigate the user to "/users/". app.navigate('/users/');
Refer to App's API Docs for more details about the navigate
event.
Y.App
The Y.App
class is intended to be mutable insofar as being the host for all App-component features, whereas Y.App.Base
is intended to remain pure and just host the basic set of features. This allows for two different ways to extend the functionality of Y.App
: mixing-in features, and subclassing.
Additional class extensions can be automatically mixed-into Y.App
, doing so will dynamically extend the functionality of the App class making these new features available to all of its instances and subclasses. The Y.App.Content
and Y.App.Transitions
class extensions, provided by the app-content
and app-transitions
modules respectively, use this pattern to add features Y.App
.
The following shows a simplified view of how the transitions feature is added to Y.App
:
// Creates the namespace for the transitions class extension and assigns a // simple constructor function to it. Y.App.Transitions = function () {}; // Defines the `transitions` attribute with a default value of `false`. This // makes the view transitions an opt-in feature. Y.App.Transitions.ATTRS = { transitions: {value: false} }; // This defines the prototype of the transitions class extension, the actual // implementation has been left out for the sake of simplicity. Y.App.Transitions.prototype = { // The implementation of the transition features would be here. }; // Here the transitions class extension is being applied automatically by // mixing itself into the `Y.App` class. Y.Base.mix(Y.App, [Y.App.Transitions]);
When this module is included in the YUI instance, the transitions support for App can be used by simply toggling it on:
YUI().use('app-base', 'app-transitions', function (Y) { var app = new Y.App({transitions: true}); });
When writing your own class extensions to add features to Y.App
, feel free to add them to App's namespace (e.g., Y.App.SomeNewFeature
), and be sure to follow these two rules:
The additional functionality should be disabled by default. The API for the class extensions should provide some way for the user to opt-in to using the features it adds.
The Y.App.Transitions
class extension does this be requiring a "truthy" value for the transitions
attribute. By default the value will be false
, making this an opt-in feature.
Be courteous to the other App component class extensions, since they all share the same prototype
be careful not to unintentionally overwrite other properties or methods.
Creating class extensions for mixing-in features to Y.App
is a great way to extend its functionality in a reusable way while not changing how you Instantiate an App; but might find it more useful to extend the Y.App
class to create a subclass customized to your specific needs.
Use the Y.Base.create()
method to extend Y.App
and add or override prototype and static members and attributes. You may also optionally specify one or more class extensions to mix into your new class.
// Create a Y.CustomApp class that extends Y.App. Y.CustomApp = Y.Base.create('customApp', Y.App, [], { // Add or override prototype properties and methods here. }, { // Add static properties and methods here. ATTRS: { // Add or override default attributes here. } });
One benefit of extending Y.App
is that you can easily add default views, routes, and route handlers to your custom App class, and they'll be shared by all instances of that class unless overridden at the instance level:
// Create a Y.CustomApp class that extends Y.App. Y.CustomApp = Y.Base.create('customApp', Y.App, [], { // Default registered views inherited by all CustomApp instances. views: { home : {preserve: true}, users: {preserve: true}, user : {parent: 'users'} }, // Default route handlers inherited by all CustomApp instances. handleHome: function (req) { // Handle the "/" route here. }, handleUsers: function (req) { // Handle the "/users/" route here. }, handleUser: function (req) { // Handle the "/users/:name/" route here. } }, { ATTRS: { // Share these routes with all CustomApp instances. routes: { value: [ {path: '/', callbacks: 'handleHome'}, {path: '/users/', callbacks: 'handleUsers'}, {path: '/users/:name/', callbacks: 'handleUser'} ] } } }); // Create a CustomApp instance that inherits the defaults and adds to them. var app = new Y.CustomApp({ // Register an additional view. The `home`, `users`, and `user` views will // also be inherited. views: { about: {preserve: true} } }); // Add a route and route handler. app.route('/about/', function (req) { // Handle the "/about/" route here. });
Now all instances of Y.CustomApp
will inherit all the custom defaults and can add to or override them. The app
instance created here will handle the "/"
, "/users/"
, and "/users/:name/"
routes in addition to its own "/about/"
route.
Before you subclass Y.App
, you should refer to App's API docs to become familiar with its public and protected properties and methods.
Y.App.Content
The Content extension provides pjax-style content fetching and handling. This makes it easy to fetch server rendered content for URLs using Ajax. The HTML content returned from the server will be view-ified and set as the app's main content, making it seamless to use a mixture of server and client rendered views.
Y.App.Transitions
The Transitions extension provides view transitions for apps in browsers which support native CSS3 transitions. View transitions visually enhance the change from one "page" to the next that is both pleasant to the user and helps to communicate a hierarchy between sections of an application.
Enabling transitions for an app just requires opting-in. The following will create a new Y.App
instance with the default transitions enabled:
var app = new Y.App({transitions: true});
With transitions enabled for an app, anytime the app's activeView
changes, there will be a visual transition from the old to the new active view. How an app's views
are configured effects which transition will be used, e.g. when changing between views which have a hierarchical relationship a sliding transition will be used.
activeView
Changes
The following are the types of activeView
changes for which transitions can be defined that correspond to the relationship between the new and old activeView
:
navigate
The default transition to use when changing the activeView
of the application. This will be "fade"
by default.
toChild
The transition to use when the new activeView
is configured as a child of the previously active view via its parent
property as defined in this app's views
. This will be "slideLeft"
by default.
toParent
The transition to use when the new activeView
is configured as the parent
of the previously active view as defined in this app's views
. This will be "slideRight"
by default.
The default transitions can be overridden in the following ways:
Overriding the transitions
property which is used as the default transitions for all Y.App
instances.
Specifying a transitions
configuration object when instantiating a new app, which sets the app's transitions
attribute.
Specifying a transition
when calling the showView()
method.
The following example will override which transitions should be used by default, and specifically set the transition
option on certain calls to showView()
.
var app = new Y.App({ // The app's three views and their relationships between one another. views: { home : {}, about : {}, contact: {parent: 'about'}, team : {parent: 'about'} }, // Overrides the default transitions types all to "fade" which will // cross-fade between `activeView` changes. transitions: { navigate: 'fade', toChild : 'fade', toParent: 'fade' }, // The app's `team` model list for use by the "team" view. team: new Y.ModelList().reset([ {name: 'Eric Ferraiuolo'}, {name: 'Ryan Grove'} ]) }); app.route('/', function () { // Will transition via "fade". app.showView('home'); }); app.route('/about/', function () { // Will transition via "fade", even though we maybe coming from a child // view (i.e. "contact" or "team"). app.showView('about'); }); app.route('/about/contact/', function () { // Will transition via "fade", even though we maybe coming from the parent // "about" view. app.showView('contact'); }); app.route('/about/team/', function () { // Shows the "team" view, passing it the app's `team` model list, and // overrides the `transition` options so no visual transitions will occur. app.showView('team', {team: this.get('team')}, {transition: false}); }); app.render().dispatch();
Some structural CSS is required to setup an app's container
and viewContainer
nodes so the app's views transition properly. While an app's views are transitioning, the CSS class: yui3-app-transitioning
will be added to the app's container
node.
Note: While transitioning, the app's viewContainer
node will have its overflow-x
set to hidden
. This causes its margins to not collapse with its child nodes. To compensate for this, it is best to not style your views with margins.
It's important to understand the difference between the preferred approach of developing an app using a progressive enhancement strategy vs. creating a client side-only app, and when it might be appropriate to do so. As discussed in the Routing Coordination with Server, Y.App
provides the tools and is flexible enough to implement an app using either strategy, but there are drawbacks to client-only applications.
The following sections build on Router's Best Practices, all of which apply, by looking at things from a higher level, application perspective:
Before making a decision to use hash-based URLs in your application, you should be aware of their drawbacks.
A client-only app will use hash-based URLs instead of the standard, full-path URLs. When the server receives a request for a hash-based URL everything after and including the fragment identifier ("#") is lost. This means your application will have to rely on the JavaScript to finish fulfilling the request for the user.
The following shows how this information loss manifests itself:
Browser URL: http://example.com/#/foo/bar/ Request URL: http://example.com/
The request URL that the server will see is missing the entire path, this means every URL in your application looks to the server as if it's the same page! To prevent link rot in your application, you'll have to support the hash-based variation of your URLs indefinitely.
Ideally you should prefer full-path URLs (e.g., http://example.com/foo/bar/) for your application and be able to handle full page loads for browsers which are not capable of using HTML5 history. This requires a server capable of routing requests and rendering the initial state of the application in the HTML responses. This is the main idea behind a progressive enhancement development strategy — providing a better experience for more capable browsers/devices.
Client-only apps suffer in more places than just their URLs. There are horrible side-effects of using hash-based URLs: degradation in performance of rendering the app's initial state, and lack of SEO.
It is faster to render the initial state of an app on the server before delegating control to the client-side JavaScript. But in order to achieve this, the server must be capable of routing requests and rendering this initial state. This means your application needs to be using full-path URLs.
To render the initial state in a client-only app, the server has to send over the HTML "shell" page, which then makes more HTTP requests to get the JavaScript. Once the JavaScript has been downloaded and parsed, it can execute and construct the Y.App
instance. The app now needs to fetch data from the server (yet another HTTP request), and then it can display the initial state. All while the user is staring at a blank page — which is a horrible user experience, especially on a mobile device with a slower connection.
Optimizing your app to be indexed by search engines also relates to this. SEO requires that your app uses full-path URLs so the server can route requests and render the initial state. The basic functions of your app which you want to be indexable should work without JavaScript.
Given all the drawbacks of client-only apps, there's one major advantage: they don't rely on the server. If your app needs to run off the filesystem, or be deployed to a "dumb" static file server, then this might be your only option.
Developing a client-only app using Y.App
still gives you nice (but hash-based) URLs that are bookmarkable, browser back/forward button support, and view management.
Please make sure you understand the issues with hash-based URLs and their side-effects. Consider developing your application using a progressive enhancement strategy, it will be worth the extra effort in the long run.
When developing an app, it's important to have a clear understanding about how the various components are working together. The use of consistent patterns for information sharing, and execution of operations aid in maintaining a separation of concerns which becomes ever more crucial as the size of the app and team increases.
Restricting the flow of information to a single direction creates a "one-way street" which enforces a strict separation of concerns and divides control over execution throughout an application. This strategy can be implemented by leveraging YUI's powerful custom event infrastructure. All of the components of App Framework are Y.Base
-based, and therefore are primed to be both publishers and subscribers of an app's custom events.
The following example app demonstrates how to wire up the components of an application using custom events to achieve the one-way flow of information and divide control by maintaining a strict separation of concerns:
// Create the `UserView`. Y.UserView = Y.Base.create('userView', Y.View, [], { template: '<p>{name}</p>' + '<label for="name-input">Name:</label>' + '<input id="name-input" type="text" value="{name}" />' + '<button>Save</button>', // Listens for DOM events from the user and translates them into application // events; e.g., "click" -> "save". events: { 'button': {click: 'save'} }, initializer: function () { // Listens for the user model to change, and re-renders the view. this.get('user').after('change', this.render, this); }, render: function () { var user = this.get('user'), content = Y.Lang.sub(this.template, {name: user.get('name')}); this.get('container').setHTML(content); return this; }, // This view doesn't actually do the saving, instead, it is translating the // user's intent (they click the Save button) to an application-level event. // This way control is given to the app to respond to this action. save: function () { var user = this.get('user'), name = this.get('container').one('input').get('value'); // Fires the save event and passes along the needed information. When // this is the app's `activeView` this event will bubble to the app. this.fire('save', { user: user, name: name }); } }); // Create the app and define the app's `views`, and assign a new `Model` // instance as an ad-hoc attribute to the app. var app = new Y.App({ views: { user: { type : 'UserView', preserve: true } }, user: new Y.Model() }); // The app listens for the `UserView` to fire its `save` event. The only will // only be notified if the view is the app's current `activeView`. app.on('userView:save', function (e) { // The app has the control to execute update the model and execute the save // operation. Updating the model will cause the view to re-render. e.user.set('name', e.name).save(); }); // Renders the app and set's the `user` view and the `activeView`, passing it // the user model. app.render().showView('user', {user: app.get('user')});
The main thing to note about the above example is that the UserView
is not updating its user
model directly, instead it is translating the user's intent via DOM events into application-level events. This gives control back to the app so it can decide how best to proceed. The result is a flow of information and control that moves in one direction and in a continuous loop.
An app's views
can be defined so that a single instance of a particular view is preserved and can be reused. When a view is defined with its preserve
property set to true
, the same view instance will be used each time the app's showView()
method is called to set it as the app's activeView
.
Preserved views should be used in situations where the contents of the view might not change and that view will be displayed often. They are especially useful if its contents have a complex DOM structure that would be costly to create each time the view was to be shown.
Imagine a simple app that displays user information in two views: a users
view that lists all the users, and a user
view that displays the details of a single user. If the set of all users in this application won't change often, then it would be a good idea to make the users
view a preserved view. That way it only has to be constructed once, and the same view instance can be shown many times. Whereas the contents of the user
view are always going to change depending on which user is being displayed, making it not suitable to preserve.
The following example demonstrates how this hypothetical users app would define its two views:
// Create the `UsersView`. Y.UsersView = Y.Base.create('usersView', Y.View, [], { // Implementation would be here... }); // Create the `UserView`. Y.UserView = Y.Base.create('userView', Y.View, [], { // Implementation would be here... }); // Create the app and define its two views. var app = new Y.App({ views: { // Only one `users` view instance will be created and it will be // preserved for reuse. users: { preserve: true, type : 'UsersView' }, // A new `user` view instance will be created every time. user: { type : 'UserView', parent: 'users' } } });
Be cautious about the number of preserved views you have in your application. Their DOM structures are maintained even when they are not in the visible portion of the page's DOM, this means the more preserved views, the more memory your application will consume. On mobile devices with very limited memory and no swap space, consuming too much memory in your app can cause the browser to crash.
When multiple App instances are on the page, only one can manage the URL and navigation. All of the app instances can have route handlers, but only one should have its linkSelector
attribute set to a non-falsy value. See Configuring Navigation Behavior for more details.
Transitions are only supported in WebKit and Firefox browsers which also support CSS3 transitions. It is intended to only support transitions in browser which have native CSS3 transitions, and YUI's Transition component only supports WebKit and Firefox.
App relies heavily on Router which has some known limitations in Internet Explorer 9 and lower, and Android 2.x. See Router's Known Limitations for more details.