Getting Started with Phoenix LiveView
As promised in the first article of this series we are about to embark on a journey to build a collaborative rich text editor widget. In this post we will do all the setup work of starting a new Phoenix LiveView app, adding a Document context and doing the initial editor integration. I picked this task, not because I know what I'm doing, but because I want to explore the edges where frontend SPA widgets interact with the realtime features of LiveView.
Prerequisites
If you haven't worked with Phoenix before, I recommend following the Phoenix Installation guide. Documentation is one of the many great things about the Elixir community. It is a first class citizen in community-supported libraries. The documentation website is generated out of the source of the various projects, so it stays fairly accurate and up-to-date. If you don't have time to head over there and follow along, here are the steps at the command line.
brew install elixir
(for mac) orcinst elixir
(for windows with Choclatey). Each linux distro has it's own method so check the referenced link for your distro. Elixirmix local.hex
to install or update the package manager for Elixirmix archive.install hex phx_new
to install or update Phoenix- Postgres is the default database for Phoenix, but you can specify others at application creation time. Here's a link to install Postgres
- If you are on linux, you will need to install inotify-tools to enable Phoenix's live reload feature.
At this point you should be ready to start building the application.
Init the App
To make the inital steps go faster, we'll lean on Phoenix's generators to build the app and add initial scaffolding. If you've used Ruby on Rails or Laravel in php you will be familiar with app generators. Phoenix's generators are surprisingly complete and useful compared to generators I've used in other frameworks. To start the app, navigate to where you want the app to live on disk and type:
# add --database=xxx if not using Postgres
$ mix phx.new editor
...
Fetch and install dependencies? [Yn] Y
* running mix deps.get
* running mix deps.compile
$ cd editor
$ mix deps.get # if you said 'no' above
$ mix ecto.create
$ git init
mix phx.new <app_name>
initializes the application. If you are using a Phoenix version before 1.6 you will need to add the --live
flag, but upgrading would be a better strategy for a new probject. ecto.create
creates the database and mix deps.get
downloads all the dependencies. Phoenix comes with an alias mix setup
that does all these initialization steps, including writing your seeds file to the db, which can be really handy if you are onboarding to an existing project. Optionally you can add git init
to create a git repo to track your code changes and make it easy to back up if something goes wrong.
Adding default authentication
For our app, we want to create documents and attach each one to a user. That means we need to add auth to our app. Fortunately Phoenix has a generator for this. Simply type:
$ mix phx.gen.auth Accounts User users
There's a lot going on here so lets take a minute to wrap our heads around what the generator provides. Phoenix apps work hard at helping find the best place for each type of code. All the generators phx.gen.live
, phx.gen.html
, phx.gen.json
and phx.gen.context
that create a database table are designed to create both a model and a context. The model is for the database interactions that don't change often like validations while the context is where much of the business logic goes. So, when you run phx.gen.auth
it needs some parameters. The first parameter is the name of the context module. The second is the model name and the third is the database table name. In our case the generator creates a context file in lib/editor/accounts.ex and a model file in lib/editor/accounts/user.ex. phx.gen.auth
does a lot more than that. It creates all the views and controller to handle basic email/password authentication including sending emails for email verification and forgot password cases. At this point you can fire up the app.
$ mix ecto.migrate
$ mix phx.server
Then you can point your browser at localhost:4000 to see your app's default main page with links to the auth functions as well as to lots of documentation. Click on the register link and add your first user. If you enter a malformed email or password you will see that the pages include standard validation and error handling code. In fact the auth code has been thoughtfully constructed to even thwart timing attacks.
Add our documents
Now we can create the documents table and CRUD LiveViews to allow users do what the app is for. To generate our first LiveView, it's as easy as:
$ mix phx.gen.live Documents Document documents name:string data:text user_id:references:users
--- output ---
Add the live routes to your browser scope in lib/editor_web/router.ex:
live "/documents", DocumentLive.Index, :index
live "/documents/new", DocumentLive.Index, :new
live "/documents/:id/edit", DocumentLive.Index, :edit
live "/documents/:id", DocumentLive.Show, :show
live "/documents/:id/show/edit", DocumentLive.Show, :edit
Remember to update your repository by running migrations:
$ mix ecto.migrate
Here you see the consistency of a well designed api. Just like we did with phx.gen.auth
the LiveView needs a backing context and model. The pattern is extended to allow us to specify the fields and types that our document needs. Notice that we can easily create cross-table references using this tool. Along with the context and model, the Phoenix generator also created a documents index page that lists documents and includes a modal form to create and edit documents as well as a show page so users can see the document details. Add the new routes to lib/editor_web/router.ex
as directed and run mix ecto.migrate
to turn on the new functionality.
So far it all feels like any other CRUD app, and the user pages, context and controller work exactly like a standard CRUD app with HTTP REST verbs and view rendering and the auth functionality does exactly follow that model. The document routes are served using LiveView which is a very different process. REST endpoints are designed to be stateless. Each transaction has to come with all the info the action needs (or has to be looked up somewhere). LiveView endpoints are very different. Because Elixir is built on Erlang and the BEAM, Phoenix inherits the lightweight processes and fault tolerance built into that platform. When a user navigates to /documents
the server sends code to have the browser spin up a new web socket connection back to a lightweight BEAM process to manage a long-running user and session-specific conversation. This enables performant, resource efficient ways for the server and browser to send messages back and forth as events happen. Because the BEAM support efficient message passing between these lightweight processes it is surprisingly straightforward to connect multiple users on the server to the same document and see realtime update.
One area we are totally ignoring is Elixir and Phoenix's unique testing capabilities. All the generators we've used so far have created test files for us. I'm going to continue ignoring it for lack of space, but I recommend you check out the docs and this excellent article by Brooklin Myers as well as take a peek at the boilerplate test files the generators created.
Generators' end
As with any code generator, it doesn't take long to get to the point where your use case diverges from what the code generator can provide. We've reached that point in our journey. The generated create/edit code for documents presents a modal dialog for those actions which is not what we need for our collaborative rich text editor. Our app doesn't really need a show page but does need a separate edit page. The quickest way to get there is to rename lib/editor_web/live/document_live/show.ex
to lib/editor_web/live/document_live/edit.ex
and update the module name, routes and links accordingly.
Integrating the rich text editor
There are several rich text editors available around the internet. Newer entries like Draft.js and Trix embed well but aren't built in a way that makes collaborative editing easy. ProseMirror and CKEditor both support collaborative editing but are more challenging to integrate.
A recent change to Phoenix is the inclusion of ESBuild as the default asset bundler. ESBuild is pre-configured by Phoenix to bundle using the assets directory as the root. When you add a js library to your project you have two options. You can vendor the library in by downloading files and putting them in assets/vendor, or you can use npm install --save
from the assets directory. In either case, import the module in your assets/js/app.js file like so:
// for vendored files
import ClassicEditor from "../vendor/ckeditor5-build-classic"
// for npm files
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
The default ESBuild configuration will bundle up anything imported into app.js. Getting the non-shared CKEditor integrated into a LiveView page is almost what'd you'd expect. In it's simplest form, it's just an import, an init function and an on-page script to trigger the js behavior. Here's my bare-bones implementation.
// in assets/js/app.js
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
// ... trimmed standard LiveView init code
window.setup_editor = function setup_editor(id) {
ClassicEditor
.create( document.querySelector(`#${id}`) )
.then( editor => {
console.log( editor );
})
.catch( error => {
console.error( error );
});
}
The page that needs to instantiate the rich text editor can call the setup_editor function. Because your LiveView template lives inside the main app template, even if you put your js snippet at the bottom of the page, the main app.js
file will not have loaded yet. You must wait until app.js has loaded to call the initialization function. One of the easier ways to do that is to wrap the setup call in a DOMContentLoaded
event callback. This will work well in any modern browser. If you are stuck supporting IE 8 or below you will need to make other accommodations. (Editor's note: this is the wrong way to integrate with LiveView, but I left it for historical reasons.)
document.addEventListener("DOMContentLoaded", function(_event) {
window.setup_editor('document-form_data');
});
CKEditor's default implementation attaches to and saves back to the text area, so any changes you make will be communicated to the server when you click 'Save'.
That's it for all the boilerplate we needed to get the app up and running and get the js rich text editor integrated. In our next installment we will be discovering how to let multiple people see who else is viewing and editing the same document at the same time. Won't that be fun! We'll be covering both the back-end Phoenix concepts related to Channels and Presence as well as how to integrate those LiveView messages into the javascript component to keep everyone's document instances in sync. I've added a repo with all this code. The code as it exists in this article is in the blog-post-2
branch.