• Stars
    star
    336
  • Rank 121,250 (Top 3 %)
  • Language
    JavaScript
  • Created almost 14 years ago
  • Updated almost 13 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A lightweight MVC framework for building fast, tidy javascript web apps

eyeballs.js

eyeballs.js is a slim javascript library designed to sit on top of a javascript framework, such as jQuery or Prototype.

The goals are:

  • Organisation of client-side web app code using the Model-View-Controller pattern.
  • Simple model implementation for handling non event-related concerns.
  • Simple routing layer for hash-tag change based navigation, preserving the back-button
  • Rapid development of javascript apps using strong conventions.
  • Easing the pain of building fast, responsive interfaces.
  • Exploring the possibilities of offline web apps.

The implementation is owes a lot to Ruby on Rails, but also attempts to be idiomatic javascript.

Overview

eyeballs.js can sit on top of an already implemented web app with a well thought out object model. It can also be used to build standalone javascript apps, backed by HTML5 local storage or something like CouchDB.

eyeballs.js models are not necessarily one-to-one mapped to server side models using your web framework of choice. They more closely map to interface elements, but there aren't really any rules as yet.

Finally, eyeballs.js is still a bit of an experiment. It's a quick implementation of a crazy idea to help make javascript code a little bit more organised.

eyeballs.js is supposed to be both agnostic and modular. The code is broken down into modules, drivers and adapters.

Modules add various parts of functionality, for example the code that powers the individual model, controller, routing and validation layers. Drivers add support for underlying javascript frameworks. Features that rely on event handling etc. are part of driver logic. Adapters provide an API to various persistence layers, eg. HTML5 Local Storage, a REST interface or a CouchDB instance.

Getting Started

eyeballs.js is packaged into modules, according to dependencies.

The main library has no dependencies and lets you use eyeballs.js models standalone.

Standalone, eyeballs.js doesn't do much: it provides the o_O() function for initializing models and some validations.

At a very minimum, you should choose an adapter. There are a few to choose from:

  • o_O.dom - a basic adapter without any persistence.
  • o_O.localstorage - persist records to HTML5 local storage.
  • o_O.couchdb - persist records to a local CouchDB instance, for building MVC CouchApps, for example.
  • o_O.rest - An adapter for persisting models to a backend powered by Rails, or using Rails-style RESTful routing.

Finally, you need a controller. The first release of eyeballs.js includes a controller as part of the jQuery driver.

You can also use a javascript templating language. Mustache.js fits this need quite nicely.

Wrapping that all up, to use eyeballs.js with the Rails adapter and jQuery:

<!-- jQuery and livequery -->
<script src="vendor/javascripts/jquery.1.4.min.js"></script>
<script src="vendor/jquery/jquery.ba-bbq.min.js"></script>

<!-- Mustache for templating -->
<script src="vendor/mustache/mustache.0.3.0.js"></script>

<!-- eyeballs.js basic -->
<script src="vendor/eyeballs/o_O.js"></script>
<script src="vendor/eyeballs/modules/o_O.model.js"></script>
<script src="vendor/eyeballs/modules/o_O.validations.js"></script>

<!-- eyeballs.js jquery driver for controller logic -->
<script src="vendor/eyeballs/drivers/jquery/modules/o_O.controller.js"></script>
<script src="vendor/eyeballs/drivers/jquery/modules/o_O.support.js"></script>
<script src="vendor/eyeballs/drivers/jquery/modules/o_O.routes.js"></script>

<!-- REST adapter -->
<script src="vendor/eyeballs/drivers/jquery/adapters/o_O.rest.js"></script>

<!-- Configuration -->
<script src="config/initializer.js"></script>
<script src="config/routes.js"></script>

Badabing, badaboom! You're now ready to start creating some models and controllers.

Generators

If you install the eyeballs.js Ruby gem, you can use the eyeballs command to generate eyeballs.js apps, models and controllers:

gem install eyeballs

To create a new eyeballs.js app:

eyeballs my_new_app

This will create a new app in the my_new_app folder.

When you're up and running, you can use the model and controller generators:

eyeballs generate model Post
eyeballs generate controller Posts

These generators will install files to app/models and app/controllers relative to where you run the eyeballs command.

You can also generate a scaffold:

eyeballs generate scaffold Post

This will generate a posts.html, a post.js and a posts_controller.js.

If the generator detects a "public" directory when you run it, it will install into public/javascripts.

Models

You define a model by passing a name and function to the eyeballs ( o_O ) function (pronounced 'eep eep'). As inspired by Rails, model definitions are capitalised. Note, however, that the new prefix is not used.

o_O('Post', function(){ })

You can now initialize an individual post:

var post = Post.initialize({title: 'My New Post'});
post.title //=> 'My New Post'

Not very exciting.

However, if you're familiar with Rails, you'll be familiar with the wonderful syntax for adding validations to your models. eyeballs.js lets you add validations to your models as follows:

o_O('Post', function(){
  this.validates_presence_of('title')
})

Now, when you initialize a new Post, you can validate it, nice and easy:

