Views are one of the most important concepts in Famo.us. Once you move beyond the fundamentals of Famo.us and start constructing more complex applications, views will be an indispensible tool for code organization and encapsulation.
Conceptually, a view is an just an object comprised of multiple renderables. A view might manage a collection of surfaces, or even a collection of other views, each of which manages its own internal renderable components. In Famo.us, you will use views to encapsulate the components of your application.
More concretely, a view is an object which inherits from the View
prototype. In OOP terms, you might think of the View
prototype as a sort of abstract class — a class only used for subclassing. The View
prototype provides few things that just about every component in a complex Famo.us application will need:
RenderNode
interface, meaning any view can be added directly to the render tree.Here’s the boilerplate code you’ll use when creating your own custom view that inherits from View
. (It’s up to you to add functionality on top of the boilerplate.)
var View = famous.core.View;
function MyView() {
View.apply(this, arguments);
}
MyView.prototype = Object.create(View.prototype);
MyView.prototype.constructor = MyView;
MyView.DEFAULT_OPTIONS = {};
module.exports = MyView;
As you build out your app, it’s good practice to separate each view out into its own file. For basic applications, we recommend placing all of these views into a views
folder:
myapp/
views/
AppView.js
FooterView.js
HeaderView.js
In this example, we’ll create two views, each with a surface, and add them both to a GridLayout
. Each view, when added to a grid cell, inherits the size of the cell; their surfaces will be centered by a modifier.
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
var Engine = famous.core.Engine;
var Surface = famous.core.Surface;
var Modifier = famous.core.Modifier;
var HeaderFooterLayout = famous.views.HeaderFooterLayout;
var View = famous.core.View;
var GridLayout = famous.views.GridLayout;
var mainContext = Engine.createContext();
var layout;
createLayout();
addHeader();
addFooter();
addContent();
function createLayout() {
layout = new HeaderFooterLayout({
headerSize: 100,
footerSize: 50
});
mainContext.add(layout);
}
function addHeader() {
layout.header.add(new Surface({
content: "Header",
properties: {
backgroundColor: 'gray',
lineHeight: "100px",
textAlign: "center"
}
}));
}
function addFooter() {
layout.footer.add(new Surface({
content: "Footer",
properties: {
backgroundColor: 'gray',
lineHeight: "50px",
textAlign: "center"
}
}));
}
function addContent() {
var grid = new GridLayout({
dimensions: [2, 1]
});
var views = [];
grid.sequenceFrom(views);
for(var i = 0; i < 2; i++) {
var view = new View();
var centerModifier = new Modifier({
origin: [0.5, 0.5],
align: [0.5, 0.5]
});
var surface = new Surface({
content: 'content' + (i + 1),
size: [100, 100],
properties: {
backgroundColor: '#fa5c4f',
color: 'white',
textAlign: 'center',
lineHeight: '100px'
}
});
view.add(centerModifier).add(surface);
views.push(view);
}
layout.content.add(grid);
}
Let’s explore one way to position a surface within a view. First, we create a view that expands to the width of our parent context — but which has a specified height. Inside the view, we’ll include a “background surface” that fills the size of the view, and another “tab surface” that is centered vertically within the view.
Instead of adding the surfaces to the view itself, we’ll add them to a reference node. In this way, the background surface expands to the view size, while the tab surface is centered vertically within the view.
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
var Engine = famous.core.Engine;
var View = famous.core.View;
var Surface = famous.core.Surface;
var Transform = famous.core.Transform;
var StateModifier = famous.modifiers.StateModifier;
var mainContext = Engine.createContext();
var viewSize = [undefined, 100];
// adding a view
var view = new View();
var bottomModifier = new StateModifier({
origin: [0, 1],
align: [0, 1]
});
mainContext.add(bottomModifier).add(view);
// creating a reference node in the view with size
var sizeModifier = new StateModifier({
size: viewSize
});
var sizeNode = view.add(sizeModifier);
// creating a background surface in the view
var background = new Surface({
properties: {
backgroundColor: 'gray',
}
});
var backModifier = new StateModifier({
// positions the background behind the tab surface
transform: Transform.behind
});
// adding to reference node to get parent size
sizeNode.add(backModifier).add(background);
// create a tab on the left
var tab = new Surface({
content: 'Feedback',
size: [150, 50],
properties: {
fontSize: '1.5em',
lineHeight: '50px',
textAlign: 'center',
backgroundColor: '#fa5c4f',
}
});
var tabOriginModifier = new StateModifier({
origin: [0, 0.5],
align: [0, 0.5]
});
sizeNode.add(tabOriginModifier).add(tab);
When views contain just a single renderable, the view can simply defer to the size of the item when it comes to defining its size. When multiple renderables are involved, life can become very complicated. There are two general approaches to solving this problem.
If you add a size option to the view, specifying a precise height, then the layout will will be able to arrange its renderables properly. E.g.:
view.setOptions({
size: [undefined, 300]
});
You could also override the .getSize()
method of the view. E.g.:
view.getSize = function() {
return [undefined, 300];
};
It is generally better to not override functions if you don’t need to, but there are cases where defining your own .getSize()
function can be very beneficial.
Every view instance has event handlers set up for you out of the box, allowing your modules to publish and subscribe to events as necessary. Using events with views is a great way to keep your application modular and your components reusable.
View instances have two “private” properties, ._eventInput
and ._eventOutput
, which can be used to subscribe to, and publish events, respectively. Here’s a snippet that illustrates how they can be used:
var view = new MyView();
view._eventInput.on('incoming-event', function() {
// Handle the 'incoming-event' here
});
// These are equivalent to `view._eventInput.on`:
view.on('incoming-event', function(){});
view.addListener('incoming-event', function(){});
view._eventOutput.emit('outgoing-event', 'message');
In most cases, you’d want to treat these properties as private to the module, only accessing them from the inside:
function MyView() {
View.apply(this);
this._eventInput.on('...', function(){});
}
MyView.prototype.emitReady() {
this._eventOutput.emit('ready');
}
Views also have .pipe()
/.unpipe()
and .subscribe()
/.unsubscribe()
methods that you can use to channel events from one view to another.
view.pipe(otherView)
means “send my events to the other view’s input”view.subscribe(otherView)
means “send events from the other view to me”A common use of pipe
/subscribe
might be to channel events from a view’s interior renderables up to the view itself, and then outward to other modules in the application. Here’s a quick live example to demonstrate one way we might do it:
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
var Engine = famous.core.Engine;
var View = famous.core.View;
var Surface = famous.core.Surface;
var context = Engine.createContext();
function MyView() {
View.apply(this, arguments);
this.surface = new Surface({
size: [100, 100],
content: 'I belong to a view. Click me!',
properties: {
cursor: 'pointer',
textAlign: 'center',
padding: '1em',
backgroundColor: '#fa5c4f'
}
});
this.add(this.surface);
this.surface.pipe(this);
this._eventInput.on('click', function() {
this._eventOutput.emit('surface-clicked');
}.bind(this));
}
MyView.prototype = Object.create(View.prototype);
MyView.prototype.constructor = MyView;
MyView.DEFAULT_OPTIONS = {};
var myView = new MyView();
myView.on('surface-clicked', function() {
myView.surface.setProperties({ backgroundColor: 'red' });
});
context.add(myView);
Copyright © 2013-2015 Famous Industries, Inc.