ƒ is a component framework for JavaScript applications that promotes code-reuse and frees you from writing boilerplate code.
ƒ relies heavily on Backbone and jQuery, and is a perfect companion for your templating language of choice.
Let's avoid the buzzword game and explain we we're talking about.
Models are a local representation of an object or database entry. ƒ uses vanilla Backbone models. Templates are HTML structure and template code. Templates control the presentation of your models on the page. ƒ uses vanilla Handlebars templates. Views render a model with a given template, putting your data into your HTML structure. Views can be shown or hidden. ƒ uses vanilla Backbone views. Components are logical units of application code. They have methods, models, views, and sub-components. A component consists of many sub-components that it connects together by listening to events from one component and calling methods on another.Models
Templates
Views
Components
ƒ uses PseudoClass, an OOP framework that lets you create building blocks that you can extend, mixin, and re-use.
You can use new Class(descriptor) to create a class based on the descriptor object you pass to it.
Just call new Class(descriptor) with a descriptor that describes your class.
var Widget = new Class({
toString: 'Widget',
construct: function() {
this.$el = $(document.createElement(this.tag));
},
destruct: function() {
this.$el.remove();
},
tag: 'div',
show: function() {
this.$el.show();
},
hide: function() {
this.$el.hide();
}
});
Set descriptor.extend to the class you would like to inherit from.
To override a method, simply define the method in the prototype of the child class.
var Panel = new Class({
toString: 'Panel',
extend: Widget,
show: function() {
this.$el.fadeIn();
}
});
Alternatively, you can use the static extend method of any class:
var Panel = Widget.extend({
toString: 'Panel',
show: function() {
this.$el.fadeIn();
}
});
Add _super as the first argument to the child class' method, then call it using _super.call(this, 'arg1') or _super.apply(this, arguments).
var Widget = new Class({
toString: 'Button',
extend: Widget,
tag: 'button',
hide: function(_super) {
_super.apply(this, arguments);
this.$el.attr('disabled', true);
}
});
For more information, view the JSDoc for Class.
<script src="js/F.js"></script>
var App = {};
ƒ provides F.ModelComponent, which encapsulates some of the common boilerplate code you'd otherwise write when dealing with models.
App.Models.Item = Backbone.Model.extend({
urlRoot: 'api/items'
});
App.Templates.ItemDetails = Handlebars.compile(
'<h2>{{name}}</h2>'+
'<p><a href="mailto:{{email}}">{{email}}</a></p>'+
'{{#if phone}}<p>{{phone}}</p>{{/if}}'
);
App.ItemDetails = new Class({
toString: 'ItemDetails',
extend: F.ModelComponent,
construct: function(options) {
// Let users specify options; extend defaults
// so users can specify a new template or model.
options = _.extend({
model: this.Model,
template: this.Template
}, options);
// Create a view
this.view = new this.View(options);
},
// The model we'll be fetching and displaying
Model: App.Models.Item,
View: F.View.extend({
events: {
'click .sendMessage': 'doSend'
}
}),
Template: App.Templates.ItemDetails
});
// Generally, you'll add the editor as a component
// For this example, we'll just create an instance
app.details = new App.ItemDetails();
// Display the model with the ID=10, fetching it from the server first
app.details.show({ id: 10 });
// If you already have the model, call show and pass it directly
var tempModel = new App.Models.Item({
name: 'Larry Davis',
email: '[email protected]'
});
app.details.show({
model: tempModel
});
See the Contacts example for a live demo.
ƒ provides F.FormComponent, which handles fetching models from the server, rendering the corresponding edit form, and asynchronously saving changes.
App.Models.Item = Backbone.Model.extend({
urlRoot: 'api/items'
});
App.Templates.ItemEditor = Handlebars.compile(
'<label>Name</label>'+
'<input type="text" name="name" value="{{name}}">'+
'<label>E-mail</label>'+
'<input type="text" name="email" value="{{email}}">'+
'<label>Phone</label>'+
'<input type="text" name="phone" value="{{phone}}">'+
'<button type="submit">Save</button>'
);
App.ItemEditor = new Class({
toString: 'ItemEditor',
extend: F.FormComponent,
// Indicate the model you'll be loading and saving
Model: App.Models.Item,
Template: App.Templates.ItemEditor
});
// Generally, you'll add the editor as a component
// For this example, we'll just create an instance
app.editor = new App.ItemEditor();
// Do something on a successful save
app.editor.on('saved', function() {
alert('Changes saved!');
});
// Do something when the save fails
app.editor.on('saveFailed', function() {
alert('ERROR: Failed to save changes!');
});
// Edit the model with the ID=10, fetching it from the server first
app.editor.show({ id: 10 });
// If you already have the model, call show and pass it directly
var tempModel = new App.Models.Item({
name: 'Larry Davis',
email: '[email protected]'
});
app.editor.show({
model: tempModel
});
See the Contacts example for a live demo.
ƒ provides F.ListComponent, which handles fetching a collection of models from the server and rendering the corresponding list.
App.Models.Item = Backbone.Model.extend({
urlRoot: 'api/items'
});
App.Collections.Items = Backbone.Model.extend({
url: 'api/items',
model: App.Models.Item
});
App.Templates.ListItem = Handlebars.compile(
'<strong>{{name}}</strong>'
);
App.ItemList = new Class({
toString: 'ItemList',
extend: F.ListComponent,
// Indicate the collection you'll be loading and rendering
Collection: App.Models.Item,
ItemTemplate: App.Templates.ListItem
});
// Generally, you'll add the editor as a component
// For this example, we'll just create an instance
app.list = new App.ItemList();
// Do something when an item is clicked
app.list.on('itemSelected', function() {
alert('Changes saved!');
});
// Show the list. show() will call load() if necessary
app.list.show();
// Generally, you'll add the editor as a component
// For this example, we'll just create an instance
app.list = new App.ItemList();
// Do something when an item is clicked
app.list.on('itemSelected', function() {
alert('Changes saved!');
});
// Load the list with custom parameters
app.list.load({
query: 'shoes'
});
See the Contacts example for a live demo.
Coming soon.
Coming soon.
Coming soon.
Designing for one use case and moving on is easy the first time, but when you're writing the same boilerplate code for every part of your project, it's time to go back to the drawing board.
That block of code you keep pasting into every component should really be part of a parent class from which you inherit.
App.Hustler = new Class({
toString: 'Hustler',
extend: F.Component,
construct: function(options) {
this.hustle();
},
hustle: function() {
// This line of code is boilerplate;
// that is, we have it in all related components
alert("I'm a "+this.toString());
}
});
App.HustlerBaby = new Class({
toString: 'HustlerBaby',
// Inherit from Hustler so we get hustle()
extend: App.Hustler,
hustle: function(_super) {
// Instead of re-writing the boilerplate code,
// call the parent class' hustle method
_super.apply(this, arguments);
// Do additional stuff
alert('I just want you to know');
}
});
Declare your models, views, and templates in the prototype of the components you create. This lets child classes easily override them.
When you have many items that all need to handle the same event, instead of adding one listener for each item, add a listener to the parent container. This helps performance when your list has thousands of items.
But how do I know which item got the click? Use evt.target to determine the DOM node that got the click. If that node is associated with some data (a model, for instance), then use $(node).data('model', theModel) to store and retrieve the model. F.ListComponent will store the model in the node's data for you.
App.PhotoGrid = new Class({
toString: 'PhotoGrid',
extend: F.ListComponent,
Collection: Backbone.Collection.extend(),
ItemTemplate: Handlebars.compile(
'<img src="{{url}}">'
),
// Extend ListComponent, override its events
View: F.ListComponent.prototype.ListView.extend({
tag: 'ul',
className: 'photoGrid',
events: {
'mouseover li': 'handleMouseover'
}
}),
handleMouseover: function(evt) {
var target = $(evt.target);
var model = target.data('model');
console.log(model.name+' has the mouse over it!');
}
});
Yes, ƒ is a valid JavaScript variable name, and you use it instead of F if you're super hip.