var post = Post.initialize()
post.valid()
post.errors   // => [{
              //     field: 'title',
              //     message: 'title should be present',
              //     type: 'presence'}]

and so on, so forth. This will be very familiar to those who use Rails.

You can also add your own validations, again, similar to how Rails does things:

o_O('Post', function(){
  this.validates(function(post){
    if(post.title != 'Awesome')
    {
      post.errors.push({message: 'Not Awesome Enough'})
    }
  })
})

var post = Post.initialize()
post.save(function(saved_post){
  post.errors   // => [{
                //     message: 'title should be present'}]
})              // yep, there's a save method too!

Even better, using the invalid callback:

post.save({
  invalid: function(saved_post){
    // you can assume:
    saved_post.errors 
  }
})

When you want to find things:

Post.find({
  loading: function(){
    console.log("I'm loading...")
  },
  success: function(post){
    console.log("here I am")
  }
})

And if you want to add your own methods:

o_O('Post', function(){
  this.methods.title_downcased: function(){
    this.title.toLowerCase();
  }
})

var post = Post.initialize({title: "HUGE"})
post.title_downcased()  //=> 'huge'

Connecting to an adapter:

If you want to hook eyeballs.js up to the local storage adapter for example:

o_O.model.adapter = o_O.localstorage

Finding, saving, updating and deleting. With callbacks? Easy peasy:

post = Post.initialize({title: 'My new post'})
post.save({
  invalid: function(post){
    alert('Sorry, invalid!');
  },
  loading: function(post){
    alert('I hapeen straight away');
  },
  success: function(post){
    alert('Saved, whoop!');
  }
})

There's a strong emphasis on callbacks: since any persisting to backends should be done asynchronously.

Controllers

An eyeballs.js controller is also initialized with the eyeballs function, by passing a string name and an object containing the controller actions.

o_O('PostsController', {
  new: function(){
    alert("I'm new");
  },
  create: function(){
    alert("I'm create");
  }
})

Again, this looks nice and familiar. Dead, dead simple.

Calling Controller Actions & Binding Events

There are several ways to bind events to controller actions.

Calling Directly

The simplest way to call controller actions is to bind them directly. From the above example, you can simply call:

o_O('PostsController').new()

...once you have initialized your controller.

Routing

You can use the eyeballs.js router to bind events to changes in the URL hash. This is particularly effective for graceful degradation, as well as preserving the back button history.

Your config/routes.js file would look something like this:

o_O.routes.draw(function(map){
  map.match('/posts/new/', {to: 'posts#new'})
})

You can now bind this to particular links, by adding the data-ajax-history attribute to your a elements:

<a href="/posts/new" data-ajax-history="true">Click Me!</a>

This link will now call PostsController.new() when it is clicked.

You can also set params in the URL, eg:

o_O.routes.draw(function(map){
  map.match('/posts/:id/', {to: 'posts#show'})
})

<a href="/posts/1">Click Me for Post 1!</a>

or in the route:

o_O.routes.draw(function(map){
  map.match('/posts/:id/', {to: 'posts#show', 'custom':'posts'})
})

o_O.params('custom') #=> 'post'

and your controller:

o_O('PostsController', {
  show: function(){
    alert(o_O.params('id')) //=> '1'
  }
})

If you want a default action to fire, that is when the document.hash is empty, just hook up a map.root:

o_O.routes.draw(function(map){
  map.root({to: 'posts#index'})
})

If you have several routes that share the same prefix, you can use a namespace:

o_O.routes.draw(function(map){
  map.namespace('my', function(){
    map.match('posts/new', {to: "myposts#new"}) # hooks up to MypostsController.new()
  })
})

Tasty!

Binding actions to events

To bind events to these controller actions, use the data-bind attribute:

<a href="/posts/new" data-bind="posts#new">Click me!</a>

This binds all clicks on this element to the new action on the PostsController. By default, if you add these attributes to a form, the action is bound to the submit event; to all other elements it binds to a click.

It also returns false, canceling out the default behavior. If you want the default behavior, prefix with + to "add" the action to the propagation chain:

<a href="/posts/new" data-bind="+posts#new">Click me!</a>

You can also bind to custom events:

<a href="/posts/new" data-bind="+mouseover:posts#new">Hover over me!</a>

You can bind multiple events and actions to a single element:

<a href="/posts/new" data-bind="mouseover:posts#preview; click: posts/new">Hover first, then Click me!</a>

It's called "obtrusive UJS" ... explicit, yet everything has its own place.

Putting it all together

Imagine a simple app for posting reviews. It will comprise a "Review" model, "ReviewsController" and associated views.

models/review.js looks like this:

o_O('Review', function(){
  this.validates_presence_of('title');
  this.validates_presence_of('content');
});

This defines the Review model, allowing us to initialize and save Review objects, while ensuring title and content are included.

The create action in controllers/reviews_controller.js looks like this (using jQuery):

...
create: function(){
  var review = Review.initialize(o_O.params('review'));
  var form = $(this);
  review.save({o
    invalid: function(review){
      o_O.alert_errors(review);
    },
    success: function(review){
      o_O.render('reviews/_review', review, {prepend: 'div#reviews'});
      form.find('input[type=text], textarea').val('');
    }
  })
}
...

