Home

Awesome

Surface Catalogue

This is mostly a prototype, meant to validate a few ideas to have something similar to https://storybook.js.org/ for Surface.

Installation

Add surface_catalogue to your list of dependencies in mix.exs:

def deps do
  [
    {:surface_catalogue, "~> 0.6.3", only: :dev}
  ]
end

Update your router.ex configuration:

# lib/my_app_web/router.ex
use MyAppWeb, :router
import Surface.Catalogue.Router

...

if Mix.env() == :dev do
  scope "/" do
    pipe_through :browser
    surface_catalogue "/catalogue"
  end
end

Add a :catalogue entry in the :esbuild config in config.exs:

config :esbuild,
  default: ...,
  # Add a new entry for the catalogue
  catalogue: [
    args: ~w(../deps/surface_catalogue/assets/js/app.js --bundle --target=es2016 --minify --outdir=../priv/static/assets/catalogue),
    cd: Path.expand("../assets", __DIR__),
    env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
  ]

Then update the endpoint configuration in config/dev.exs to add an esbuild watcher for catalogue:

config :my_app, MyAppWeb.Endpoint,
  ...
  watchers: [
    esbuild: ...,
    # Add an esbuild watcher for :catalogue
    esbuild: {Esbuild, :install_and_run, [:catalogue, ~w(--sourcemap=inline --watch)]},
  ]

That's all!

Run mix phx.server and access "/catalogue" to see the list of all available components in your project.

Loading Examples and Playgrounds

If you want to access examples and playgrounds for components, edit your mix.exs file, adding a new entry for elixirc_paths along with a catalogues function listing the catalogues you want to be loaded:

...

def catalogues do
  [
    # Local catalogue
    "priv/catalogue",
    # Dependencies catalogues
    "deps/surface/priv/catalogue",
    "deps/surface_bulma/priv/catalogue",
    # External catalogues
    Path.expand("../my_components/priv/catalogue"),
    "/Users/johndoe/workspace/other_components/priv/catalogue"
  ]
end

defp elixirc_paths(:dev), do: ["lib"] ++ catalogues()

...

Then update the endpoint configuration in config/dev.exs to set up live reloading for your catalogue:

config :my_app, MyAppWeb.Endpoint,
  live_reload: [
    patterns: [
      ~r"priv/catalogue/.*(ex)$",
      ...
    ]
  ]

Note: Without the above configurations, the list of available components is still presented in the catalogue page. However, when selecting a component, only its documentation and API will be shown. No example/playground will be loaded nor tracked by Phoenix's live reloader.

Sharing catalogues

If you're working on a suite of components that you want to share as a library, you may need to provide additional information about the catalogue. This will be necessary whenever your components require any css or js code that might not be available on the host project.

To provide that additional information you must create a module implementing the Surface.Catalogue behaviour in your priv/catalogue/ folder. Example:

defmodule MySuite.Catalogue do
  use Surface.Catalogue

  @impl true
  def config() do
    [
      head_css: """
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.8.2/css/bulma.min.css" />
      """
    ]
  end
end

Running the built-in catalogue server

In case you're working on a lib that doesn't initialize its own Phoenix endpoint, you can use the built-in server provided by the surface_catalogue following these steps:

Add the required dependencies to your list of dependencies in mix.exs:

def deps do
  [
    {:plug_cowboy, "~> 2.0", only: :dev},
    {:phoenix_live_reload, "~> 1.2", only: :dev},
    {:jason, "~> 1.0", only: :dev}
  ]
end

Create a dev.exs script at the root of your project with the following content:

# iex -S mix dev

Logger.configure(level: :debug)

# Start the catalogue server
Surface.Catalogue.Server.start(
  watchers: [
    esbuild: {Esbuild, :install_and_run, [:catalogue, ~w(--sourcemap=inline --watch)]}
  ],
  live_reload: [
    patterns: [
      ~r"priv/static/.*(js|css)$",
      ~r"lib/my_lib_web/.*(ex)$"
    ],
    notify: [
      live_view: [
        # Colocated sface and css can be reloaded without a hard refresh
        ~r"lib/my_lib_web/.*(sface|css)$"
      ]
    ]
  ]
)

Surface Catalogue's assets need to be generated by your project to make it compatible with your Phoenix dependency versions.

Add the following content to your config/config.exs or config/dev.exs:

import Config

if Mix.env() == :dev do
  config :surface_catalogue,
    title: "My Lib Web",

  config :esbuild,
    ...
    catalogue: [
      args:
        ~w(#{Mix.Project.deps_paths().surface_catalogue}/assets/js/app.js --bundle --target=es2016 --minify --outdir=priv/static/assets/catalogue),
      env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
    ]

  # Required at compile time
  config :surface_catalogue, Surface.Catalogue.Server.Endpoint,
    code_reloader: true,
    debug_errors: true
end

Make sure you set the patterns option according to your project.

To make things easier, add an alias run the script in your mix.exs:

def project do
  [
    ...,
    aliases: aliases()
  ]
end

...

defp aliases do
  [
    dev: "run --no-halt dev.exs",
    ...
  ]
end

Run the server with:

mix dev

or using iex:

iex -S mix dev

You can now access the catalogue at localhost:4000.

If you need, you can also start the server using a different port:

PORT=4444 iex -S mix dev

Credits

The Surface.Catalogue.Server implementation was mostly extracted from the dev.exs script from phoenix_live_dashboard. All credits to the Phoenix Core Team.

License

Copyright (c) 2021, Marlus Saraiva.

Surface source code is licensed under the MIT License.