-
Notifications
You must be signed in to change notification settings - Fork 7
Building a SPA Website with Alchemy Angular
This article will show you how to use the alchemy-angular gem with AngularJS to build a one page web site.
Angular doesn't need an introduction anymore. Google's frontend JS framework
is a huge success and ubiquitous.
Alchemy is a content management framework written in Ruby On Rails and is used by hundreds of websites around the globe. It gives you, the developer, the power
to build your customer exactly the cms they need.
Never tried it? Try it!
Let's Go!
$ gem install alchemy_cms --pre
$ alchemy new angular-demo
We are using bower-rails
to handle frontend js dependencies. Add it to your Gemfile
:
# Gemfile
...
gem 'alchemy_cms', github: 'magiclabs/alchemy_cms', branch: 'master'
gem 'alchemy-angular', github: 'magiclabs/alchemy-angular', branch: 'master'
gem 'bower-rails', group: 'development'
...
- Note: Since we want to use the new
/api
namespaced JSON API urls
from Alchemy we need to specify themaster
branch in theGemfile
Also we add the alchemy-angular
gem, that provides directives, controllers
and routes for a seamless Alchemy integration.
Don't forget to install the dependencies with bundler:
$ bundle install
Now let's add Angular and some useful modules into your Bowerfile
:
# Bowerfile
asset 'angular'
asset 'angular-route'
asset 'angular-animate'
asset 'angular-sanitize'
asset 'underscore'
And install them with:
$ bin/rake bower:install
- Notice how bower-rails installs all dependencies into
vendor/assets/bower_components
. It is highly recommended to
add these to your git repository. Rails will then kindly compile
them into the asset pipeline for you. Great!
We need to define some elements and page layouts in Alchemy in order to
manage some content. ( If you are not familiar with page layouts and elements, I
highly recommend to read the Guideline ).
Let's keep it short and simple. An article element and one basic page layout
is enough for the purpose of this demo.
In your config/alchemy/elements.yml
file add:
# config/alchemy/elements.yml
- name: article
contents:
- name: headline
type: EssenceText
- name: color
type: EssenceSelect
default: black
- name: logo
type: EssencePicture
- name: text
type: EssenceRichtext
And in your config/alchemy/page_layouts.yml file:
# config/alchemy/page_layouts.yml
- name: standard
elements: [article]
Like I said, short and simple.
Use Alchemy's view generator for elements and page layouts.
$ bin/rails g alchemy:page_layouts --skip
$ bin/rails g alchemy:elements --skip
But, since we are not going to render the views via Rails, we can safely remove
the generated element views and add some Angular templates instead.
$ rm app/views/alchemy/elements/_article_view.html.erb
As we use the angular-rails-templates
gem, we put the Angular templates into app/assets/javascripts/angular/templates
inside an alchemy/elements
folder.
The template file itself is named like the element. (You see, we follow
common Alchemy naming patterns here. Naming over configuration)
So, the full path to our template is:
app/assets/javascripts/angular/templates/alchemy/elements/article.html
and here is how it looks like:
<article class="{{ingredient('color').value}}">
<h1 ng-if="ingredient_present('headline')">{{ingredient('headline').value}}</h1>
<img ng-if="ingredient_present('logo')" ng-src="{{ingredient('logo').value}}" class="logo">
<div ng-bind-html="ingredient('text').value" class="text"></div>
</article>
You see we already use a function called ingredient in the views.
Also we use a useful helper function to check to see if the Angular
should render the dom
element or not.
Where does it all come from? It is provided by the
alchemyElement directive from the alchemy-angular
gem.
# app/assets/javascripts/alchemy-angular/directives/alchemy_element.js.coffee
App.directive 'alchemyElement', ['$sce', ($sce) ->
{
restrict: 'E'
$scope: {
element: "="
}
replace: true
link: (scope, element, attrs) ->
# Every element has its very own template.
# The elements are placed at your angular template path
# with 'alchemy/elements' prefix
scope.elementTemplate = "alchemy/elements/#{scope.element.name}.html"
# The ingredients of current element
scope.ingredients = scope.element.ingredients
# Returns all ingredients with given name
scope.ingredients_with_name = (name) ->
_.where(scope.ingredients, {name: name})
# Returns the ingredient value of given name
scope.ingredient = (name) ->
_.findWhere(scope.ingredients, {name: name})
# Checks if the ingredient with given name is not null or empty
scope.ingredient_present = (name) ->
ingredient = scope.ingredient(name)
return false unless ingredient
ingredient.value && ingredient.value != ''
return
template: '<div ng-include="elementTemplate"></div>'
}
]
- Note: You should only need to pass in
$sce
for Strict Contextual Escaping
if you are using AngularJS < v.1.2
So, wait. What happens here? Angular does not really support using dynamic templates from current scope. So we need to use this ng-include
trick to make dynamic template support work.
What we are basically doing here is to get the current element's name and include the template into the template wrapper.
Ok, and how do we use this directive and how does it get the data?
First of all we need a controller, that uses a service to load the data from Alchemy's JSON API.
The Pages Controller
The controller simply takes the params provided by the router and fetches the data via the AlchemyPage service from the Alchemy JSON API.
The controller also sets some values on the $scope
and $rootScope
. If a Google Analytics tracker is present it also tracks the page hit.
App.controller('AlchemyPagesController',
['$rootScope', '$scope', '$routeParams', 'AlchemyPage',
($rootScope, $scope, $routeParams, AlchemyPage) ->
# Track a google analytics page hit
trackGAPageHit = (url, title) ->
window.ga 'send',
'hitType': 'pageview',
'page': url,
'title': title
# Load the page via service
#
request = AlchemyPage.load($routeParams)
# Sets these values on success:
#
# $rootScope.title
# $rootScope.metaKeywords
# $rootScope.metaDescription
# $scope.elements
# $scope.cells
# $scope.pageLayout
#
request.success (response) ->
# if google analytics object is present we track the page hit
if window.ga?
trackGAPageHit("/#{$routeParams.page}", response.title)
$rootScope.title = response.title
$rootScope.metaKeywords = response.meta_keywords
$rootScope.metaDescription = response.meta_description
$scope.elements = response.elements
$scope.cells = response.cells
$scope.pageLayout = response.page_layout
return
return
]
)
The Page service
App.service 'AlchemyPage', ['$http', ($http) ->
url_for = (params) ->
if params.id
# To make this work in Alchemy preview frame we need to load the page from id
"/api/admin/pages/#{params.id}.json"
else if params.page
# The url from page param
"/api/pages/#{params.page}.json"
else
throw new Error("No params to load page from found! You need to provide an object with either `id` or `page` attribute.")
# Loads the page from Alchemy's JSON API via given params and returns a promise
#
@load = (params) ->
$http.get url_for(params)
return
]
And the router:
App.config ['$routeProvider', '$locationProvider', ($routeProvider, $locationProvider) ->
$locationProvider.html5Mode(true)
$routeProvider
# Route for displaying this page in Alchemy page preview
.when '/admin/pages/:id',
controller: 'AlchemyPagesController'
templateUrl: 'alchemy/page.html'
# Route for displaying page
.when '/:page*',
controller: 'AlchemyPagesController'
templateUrl: 'alchemy/page.html'
# Route not found -> 404
.otherwise
templateUrl: '/404.html'
return
]
Now we need to wire everything together.
In your application.html.erb
template you only need to use the ng-view
directive on a container div and the everything else is handled for you.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<base href="/">
<meta name="fragment" content="!">
<%= stylesheet_link_tag :application, media: 'all' %>
<%= javascript_include_tag :application %>
</head>
<body>
<div ng-view class="container"></div>
<footer>
<%= render_navigation %>
</footer>
</body>
</html>
Pretty straight forward. We can even use the navigation renderer from Alchemy, because Angular catches the requests and loads the content we need from our API.
That's it! A nice one page webpage managed through a CMS!