XHIVE
xhive is a gem built for turning your Rails application into an AJAXified CMS.
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
- Thanks to Daniel Cadenas for the Policy class inspiration.