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

Where Next?

Popular in Guides/Tuts Top

benwilson512
Correct if me I’m wrong, as best I can tell there aren’t any reasons to use mix run --no-halt in production vs releases. The marginal val...
New
thetechnologyvault
One of our team members just published this getting started guide for Elixir/Phoenix devs to use the Nanobox platform: https://content.n...
New
1player
A question I had when first learning contexts and Ecto was how to expand the default context API to support more flexible queries. Usuall...
New
OvermindDL1
Ran across this recently, it’s a set of cheatsheet inforgraphic things of the OTP behaviours on the BEAM: https://github.com/Telichkin/o...
New
OndrejValenta
Me and my boys started a new website specifically designed for other ASP.NET programmers that struggle, as we do, with their transformati...
New
zazaian
I recently generated the boilerplate for a new mix app using the Phoenix 1.3.1 phx.new generator and realized that the structure of the w...
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
ob1
Recently we partitioned a table with 28 million rows by its ULID to improve query speeds and reduce query timeouts. To do this I ended up...
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
nelsonic
When we were figuring out how to use Phoenix LiveView we got stuck a few times. So in order to save other people time, we created a comp...
New

Other popular topics Top

msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43622 214
New
Lily
In templates/appointment/index.html.eex: &lt;%= for appointment &lt;- @appointments do %&gt; &lt;tr&gt; &lt;td&gt;&lt;%= appoi...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
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
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement