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.
Getting Started
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.5.0/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.
Components of the App Framework
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 Component
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 craw-ability. The Routing Coordination with Server and Progressively-Enhanced Apps sections of this guide contain details and best practices 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 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.
Using App
Instantiating App
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.
Config Properties
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 theprototype
. EveryY.App
instance gets its own copy of aviews
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'} } });
App Properties
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 Attributes
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:
|
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. |
Extending 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.
Mixing-In Features
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.Transitions
class extension, provided by the app-transitions
module, uses this pattern to add transitions support to Y.App
.
// Creates the namespace for the transitions class extension and assigns a // simple constructor function to it. Y.App.Transitions = function () {}; // 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 addition 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 thetransitions
property of the config object passed-in during instance creation. -
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.
Subclassing
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: '/', callback: 'handleHome'}, {path: '/users/', callback: 'handleUsers'} {path: '/users/:name/', callback: '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.
Routing Coordination with Server
Rendering an App
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.
Specifying Container Nodes
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.
Overriding 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.
View Management
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.
Declaring Views
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 related 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 follow 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.
Switching the Active View
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`. app.render().showView(view.render()); // 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>
Refer to App's API docs for more details about the showView()
method.
Navigating Between "Pages"
A key feature of Y.App
is its powerful 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 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.
Programmatic navigation
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.
Configuring Navigation Behavior
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.
Best Practices
Progressively-Enhanced Apps
Client-Only Apps
App Extensions
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
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.
Types of 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 itsparent
property as defined in this app'sviews
. This will be"slideLeft"
by default. toParent
-
The transition to use when the new
activeView
is configured as theparent
of the previously active view as defined in this app'sviews
. This will be"slideRight"
by default.
Overriding Default Transitions
The default transitions can be overridden in the following ways:
-
Overriding the
transitions
property which is used as the default transitions for allY.App
instances. -
Specifying a
transitions
configuration object when instantiating a new app, which sets the app'stransitions
attribute. -
Specifying a
transition
when calling theshowView()
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();
Transition-Aiding CSS
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.