Skip to content


Folders and files

Last commit message
Last commit date

Latest commit


Repository files navigation CircleCI


A module for creating coupled modules of CSS, Javascript and Views in Phoenix.


Add the following to the dependencies in mix.exs:

{:ex_cell, "~> 0.0.14"}

In Phoenix 1.3.0+ add the following to lib/app_web/web.ex:

def controller do
  quote do

  import ExCell.Controller


def view(opts \\ []) do
  quote do

    import ExCell.View


def cell(opts \\ []) do
  quote do
    use ExCell.Cell, namespace: AppWeb,
                     adapter: ExCell.Adapters.CellJS

    use Phoenix.View, root: "lib/app_web/cells",
                      path: ExCell.View.relative_path(__MODULE__, AppWeb)

    import Phoenix.Controller,
           only: [get_csrf_token: 0, get_flash: 2, view_module: 1]

    use Phoenix.HTML

    import AppWeb.Router.Helpers
    import AppWeb.Gettext

    # Add everything you want to use in the cells

Now you can add a cells/ directory in lib/app_web and place cells in that directory.

Every cell should contain a view.ex and a template.html.eex. The view and template are tightly linked together by the Cell.


To ensure all the CSS can be placed next to your cell you need to add the following to your brunch-config.js:

stylesheets: {
  joinTo: {
    "css/app.css": [

If you use something other than Brunch to manage your assets, you need to add the files to the assets manager of choice.


If you wish to use the accompanying cell-js library you can install it with your package manager. After you installed the Javascript package, add the following to your brunch-config.js:

javascripts: {
  joinTo: {
    "js/vendor.js": /^node_modules/,
    "js/app.js": [


A cell consists of a couple of files:

|- avatar
|  |- template.html.eex
|  |- view.ex
|  |- style.css (optional)
|  |- index.js (optional)
|- header

You can render the cell in a view, controller or another cell by adding the following code:

cell(AvatarCell, class: "CustomClassName", user: %User{})

This would generate the following HTML when you render the cell:

<span class="AvatarCell" data-cell="AvatarCell" data-cell-params="{}">
  <img src="/images/foo/avatar.jpg" class="AvatarCell-Image" alt="foo" />


Views of cells behave like normal views in Phoenix, except that they have provide a container method that can be used in a template to render the appropriate HTML needed to initialize the Javascript for a cell and have a predefined class that is the same as the cell name minus the namespace.

# lib/app_web/cell/avatar/view.ex
defmodule AppWeb.AvatarCell do
  @moduledoc """
  The avatar cell used to render the user avatars.

  use AppWeb, :cell
  alias App.Accounts.Avatar

  def class_names(assigns) do
    [assigns[:class], class_name(assigns[:size])]
    |> Enum.reject(fn(v) -> is_nil(v) end)

  def avatar_image_path(user) do
    Avatar.url({user.avatar, user}, :thumb)

  def avatar_image_alt(user) do
    [user.first_name, user.last_name]
    |> Enum.join(" ")
    |> String.trim()


The template behave like any other template in Phoenix except that they have access to a container method to render the appropriate cell HTML container:

<!-- lib/app_web/cell/avatar/template.html.eex -->
<%= container(tag: :span,  class: class_names(assigns)) do %>
  <%= if image_path = avatar_image_path(@user) do %>
    <%= img_tag(image_path, class: class_name("image"), alt: avatar_image_alt(@user)) %>
  <% end %>
<% end %>


This can be any type of CSS file that you wish (preprocessed or other wise). Because cells provides methods to namespace your CSS you are advised to use a similar namespace or use something like postcss-modules to ensure all classes defined are unique.

/* lib/app_web/cell/avatar/style.css */
.AvatarCell {
  border-radius: 50%;
  height: 50px;
  width: 50px;

.AvatarCell-image {
  display: inline-block;
  max-width: 100%;


If you use cell-js you can create Javascript that is tightly coupled to the cell:

// lib/app_web/cells/avatar/index.js
import { Cell, Builder } from "@defacto/cell-js";

class AvatarCell extends Cell {
  initialize() {
    this.element.addEventListener("click", this.onToggleOpenClass);

  onToggleOpenClass = e => this.element.classList.toggle("open");

Builder.register(AvatarCell, "AvatarCell");

export default AvatarCell;

Nested cells

For nested cells (e.g. AppWeb.User.AvatarCell) make sure you include the namespace in the stylesheet/javascript.

.User-AvatarCell {}
Builder.register(AvatarCell, "User-AvatarCell");

When in doubt, the cell name corresponds to the data-cell attribute on the DOM element.