Morzaram

Morzaram

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

Most Liked

cvkmohan

cvkmohan

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

Morzaram

Morzaram

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

mayel

mayel

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

MMAcode

MMAcode

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

Morzaram

Morzaram

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.

Where Next?

Popular in Guides/Tuts Top

hauks96
Hello everyone, I created a deployment tutorial for Phoenix applications with Kubernetes (microk8s) a few months back with the goal of s...
New
New
pel_daniel
Hi! I created some animations to explain what happens internally with map &amp; reduce functions. The code is open source. Any feedback...
New
sergio
Wrote this guide on how to integrate DropzoneJS with Phoenix Liveview. Hope it helps someone out! https://sergiotapia.com/dropzonejs-dir...
New
nelsonic
Complete beginners Todo List Tutorial in Phoenix 1.5.3 (latest and greatest): It’s a feature complete TodoMVC clone with 0% JavaScri...
New
rhcarvalho
After collecting information from multiple sources (this forum, blogs, StackOverflow and GitHub), I was finally able to successfully embe...
New
mhanberg
Hi! I recently finished adding authentication to my Phoenix API, so I wanted to share what I learned. I haven't created authentication ...
New
GenericJam
Just leaving some breadcrumbs for future me and future others like me. Connect with TCP (not secured) - most servers will reject but use...
New
smpallen99
Some advice for Elixir programmers. I was reviewing someone's Elixir Code yesterday and found a deadlock condition bug in a GenServer i...
New
OvermindDL1
Ran across this recently, it's a set of cheatsheet inforgraphic things of the OTP behaviours on the BEAM:
New

Other popular topics Top

sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New

We're in Beta

About us Mission Statement