How to connect Quill with Phoenix

Hey guys I’ve made a guide on how to connect Quill to Phoenix.

For the sake of formatting it might be easier to view it on my Notion Doc Here

How to connect Quill with Phoenix

  1. Make a hook file ex ./assets/js/hooks/textEditHook.js
import Quill from 'quill';
export let TextEditor = {
  mounted() {
    console.log('Mounting');
    let quill = new Quill(this.el, {
      theme: 'snow'
    });

    quill.on('text-change', (delta, oldDelta, source) => {
      if (source == 'api') {
        console.log("An API call triggered this change.");
      } else if (source == 'user') {
        console.log(this);
        this.pushEventTo(this.el.phxHookId, "text-editor", {"text_content": quill.getContents()})
      }
    });
    

  },
  updated(){
    console.log('U');
  }
}
  1. Import it to your ./assets/js/app.js
import { TextEditor } from './hooks/textEditHook' //Put this at the top

  1. Add hooks and assign it to Hooks.TextEditor (call this whatever but you must assign to Hooks

let Hooks = {}

// WYSIWYG
Hooks.TextEditor = TextEditor
  1. Assign the hooks to your LiveSocket
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}})

Note!! This must be at the top level of the map; Not within params #IDidThisByAccident

  1. Assign the phx-hook to where you want the element replaced
<div id="editor" phx-hook="TextEditor" phx-target={@myself} />

Note: I’ve set the phx-target to itself since I’m placing the handle-event function in there

  1. Import the CSS whatever way you want. For my lazy butt I just did an inline CSS import at the top
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
  1. Load that page and give it a shot

My code

My HEEX

<div>
  <link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">

  <h2><%= @title %></h2>

  <.form
    let={f}
    for={@changeset}
    id="profile-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">
  
    <%= label f, :name %>
    <%= text_input f, :name %>
    <%= error_tag f, :name %>
  
    <%= label f, :description %>
    <div id="editor" phx-hook="TextEditor" phx-target={@myself} />
    <%= error_tag f, :description %>

    <%= label f, :birthdate %>
    <%= date_select f, :birthdate %>
    <%= error_tag f, :birthdate %>

    <%= inputs_for f, :links, fn fl -> %>
    <div class="flex flex-row space-x-6">
      <div id="link-title">
        <p>Title</p>
        <%= text_input fl, :title %>
      </div>
      <div id="link-url">
        <p>url</p>
        <%= text_input fl, :url %>
      </div>
    </div>
    <% end %>

    <a phx-click="add-link" phx-target={@myself} >Add Link</a>

    <%= inputs_for f, :pics, [multipart: true], fn fl -> %>
    <div class="flex flex-row space-x-6">
      <div id="link-title">
        <p>Caption</p>
        <%= text_input fl, :alt %>
      </div>
      <div id="link-url">
        <p>url</p>
        <%= text_input fl, :url %>
      </div>
    </div>
    <% end %>

    <a phx-click="add-pic" phx-target={@myself} >Add Pic</a>

  
    <div>
      <%= submit "Save", phx_disable_with: "Saving..." %>
    </div>
  </.form>

</div>

My App.js

// Svelte
import 'svonix'
// We import the CSS which is extracted to its own file by esbuild.
// Remove this line if you add a your own CSS build pipeline (e.g postcss).
import "../css/app.css"
import { TextEditor } from './hooks/textEditHook'

let Hooks = {}

// WYSIWYG
Hooks.TextEditor = TextEditor

// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
//     import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
//     import "some-package"
//

// 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 topbar from "../vendor/topbar"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")

let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}})

// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", info => topbar.show())
window.addEventListener("phx:page-loading-stop", info => topbar.hide())

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

// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket

My Hook (with comments)

