Elm and contenteditable

1,727 views
Skip to first unread message

Vincent Jousse

unread,
Aug 30, 2016, 5:03:08 AM8/30/16
to Elm Discuss
Hello,

I'm writing an application where the user needs to edit some HTML content. When I first wrote this application in JS, I was using some contenteditable fields to do so.

How should I handle this with Elm? My problem is that if I set a div with contenteditable=true in elm, and that the content of this div depends on my model state, when an user edits the HTML, my model is now out of sync.
Maybe getting the innerHTML field when the user is changing the content of the div and set this to a field in my model? But how would I do to convert this string back to some Html.div/Html.span/whatever code in Elm?
The tricky part is that I need to handle events on spans that are in the div with contenteditable=true.

I tried to do it using Ports and Draft-js but the problem is that I now have 2 states in my application: one in Elm and one in JS/React. Then, all the beauty of "my application just depends on my Elm model state" is not true anymore, as I know need to sync the 2 states…

Not sure if I'm really clear, but thanks for reading this anyway ;-)

Peter Damoc

unread,
Aug 30, 2016, 5:46:12 AM8/30/16
to Elm Discuss
I've tried a naive implementation in Elm but, for some reason, it doesn't work (events are not fired for divs that have content editable) 

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.App exposing (beginnerProgram)
import Html.Events exposing (on)
import Json.Encode as JE
import Json.Decode as JD exposing ((:=))


main =
  beginnerProgram { model = "", view = view, update = update }


onContent tagger = 
  on "input" (JD.map tagger ("innerHTML" := JD.string))

view model =
  div []
    [ div [property "innerHTML" (JE.string model), onContent OnChange, contenteditable True][]
    ]


type Msg = OnChange String 


update msg model =
  case (Debug.log "msg:" msg) of
    OnChange str -> str






--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.



--
There is NO FATE, we are the creators.
blog: http://damoc.ro/

Aaron VonderHaar

unread,
Aug 30, 2016, 9:42:47 AM8/30/16
to elm-d...@googlegroups.com

Peter, I'd guess the decoder in your onChange is failing, causing the event not to send a Msg. The decoder is going to get an event object, not a DOM node, which is probably why decoding an "innerHTML" property is failing.  (Did your comment mean that your event handler does work for non-contentEditable divs?)

Peter Damoc

unread,
Aug 30, 2016, 9:57:28 AM8/30/16
to Elm Discuss
Aaron, 

You are right, of course. 
I somehow expected that to work due to mixing in my mind the event object with the source of the event.
My bad. :)
 

Peter Damoc

unread,
Aug 30, 2016, 10:05:34 AM8/30/16
to Elm Discuss
For the record, the working version looks like this: 


import Html exposing (..)
import Html.Attributes exposing (..)
import Html.App exposing (beginnerProgram)
import Html.Events exposing (on)
import Json.Encode as JE
import Json.Decode as JD exposing ((:=), Decoder)


main =
  beginnerProgram { model = "", view = view, update = update }


onContent tagger = 
  on "input" (JD.map tagger targetInnerHtml)

targetInnerHtml : Decoder String
targetInnerHtml =
  JD.at ["target", "innerHTML"] JD.string

view model =
  div []
    [ div [property "innerHTML" (JE.string model), onContent OnChange, contenteditable True][]
    , hr [][]
    , div [property "innerHTML" (JE.string model)][]
    ]


type Msg = OnChange String 


update msg model =
  case (Debug.log "msg:" msg) of
    OnChange str -> str

Vincent Jousse

unread,
Aug 30, 2016, 10:11:48 AM8/30/16
to Elm Discuss
Thanks, but I would not say that it's "working" as it seems that the cursor position is reset after each event, causing the text to be written from right to left :-)

It's seems related to this issue: https://github.com/elm-lang/virtual-dom/issues/23

And with this approach (storing a string), how could I trigger events on the HTML inside the contenteditable div?
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--
There is NO FATE, we are the creators.
blog: http://damoc.ro/

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--
There is NO FATE, we are the creators.
blog: http://damoc.ro/

Peter Damoc

unread,
Aug 30, 2016, 10:36:10 AM8/30/16
to Elm Discuss
Well, it was a naive approach. ^_^ 

As for events inside the contenteditable div, I have no idea but what kind of events do you want to handle in this kind of a scenario?



On Tue, Aug 30, 2016 at 5:11 PM, Vincent Jousse <vjo...@gmail.com> wrote:
Thanks, but I would not say that it's "working" as it seems that the cursor position is reset after each event, causing the text to be written from right to left :-)

It's seems related to this issue: https://github.com/elm-lang/virtual-dom/issues/23

And with this approach (storing a string), how could I trigger events on the HTML inside the contenteditable div?


Vincent Jousse

unread,
Aug 30, 2016, 10:43:22 AM8/30/16
to Elm Discuss
I mainly need double click events. I'm doing a transcription editor: I'm using a Speech To Text tool to generate words with their timestamps.

I'm putting the words in a contenteditable div so that people can correct the transcription, and each word is surrounded by a span containing the timestamp info in some data- attribute.
When people double click on the span, I want the audio player to play the file starting at this timestamp. And when the audio is playing, I want the «current word» to be highlighted in the contenteditable div.

Does it make sense?

Peter Damoc

unread,
Aug 30, 2016, 11:16:21 AM8/30/16
to Elm Discuss
It makes perfect sense. 

Have you thought about putting contenteditable on each individual span? 
This should allow for single word editing and for individual double clicks. 


--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Vincent Jousse

unread,
Aug 30, 2016, 11:18:37 AM8/30/16
to Elm Discuss
I didn't but it's a very good idea :-)

Do you see a way to overcome the cursor position problem with that approach?
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Damir Batinović

unread,
Aug 31, 2016, 10:02:30 AM8/31/16
to Elm Discuss
I had to use contenteditable in my project. The true way to handling cursor position was to hard (I don't remeber what it was but I thing it involves window.selection) so I went with a  dirty hack. Added another field to my model (tmpInput), saving input events into that field until Blur event was fired. Upon blur event tmpInput is copied to real input. So model and view are out of sync until blur happens. It's not nice but there where no problems with implementation so far.

Vincent Jousse

unread,
Sep 2, 2016, 9:34:19 AM9/2/16
to Elm Discuss
Nice trick :D

I think I'll stay with my CKeditor solution for the moment, but thanks a lot anyway.

Steve Jones

unread,
Sep 26, 2016, 5:54:49 PM9/26/16
to Elm Discuss
Vincent,

So, you've had some success with CKeditor? I'm having some difficulty getting any of these rich text editors to work within my app. I have tried CKeditor yet though.

Vincent Jousse

unread,
Sep 26, 2016, 7:01:16 PM9/26/16
to Elm Discuss
Yes it works for me.

What's your problem exactly?

Steve Jones

unread,
Sep 26, 2016, 7:26:45 PM9/26/16
to Elm Discuss
Well, I'm not sure yet. But I think it was to do do with dom lifecycle and timing (I haven't tried CKeditor yet though. So far, I've tried tinymce and quill). Things that work with static HTML do not with HTML generated with elm. I've not found a lot of people doing this sort of thing successfully, so I'm mostly trying to determine what's been done.

Quill gives me "Invalid Quill container". And tinymce just silently fails to do anything.

Vincent Jousse

unread,
Sep 27, 2016, 4:56:48 AM9/27/16
to Elm Discuss
The main "hack" I had to do was to wait on the Javascript side for the DOM element to be rendered by ELM.

A setTimeout recursively checking if the element exists (and so if I can attach a CKEditor to it) did the trick.

Steve Jones

unread,
Sep 27, 2016, 6:12:37 AM9/27/16
to Elm Discuss
Yes, that seems to have been the missing piece of the puzzle for me. 'arrive.js' (which is based on Mutation Observers) has been recommended to me, though your solution may be preferable, depending on the the browser support that I'm being held to.

Thanks

Steve Jones

unread,
Sep 27, 2016, 7:36:22 AM9/27/16
to Elm Discuss
Does CKeditor provide a convenient api for working with elm?

Vincent Jousse

unread,
Sep 27, 2016, 8:22:54 AM9/27/16
to Elm Discuss
Define convenient? :) CKEditor API is ok to work with Elm.

Girish Sonawane

unread,
Sep 27, 2016, 10:35:04 AM9/27/16
to Elm Discuss
Instead of onChange, you can try using Events.on http://package.elm-lang.org/packages/elm-lang/html/1.1.0/Html-Events#on
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Simon

unread,
Feb 24, 2017, 3:38:33 AM2/24/17
to Elm Discuss
I'm also trying to work with contenteditable. I have a basic system working with pure Elm, in that I can click on a field to edit it and then click elsewhere with blur causing an update of the model. But the challenge I face is detecting an enter key and using that save the updated value and blur the current field. I seem to be getting some virtual DOM issues.  

Anyway I have an Ellie at https://ellie-app.com/tnXL92zwvka1/3 and if anyone can help, I'd love to know

Simon

Josh Szmajda

unread,
Feb 24, 2017, 10:30:22 AM2/24/17
to Elm Discuss
I'm working with http://github.com/Voog/wysihtml, integration has been pretty alright.. Still very much in the figuring things out stage for my app but here are some key snippets:

Ports:
```
port startEditor : (String, String, String) -> Cmd msg
port stopEditor : String -> Cmd msg
port editorChanges : (String -> msg) -> Sub msg
port selectionChange : (Selection -> msg) -> Sub msg
```

Selection:
```
type alias Selection =
  { text : String
  , t : Float
  , r : Float
  , b : Float
  , l : Float
  }
```

WYSIHtml View:
```
module Wysihtml.View exposing (wysiToolbar, wysiEditor)

import Html exposing (..)
import Html.Attributes exposing (id, attribute, class)

wysiToolbar : a -> Html a
wysiToolbar msg =
  div [ id "wysi-toolbar" ]
    [ a [ class "bold", command msg "bold" ] [ text "B" ]
    , a [ class "italic", command msg "italic" ] [ text "I" ]
    , a [ class "h1", command msg "formatBlock", commandValue msg "h1" ] [ text "H1" ]
    , a [ class "p", command msg "formatBlock", commandValue msg "p" ] [ text "P" ]
    , a [ class "ul", command msg "inserUnorderedList" ] [ text "list" ]
    ]

wysiEditor : a -> Html a
wysiEditor msg =
  div [ id "wysi-editor", dataPlaceholder msg "Start editing" ] []


dataPlaceholder : a -> String -> Attribute a
dataPlaceholder msg data =
  attribute "data-placeholder" data

command : a -> String -> Attribute a
command msg command =
  attribute "data-wysihtml5-command" command

commandValue : a -> String -> Attribute a
commandValue msg value =
  attribute "data-wysihtml5-command-value" value
```

index.html wiring:
```
app.ports.startEditor.subscribe(function(){
  var editor = arguments[0][0];
  var toolbar = arguments[0][1];
  var initialText = arguments[0][2];
  // todo change setTimeout to MutationObserver
  setTimeout(function(){
    console.log('start editor');
    window.x_editor = new wysihtml5.Editor(editor, {
      toolbar: toolbar,
      parserRules:  wysihtml5ParserRules
    });
    window.x_editor.setValue(initialText, true);
    window.x_editor.on('interaction', function(){
      app.ports.editorChanges.send(x_editor.getValue());
    });
  }, 60);
});
document.addEventListener("selectionchange", function(){
  console.log("selection changed, sending to elm");
  selection = window.getSelection();
  if(!selection.isCollapsed){
    for(var i = 0; i < selection.rangeCount; i++) {
      range = selection.getRangeAt(i);
      rect = range.getBoundingClientRect();
      app.ports.selectionChange.send({ text: selection.toString(), t: rect.top, r: rect.right, b: rect.bottom, l: rect.left });
    }
  }
});
```

I think that's the meat. Happy to chat more.

Witold Szczerba

unread,
Feb 25, 2017, 5:41:42 AM2/25/17
to elm-d...@googlegroups.com
One trick I have learned when integrating date range picker is that you can push data from component to Elm using custom HTML events instead of sending data through a port(s).
I have described it shortly here:

input [ class "form-control date-range" , value <| formatMaybeDuration item.duration , onDateRangePicker DurationChanged -- see demo for details ] []

Regards,
Witold Szczerba

To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.

Vincent Jousse

unread,
Feb 26, 2017, 3:11:51 AM2/26/17
to elm-d...@googlegroups.com
The best workaround I've found is to use a webcomponent wrapper (only compatible with chrome): https://github.com/vjousse/the-transcriber/blob/webcomponents/src/static/js/custom-text-editor-element.js

I'm using a custom attribute called "content" that is mapped to my Elm Model. With this solution I've got the best of both worlds: Elm is managing the DOM of my contenteditable element while being able to write custom behavior in javascript for it.

It is based on this talk by Richard Feldman: https://www.youtube.com/watch?v=ar3TakwE8o0

--
You received this message because you are subscribed to a topic in the Google Groups "Elm Discuss" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elm-discuss/YKz8rgffoWc/unsubscribe.
To unsubscribe from this group and all its topics, send an email to elm-discuss+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--

Simon

unread,
Feb 26, 2017, 11:44:52 AM2/26/17
to Elm Discuss, vin...@jousse.org
For what I needed to do (convert an enter into a 'finished editing' action i found quite a nice solution


In brief, I catch the keydown event but convert anything other than an enter in to json decode fail, so that it does not reach the update loop. An enter is caught, with preventDefault applied, and is passed to update as a finished editing action, which I pass to Dom.blur and then catch the blur event and use that to cause the model to updated.

Simon

--
You received this message because you are subscribed to a topic in the Google Groups "Elm Discuss" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elm-discuss/YKz8rgffoWc/unsubscribe.
To unsubscribe from this group and all its topics, send an email to elm-discuss...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--
Reply all
Reply to author
Forward
0 new messages