XHIVE

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

Build Status Code Quality

How it works

xhive converts your controller actions or cells into AJAX widgets.

It leverages the power of Liquid 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

rake xhive:install:migrations
rake db:migrate

Include the xhive javascript loader in your head tag.

<%= javascript_include_tag "xhive/loader" %>

Include the custom stylesheets in your head tag.

<%= include_custom_stylesheets %>

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

<%= 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

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

app/views/posts/show.html.erb

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

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

config/routes.rb

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

Just tell xhive to widgify your action:

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 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

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

app/cells/posts/show.html.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

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:

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:

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

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

Create a Page

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

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:

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

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

If you want to map the page to a specific post

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

From your posts controller, render the posts page

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:

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:

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

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

You get:

<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.

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.

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:

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:

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

And you add the key to the mapper creation step:

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:

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:

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