import Quill from 'quill';
export let TextEditor = {
  mounted() {
    console.log('Mounting');
    let quill = new Quill(this.el, {
      theme: 'snow'
    });
    // Read more on what .on('events') you can put here 
    // https://quilljs.com/docs/api/#events
    quill.on('text-change', (delta, oldDelta, source) => {
      if (source == 'api') {
        console.log("An API call triggered this change.");
      } else if (source == 'user') {
        console.log(this);
				//This sends the event of
        // def handle_event("text-editor", %{"text_content" => content}, socket) do
        this.pushEventTo(this.el.phxHookId, "text-editor", {"text_content": quill.getContents()})
      }
    });
    

  },
  updated(){
    console.log('U');
  }
}

My handle-event

def handle_event("text-editor", %{"text_content" => content}, socket) do
  IO.inspect(content)
  {:noreply, socket}
end

Things to know

How to push event to desired live view controller…

Set the phx-target in phoenix

<div id="editor" phx-hook="TextEditor" phx-target={@myself} >
   <%= text_input f, :description%>
</div>

In your js file reference it like this…

this.el.phxHookId

You will see above in the code snippet how this works together

Note: I’m using arrow functions so I don’t have to rebind this #ShitJSDoes

7 Likes

Also, I’m saving the information as JSON in the db since I’m using posgres so I believe

the field should look like this field :description, :map

3 Likes

Good guide @Morzaram . Thanks. I think, more or less similar approach should work for Prosemirror as well.

4 Likes

Hi Morzaram, I am trying to setup Quilljs for rich editing with Elixir and Phoenix. I see you did it about year ago (thank you for sharing your notes and experience!). Do you have that repo available so that I could use it for inspiration? I struggle a bit as I am relatively new to Phoenix. Eg now I trying to work out how to set default quill form values based on what quill json is stored in db (ecto-postgres).

Any answer/tips will be much apreciated

I switched to tip-tap and then abandoned it but you can find it in this commit here:

I’ve updated the notion to include the link at the bottom of the page. Please comment in only once place instead of sending duplicate messages…

IMHO, if you’re doing some heavy js stuff really consider if you need live view for that page. It’s hard to get right and the article might not be the best way to do it now adays.

1 Like

We also have an integration at GitHub - bonfire-networks/bonfire_editor_quill

3 Likes

Anybody interested how I got it working can checkout my commit here. In contains all changes I did (may be excluding some mix commands) to implement quill and get it saving and loading data from postgres: rich text implemented and working (in this single commit) · MMAcode/our_experience@b8568aa · GitHub

2 Likes

@Morzaram I am curious about your experience with Quill.

  • Why did you switch to “tip-tap”?
  • Which of the two do you know recommend, assuming the context is Elixir/Phoenix/LiveView?

Hello @MMAcode. I tried to build and run your project, but I ran into a problem:

our_experience[main|✔] % mix deps.get                                           
[mix.exs:3: OurExperience.MixProject (module)]
Mix.env() #=> :dev

** (File.Error) could not read file "/Users/charlesirvine/src/our_experience/config/secrets/auth0.secret.exs": no such file or directory
    (elixir 1.14.2) lib/file.ex:358: File.read!/1
    (elixir 1.14.2) lib/config.ex:273: Config.__import__!/1
    /Users/charlesirvine/src/our_experience/config/config.exs:72: (file)
    (stdlib 4.2) erl_eval.erl:748: :erl_eval.do_apply/7
    (stdlib 4.2) erl_eval.erl:136: :erl_eval.exprs/6
our_experience[main|✔] % 

Do you have any suggestions on how to work around this problem? Is there a way I can create that missing file?

Thanks!

Hi Charles, that file contains secrets to like username and password to connect to Auth0. There is also other file missing which also contains secrets like username and password for my postgres. Overall I don’t know how to fix this, my code should be used as a working example, not as an exact code to use.

Got ya. Not a problem. Thanks

I can’t remember. It’s been quite a while. I think it had more feature control and extensability, and doc clarity. But I can’t say any more due to time’s sake.