Morzaram

Morzaram

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

Showing Posts 1 to 10

Morzaram

Morzaram OP

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

cvkmohan

cvkmohan

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

MMAcode

MMAcode

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

Morzaram

Morzaram OP

I switched to tip-tap and then abandoned it but you can find it in this commit here:
https://github.com/Morzaram/cc_js_hooks_archvied/commit/a910ed4c4f3330ba03abe38d49cee0785721b60e

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.

mayel

mayel

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

CharlesIrvine

CharlesIrvine

@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?
CharlesIrvine

CharlesIrvine

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!

MMAcode

MMAcode

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.

CharlesIrvine

CharlesIrvine

Got ya. Not a problem. Thanks

Where Next? Top

Trending in Guides/Tuts Top

c4lliope
# ~/src/livebook/.iex.exs System.cmd("xdg-open", [ LivebookWeb.Endpoint.access_url() ] ) Because Livebook requires a unique passcode on ...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews