A module for creating coupled modules of CSS, Javascript and Views in Phoenix
Clone or download
Latest commit 15fe3b3 Oct 29, 2018

README.md

Hex.pm CircleCI

ex_cell

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

Installation

Add the following to the dependencies in mix.exs:

{:ex_cell, "~> 0.0.12"}

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

def controller do
  quote do
  ...

  import ExCell.Controller

  ...
  end
end

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

    import ExCell.View

    ...
  end
end

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

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.

CSS

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": [
      "assets/css/app.css",
      "lib/app_web/cells/**/*.css"
    ]
  }
}
...

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

Javascript

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": [
      "assets/js/**/*.js",
      "lib/app_web/cells/**/*.js"
    ]
  }
}
...

Usage

A cell consists of a couple of files:

cells
|- 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">
</span>

view.ex

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

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

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

template.html.eex

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

style.css

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.

.AvatarCell {
  border-radius: 50%;
  width: 50px;
  height: 50px;
}

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

index.js

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

import { Cell, Builder } from "cells-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;