The form that hooks up to this action is like this:

<form data-bind="reviews#create">
  <label for="review-title">Title</label><br />
  <input type="text" name="title" value="" data-attribute="title"><br />
  <label for="review-content">Review</label><br />
  <textarea name="content" data-attribute="content"></textarea><br />
  <input type="submit" name="commit" value="Save">
</form>

The main things to note here are the way that the form binds automatically to the create action (using jQuery event delegation). Also, field elements have the "data-attribute" attributes ... the o_O.params() function reads from these, returning a JSON object that can be passed to Review.initialize(...).

Notice also o_O.alert_errors(...) which displays an alert of all the errors on an invalid review.

Finally, the o_O.render function takes a template, which is a Mustache.js template stored in views/, the review object and a set of options.

Sample App

Meview is a sample app for storing reviews: Demo: http://meview.heroku.com Code: https://github.com/paulca/meview

Running the tests

eyeballs.js uses QUnit and a Sinatra app for in-browser testing.

To start the test server:

ruby app.rb

To run all the tests, visit:

http://localhost:4567/test/run_unit_tests.html

Contributors

About me

I'm Paul Campbell. I'm an avid web developer. Follow my ramblings at http://www.pabcas.com

Follow me on Twitter http://twitter.com/paulca

Copyright (c) 2010 Paul Campbell, released under the MIT license

More Repositories

1

whenever.js

A nice little library for writing and implementing web application behavior in javascript that looks like English.
JavaScript
284
star
2

configurable_engine

A Rails gem for storing app configuration data in your database, with a config file to fall back on.
Ruby
116
star
3

paths_of_glory

An achievement system generator for Rails
Ruby
79
star
4

stuffing

A Rails plugin to read and write CouchDB documents via Activerecord
Ruby
53
star
5

behavior

A library for storing Rails app configuration in the database
Ruby
20
star
6

twitter2campfire

Publish a Summize Twitter search feed to Campfire
Ruby
20
star
7

paulconf

19
star
8

realex

A Ruby class for interfacing with www.realexpayments.com
Ruby
19
star
9

shortcode-url

A really simple Rails plugin to generate a unique code on a field for TinyURL style URLS
Ruby
14
star
10

merge_translation

A plugin to merge YAML translation files in Rails 2.2
Ruby
12
star
11

url_field

A simple ActiveRecord plugin to correctly format a URL in the database whether the user enters "http://" or not
Ruby
12
star
12

shakespeare

A simple drop-in CMS for Rails
Ruby
10
star
13

formatted-dates

A simple acts_as style plugin for easily formatting of dates within Rails models
Ruby
10
star
14

address_engine

Ruby
9
star
15

phone_codes

International phone dialling codes for options_for_select
Ruby
8
star
16

town-crier

Sends a message to Twitter every hour.
Ruby
6
star
17

cukemin

A Rails Admin Controller / View Scaffold Generator that eschews specs in favour of cukes and is cut exactly to my liking.
Ruby
6
star
18

ruby-ireland-23-sept-2014

5
star
19

can_has

A simple ActiveRecord plugin to add a lovely little can_view, can_edit, can_delete permissions system in simple use cases
5
star
20

meview

A sample eyeballs.js localstorage web app
JavaScript
5
star
21

neofuturists_presenter

Ruby
4
star
22

big_decimal_price

BigDecimal.new('10.9').to_s #=> '10.90'
Ruby
4
star
23

kalipso

Ruby
4
star
24

couchy

A simple HTML template -> CouchDB document translator
Ruby
4
star
25

design-article-site

HTML/CSS to go along with an article I wrote for dotMobi
JavaScript
4
star
26

feelings

A tumble-log written in Rails using CouchDB as a data-store
Ruby
4
star
27

audit

A Rails plugin to save changes to an ActiveRecord model
Ruby
3
star
28

exceptional-php

PHP Client for geteceptional.com
PHP
3
star
29

ruby-ireland-schedule-app

Ruby
3
star
30

eyeballs_rails

Rails helpers for eyeballs.js
Ruby
2
star
31

couchpop

Upload a directory to CouchDB
Ruby
2
star
32

whenever.js-lightning-talk

a lightning talk about http://github.com/paulca/whenever.js
Ruby
2
star
33

paul-is-awesome

Ruby
2
star
34

paulca.github.com

2
star
35

blog-example

Ruby
1
star
36

ruby-ireland-schedule-api

Ruby
1
star
37

uninitialized_constant_example

Example of Rails 3 bug
Ruby
1
star
38

jaysus

Local / Remote persistence for JSON APIs / Local Store
Ruby
1
star
39

rails31rubyireland

Presentation to Ruby Ireland on August 30 for Rails 3.1 release party!
Ruby
1
star
40

db_context

Simple Rails plugin for switching context to a different database
Ruby
1
star
41

ubimport

Ruby
1
star
42

sanity-nuxt-events

Events with Nuxt.js
Vue
1
star
43

ucd-rails-intro

Ruby
1
star