Octantis

View Source

Octantis is an implementation of Polaris Design System in Elixir for Phoenix LiveView. If you are building a ShopAdmin for your Shopify App (Shopify calls this embedded app home at times), and want to conform to the Shopify perscribed design system, then you will want to use Octantis.

Octantis provides Polaris LiveView components that you can drop into your LiveView Shop admin.

Implementations

Octantis components are wrappers around two different implementations: React and Web Components.

React

The OctantisWeb.Components.Polaris components largely match the html produced by Polaris React. This implementation is meant to read like the react implementations as much as possible.

<.card>
  <.text as="h2" variant="bodyMd">
    Welcome to The Littlest Marble Shop
  </.text>
</.card>

Web Components

The OctantisWeb.Components.PolarisWC components are a light wrapper around the new Polaris Web Components. Some niceties have been added around responsive attributes with the ~s sigil and type checking. Events are forwared through the OctantisEventProxy hook.

<.s_section>
  <.s_heading>
    Welcome to The Littlest Marble Shop
  </.s_heading>
</.s_section>

σ Octantis

σ Octantis is the current southern pole star in opposition to Polaris the current northen pole star.

Other Resource

  • Elixir ShopifyAPI is most of what you need to interact with Shopify APIs. Auth, Rest, Graphql, Webhooks and so on.
  • Elixir Shopify App is a template for building an App with Elixir ShopifyAPI. Currently it lacks a LiveView ShopAdmin as a default, but there is some work towards enabling that.
  • Polaris Design System
  • Shopify AppBridge provides some functionality for the ShopAdmin, noteably toasts and navigation menues.

Installation

Octantis is available in Hex, the package can be installed by adding octantis to your list of dependencies in mix.exs:

def deps do
  [
    {:octantis, "~> 0.2.0"}
  ]
end

Install Hooks

In assets/js/app.js add

import * as octantisHooks from "octantis"

let Hooks = { ...octantisHooks }

let liveSocket = new LiveSocket("/live_view_path", Socket, {hooks: Hooks, bindingPrefix: "data-phx-"})

Note that the bindingPrefix of "data-phx-". AppBridge and Polaris have a tendancy to remove attributes that do not have the data- prefix.

Setup A LiveView Shop Admin

We recommend creating a new liveview endpoint with its own routes, templates, and JS just for your Shop Admin.

In OctantisAppWeb.Endpoint:

socket "/shop_admin_live", Phoenix.LiveView.Socket, websocket: [connect_info: []]

In OctantisAppAppWeb.Router:

pipeline :shop_admin do
  plug ShopifyAPI.Plugs.AdminAuthenticator
  plug ShopifyAPI.Plugs.PutShopifyContentHeaders
end

live_session :live_shop_admin,
  layout: {OctantisAppAppWeb.ShopAdminLive.Layouts, :app},
  root_layout: {OctantisAppAppWeb.ShopAdminLive.Layouts, :root},
  session: {OctantisAppAppWeb.ShopAdmin.Hooks.AssignScope, :build_session, []} do
  scope "/live_shop_admin", OctantisAppAppWeb do
    pipe_through :browser
    pipe_through :shop_admin

    live "/", ShopAdmin.DashboardLive.Index, :live
    live "/settings", ShopAdmin.SettingsLive.Index, :index
  end
end

In assets/js/shop_admin.js:

// JS specific to LiveView in ShopAdmin

// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import * as baseHooks from "./hooks"
import * as octantisHooks from "octantis"

let Hooks = { ...baseHooks, ...octantisHooks }
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/shop_admin_live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}, bindingPrefix: "data-phx-"})

// Show progress bar on live navigation and form submits
window.addEventListener("phx:page-loading-start", _info => shopify.loading(true))
window.addEventListener("phx:page-loading-stop", _info => shopify.loading(false))

window.addEventListener("phx:page-loading-stop", info => {
  /*
    When navigating, AppBridge detects changes within a LiveView,
    however, it does not detect between LiveView navigation, even when it is
    correctly patching within a live_session. This re-emits a navigation event
    that AppBridge will detect.
  */
  const destination = new URL(info.detail.to).pathname
  if (info.detail.kind == "initial" && destination != window.location.pathname) {
    history.pushState(null, '', destination);
  } else {
    history.replaceState(null, '', destination);
  }
})

// connect if there are any LiveViews on the page
liveSocket.connect()

window.liveSocket = liveSocket
window.shopifyIdToken = shopify.idToken();

In lib/octantis_app_web/live/shop_admin/layouts/root.html.heex

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="csrf-token" content={get_csrf_token()} />
    <meta name="shopify-api-key" content={OctantisApp.Config.api_key()} />

    <%!-- Polaris CSS --%>
    <OctantisWeb.Components.Head.stylesheet />
    <%!-- AppBridge JS --%>
    <OctantisWeb.Components.Head.javascript />
    <script defer data-phx-track-static type="text/javascript" src={~p"/assets/shop_admin.js"}>
    </script>
  </head>
  <body>
    {@inner_content}
  </body>
</html>

In lib/cotantis_app_web/live/shop_admin/layouts/app.html.heex

<.ui_nav_menu>
  <:link name="Home" url="/live_shop_admin/" />
  <:link name="Settings" url="/live_shop_admin/settings" />
</.ui_nav_menu>

<main role="main" id="root">
  <.toast kind={:info} flash={@flash} id="toastinfo" />
  <.toast kind={:error} flash={@flash} id="toasterror" />

  <.s_query_container container_name="Page">
    <.s_page heading={@page_heading} id="PageRoot">
        {@inner_content}
    </.s_page>
    <.s_box padding="small"><%!-- spacer --%></.s_box>
  </.s_query_container>
</main>

Local Setup

Run tests

mix check

Run Storybook

mix phx.server

Navigate to http://localhost:4040/storybook