frozeek/xhive

View on GitHub
README.md

Summary

Maintainability
Test Coverage
XHIVE
======

xhive is a gem built for turning your Rails application into an AJAXified CMS.

[![Build Status](https://secure.travis-ci.org/frozeek/xhive.png)](http://travis-ci.org/frozeek/xhive)
[![Code Quality](https://codeclimate.com/badge.png)](https://codeclimate.com/github/frozeek/xhive)

# How it works

xhive converts your controller actions or [cells](https://github.com/apotonick/cells) into AJAX widgets.

It leverages the power of [Liquid](http://liquidmarkup.org/) creating a custom Liquid::Tag for every
widget, so it can be called from within any HTML template.

xhive also gives you the foundation of a CMS providing the following models:

* Site
* Page
* Stylesheet
* Image

Using this models along with the xhive widgets you will be able to build a fully functional CMS.

# Installation

Add xhive to your Gemfile

`gem 'xhive'`

Run bundle install

`bundle install`

Run xhive migrations

```bash
rake xhive:install:migrations
rake db:migrate
```

Include the xhive javascript loader in your head tag.

```erb
<%= javascript_include_tag "xhive/loader" %>
```

Include the custom stylesheets in your head tag.

```erb
<%= include_custom_stylesheets %>
```

Include the widgets loader just before your \<\\body\> tag.

```erb
<%= initialize_widgets_loader %>
```

# Usage

## Widgify

### Turning your controller actions into widgets

Let's say you have a Posts controller and you want to access the show action as a widget.

`app/controller/posts_controller.rb`

```ruby
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
  end
end
```

`app/views/posts/show.html.erb`

```erb
<h1><%= @post.title %></h1>

<p><%= @post.body %></p>
```

`config/routes.rb`

```ruby
resources :posts, :only => [:show]
```

Just tell xhive to *widgify* your action:

```ruby
class PostsController < ApplicationController
  widgify :show

  def show
    @post = Post.find(params[:id])
  end
end
```

And that's it. You will now be able to insert the content of any post from within an HTML template using:

`{% posts_show id:1234 %}`

This tag will make the browser insert the post content asynchronously into the HTML document.

xhive will also enforce the tag to include the :id parameter.

### Using [cells](https://github.com/apotonick/cells) as reusable widgets

Let's use the same example to illustrate the use of cells with xhive.

We have a Posts cell and we want to use the show method as an AJAX widget.

`app/cells/posts_cell.rb`

```ruby
class PostsCell < Cell::Rails
  def show(params)
    @post = params[:id]
    render
  end
end
```

`app/cells/posts/show.html.erb`

```erb
<div class='post'>
  <h1><%= @post.title %></h1>

  <p><%= @post.body %></p>
</div>
```

In this case, we need to tell xhive how we are mounting our widgets routes:

`config/initializers/xhive.rb`

```ruby
Xhive::Router::Cells.draw do |router|
  router.mount 'posts/:id', :to => 'posts#show'
end
```

And that's it. You will now be able to insert the content of any post from within an HTML template using:

`{% posts_show id:1234 %}`

This tag will make the browser insert the post content asynchronously into the HTML document.

xhive will also enforce the tag to include the :id parameter.

You can customize the tag invocation name by using the :as symbolized option:

```ruby
Xhive::Router::Cells.draw do |router|
  router.mount 'posts/:id', :to => 'posts#show', :as => 'show_post'
end
```
Then you can insert the tag using the following snippet:

`{% show_post id:1234 %}`

You can also force the cell widget to be rendered inline instead of using AJAX.

Just include the :inline symbolized option:

```ruby
Xhive::Router::Cells.draw do |router|
  router.mount 'posts/:id', :to => 'posts#show', :as => 'show_post', :inline => true
end
```

This is also useful when using stylesheet tags inside email pages.

Caveat: the inline feature only works for Cell:Base cells.

## CMS features

Ok, I can include my cells and controller actions as widgets, but... how?

xhive provides you with some basic CMS infrastructure.

### Creating your first dynamic page

To be able to use your widgets, you have to follow the following steps:

Create a Site

```ruby
site = Xhive::Site.create(:name => 'My awesome blog', :domain => 'localhost')
```

Create a Page

```ruby
page = Xhive::Page.create(:name => 'home',
                          :title => 'My blog page',
                          :content => '<h1>Home</h1><p>{% posts_show id:1 %}</p>',
                          :site => site)
```

Start the server

Now you can access the page on http://localhost:3000/pages/home.

This should display the post with id: 1 inside the home page.

### Adding pages to your own custom data

You can also use the xhive pages from within you own data.

xhive provides the Xhive::Mapper to wire up your resources to xhive pages.

Create a new page to display all the posts

```ruby
posts_page = Xhive::Page.create(:name => 'posts',
                                :title => 'Blog Posts',
                                :content => '{% for post in posts %}{% posts_show id:post.id %}{% endfor %}',
                                :site => site)
```

Create a new stylesheet to display your posts:

```ruby
stylesheet = Xhive::Stylesheet.create(:name => 'Posts', 
                                      :content => '.post {
                                                     h1 { font-size: 20px; color: blue; }
                                                     p { font-size: 12px; color: #000; }
                                                   }',
                                      :site => site)
```

Create a new mapper record for the posts resources

```ruby
mapper = Xhive::Mapper.map_resource(site, posts_page, 'posts', 'index')
```

If you want to map the page to a specific post

```ruby
mapper = Xhive::Mapper.map_resource(site, posts_page, 'posts', 'show', post.id)
```

From your posts controller, render the posts page

```ruby
class PostsController < ApplicationController
  # This will render the page associated with the index action
  def index
    @posts = Post.limit(10)
    render_page_with :posts => @posts
  end

  # This will render the page associated with the specific post
  def show
    @post = Post.find(params[:id])
    render_page_with @post.id, :post => @post
  end
end
```

Using this feature you can let the designers implement the HTML/CSS to display the posts in your site without your intervention.

## Page mounting

You can also add pages to your ActiveRecord model using the *mount_page* statement:

```ruby
class Post < ActiveRecord::Base
  mount_page :mini
  mount_page :full
end

mini_page = Xhive::Page.create(:name => 'mini-post', :title => 'Minimized Post', :content => "<a href='/posts/{{post.id}}'>{{post.title}}</a>")
full_page = Xhive::Page.create(:name => 'post', :title => 'Post', :content => "<h1>{{post.title}}</h1><p>{{post.body}}</p>")

post = Post.create(:title => "My awesome post", :body => "This is an awesome post!")
post.mini = mini_page
post.full = full_page
```

Then you can display the pages in a view:

```ruby
<% # Render minimized content %>
<%= post.mini_page_content %>

<% # Render full content %>
<%= post.full_page_content %>
```

You get:

```html
<a href='/posts/1'>My awesome post</a>

<h1>My awesome post</h1><p>This is an awesome post!</p>
```

The post object gets injected into the page render method so you can use all its attributes inside the page.

Caveat: the mount_page statement currently supports single-site use. Multi-site support is intented to be added in the near future.

## Policy based mapping

If you need more customization in the page mapping process, you can pass a policy class name as the last attribute.

```ruby
class MyPolicyClass
  def call(opts={})
    opts[:user].country == 'US' && opts[:user].age > 18
  end
end

mapper = Xhive::Mapper.map_resource(site, posts_page, 'posts', 'show', post.id, 'MyPolicyClass')

# It will only use the page if the user is an adult from the US
render_page_with @post.id, :post => @post, :user => @user

```
Note: the mailer instance variables will be passed along to the policy class. This allows you to customize the email templates
depending on the user properties. See below for ActionMailer integration.

## ActionMailer integration

Using xhive you can extend the CMS capabilities to your system generated emails.

```ruby
class Notifications < ActionMailer::Base
  def welcome(site, user)
    @user = user
    @link = root_url

    mailer = Xhive::Mailer.new(site, self)
    mailer.send :to => user.email, :subject => 'Welcome!'
  end
end
```
In order to use this, you must create a mapper for this specific email action:

```ruby
mapper = site.mappers.new(:resource => 'notifications', :action => 'welcome')
mapper.page = my_awesome_email_page
mapper.save
```

You can use your instance variables from within the dynamic page:

```
<p>Dear {{user.first_name}}</p>

<p>Welcome to our awesome site</p>

<p>Click <a href='{{link}}'>here</a> to start!</p>

```

If you want to use different pages for different, e.g. user categories, you can pass the user category to the mailer initializer:

```ruby
mailer = Xhive::Mailer.new(site, self, user.category)
```

And you add the key to the mapper creation step:

```ruby
mapper = site.mappers.new(:resource => 'notifications', :action => 'welcome', :key => 'spanish')
mapper.page = email_for_spanish_users
mapper.save
```

Note: the page title will be used as the email subject. You can also make use of the instance variables
inside the page title as is treated as a Liquid template string.

### Inline stylesheets for your emails

If you add the inline widget to your cell routes you can use inline stylesheets within your email pages:

```ruby
Xhive::Router::Cells.draw do |router|
  router.mount 'stylesheet/:id', :to => 'xhive/stylesheet#inline', :inline => true, :as => :inline_stylesheet
end
```

Then you can add your stylesheet into your email page using the corresponding tag:

`{% inline_stylesheet id:spain_users_stylesheet %}`

This will create a `<style>` tag inside your email page and inject all the style rules.

### Inline pages for your emails

If you add the inline widget to your cell routes you can use inline pages within your email pages:

```ruby
Xhive::Router::Cells.draw do |router|
  router.mount 'page/:id', :to => 'xhive/page#inline', :inline => true, :as => :inline_page
end
```

Then you can add your inline page into your email page using the corresponding tag:

`{% inline_page id:email_header %}`

## TODO

* Remove as many dependencies as possible
* Improve test coverage

## Disclaimer

This is a work in progress and still a proof of concept. Use at your own risk.

Please let me know of any problems, ideas, improvements, etc.

## Special Thanks

* Thanks to [Daniel Cadenas](https://github.com/dcadenas) for the Policy class inspiration.