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

sergio
Hey there, we’re going to walk through deploying a Phoenix app to a DigitalOcean droplet, manually - no tools no nothing. Just straight u...
New
Neurofunk
Hi folks! I wrote an article about BEAM and Haskell Interoperabilty. I'm describing the use of the erlang-ffi project, that impersonates ...
New
tobleron
Ok, so I am so excited to share with you the most interesting setup I have made for Elixir/Phoenix today. Why? Because if you trust me, i...
New
New
eclark
I’ve been working on a phoenix project lately and I wanted to use the latest versions of everything. Webpack 5 had some breaking changes ...
New
hauks96
Hello everyone, I created a deployment tutorial for Phoenix applications with Kubernetes (microk8s) a few months back with the goal of s...
New
9mm
So I’m really loving elixir. BY FAR the most excruciating piece of learning a functional language for me is having to “transform” all my ...
New
jtormey
Hello! Having written a lot of LiveView code, I’ve made some VS Code snippets to speed up writing callbacks for LiveViews and LiveCompon...
New
nietaki
Just a quick heads up: There seems to be a bug in Erlang/OTP 21.3, which can cause some errors when making http requests. If you’re using...
New
AstonJ
With a few questions relating to this recently, I wonder if it might help having a thread where we share how we’ve set up Elixir/Erlang/P...
New

Other popular topics Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
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
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; someth...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New

We're in Beta

About us Mission Statement