Skip to content

Instantly share code, notes, and snippets.

@rtfeldman
Last active January 14, 2017 16:13
Show Gist options
  • Save rtfeldman/5f015adbdfbba541c7e7e1409b6efeef to your computer and use it in GitHub Desktop.
Save rtfeldman/5f015adbdfbba541c7e7e1409b6efeef to your computer and use it in GitHub Desktop.
{-| DOM
## Focus
@docs focus, blur
## Measurement
@docs measureText, bounds, Bounds
## Scrolling
@docs getScrollTop, setScrollTop, getScrollLeft, setScrollLeft
## Selecting
@docs SelectorError
-}
{-| After the next render, runs [`document.querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
on the given selector and [sets focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the
resulting element.
Html.focus "#searchInput"
NOTE: setting focus can silently fail if the element is invisible. This could be captured as an error by checking to see
if document.activeElement actually got updated to the element we selected. https://jsbin.com/xeletez/edit?html,js,output
-}
focus : String -> Task SelectorError a
{-| After the next render, runs [`document.querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
on the given selector and [removes focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur) from the
element it finds.
Dom.blur "#searchInput"
-}
blur : String -> Task SelectorError a
{-| The [`scrollTop`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop) of the element returned
by the given [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector).
-}
getScrollTop : String -> Task SelectorError Float
{-| Set the [`scrollTop`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop) of the element returned
by the given [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector).
-}
setScrollTop : String -> Float -> Task SelectorError Float
{-| The [`scrollLeft`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft) of the element returned
by the given selector.
-}
getScrollLeft : String -> Task SelectorError Float
{-| Set the [`scrollLeft`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft) of the element returned
by the given selector.
-}
setScrollLeft : String -> Float -> Task SelectorError Float
{- Find out how much space styled text takes up. You can provide an optional
fixed width.
-}
measureText : List ( String, String ) -> Maybe Float -> String -> ( Float, Float )
{- Calls [`getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) on the element returned
by the given [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector).
-}
bounds : String -> Task SelectorError Bounds
type alias Bounds =
{ top : Float, bottom : Float, left : Float, right : Float }
{- Describes the result of a failed call to [`document.querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector).
`InvalidSelector` represents a [`SYNTAX_ERR` exception](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector#Notes).
`ElementNotFound` represents the case where `document.querySelector` did not find an element.
-}
type SelectorError
= InvalidSelector String
| ElementNotFound
@rtfeldman
Copy link
Author

Thanks for all the comments! I don't feel like I have good solutions to the concerns raised about race conditions or the drawbacks of querySelector, so I summarized the discussion so far in order to give Evan a picture of where we are on this.

(He's finishing Guide stuff at the moment, and I know that's higher-priority, but based on past experiences I trust his judgment to resolve these points.)

Let me know on that thread if people think my summary missed anything important!

@debois
Copy link

debois commented Jul 21, 2016

Regarding scrollTop and @Gozala's neat comments above: In the current implementation of virtualdom, you cannot use, e.g., Html.Attributes.property "scrollTop" (Json.int 256) to set scrollTop. As it happens, properties are applied before children are populated, so on initial render, you're scrolling something with height 0. See here, default case applies.

@rtfeldman
Copy link
Author

rtfeldman commented Jul 22, 2016

@Gozala @debois I want to follow up a bit on this:

In fact value, selectionStart & selectionEnd of the input element are all very similar to scrollTop in regards that they can be mutated by user and if you are not listening to an appropriate DOM events and update your model accordingly your model & actual view state can get out of sync. Elm right now only deals with value, but same question(s) apply to it just as well: How ofter & when do you update value on the input field (I think all the other questions are really a subset of this one) ? Current answer is: On new render after model changes cause value of generated view to be updated.

Our production experience with value has been that it's irreparably broken. It has such nasty and unfixable race condition problems, our best practice has become to never use it and instead only to ever use defaultValue.

With value, users who type too fast (or are on older machines, or are using IE) report dropped characters and/or the cursor startlingly jumping to the end of the text field, leading them to suddenly type the wrong thing. Before we switched to defaultValue, our Webdriver-powered integration tests had to fill in form fields with one letter, because Webdriver "types" so fast that if we tried to have it type "Richard" into a form field, the form field would get filled in with some mangled version like "Ricrdha" because of value and its race condition problem, causing the test to fail.

I personally recommend that no one ever use value for input or textarea elements that users can type in, and I think having further APIs modeled after this (especially scrolling and text selection) would run into the same problems.

Granted, I can see how Task has its own problems with race conditions, but using a declarative Model -> Html approach to try and render anything where the Model is not the single source of truth for the state seems doomed to bugginess, and therefore a nonstarter. It seems like finding a way to have a Task run inside the same clock tick (which is a prerequisite for things like clipboard interactions where the browser's security model requires that anyway) is a more realistic path to explore.

What do you think?

@sylvainf911
Copy link

As I said in https://groups.google.com/d/msg/elm-dev/ThkWudq7SF0/77mbgGNoCwAJ the use of properties like scrollTop does not work. I did not experience problems with value but I did not use automated Webdriver like @rtfeldman.

One thing I had to create native lib for was adjusting the scrollTop after the content was changed in the view. When you scroll down and add some data to the model at the end of a list, there is no problem. But when you scroll up (prepend in the model's list) (think like Facebook messaging where you are at the end of the conversation first and autoload when scrolling up). You have to take the scrollHeight just before the DOM is updated, then update the DOM and then set scrollTop to a position equivalent of where it was before (el.scrollTop = el.scrollTop + (el.scrollHeight - oldHeight). Right now, I was not able to do it without a little glitch on the screen when repositioning. I don't know if this problem can be managed with the "in same tick" technique your are looking for.

Thank you.

@rtfeldman
Copy link
Author

I did not experience problems with value but I did not use automated Webdriver like @rtfeldman.

Being a race condition, reproducing it can be tricky depending on your device and such, but Kris did a good writeup of the problem including a demo of how to consistently reproduce it.

(Amusingly, later in that thread I suggested trying alternatives to defaultValue. I later talked about it with a bunch of people and concluded that defaultValue was the least bad alternative that actually solved the problem, similar to my conclusion here about querySelector. 😄)

@Gozala
Copy link

Gozala commented Aug 2, 2016

I have being thinking more about the problem of CSS selectors as poor way to reference Elements in the DOM and Declarative VS Task based approach for updating certain DOM properties. And I had a thought that there could be a way to solve both problems in a neat way. What if something along this lines was added to elm-html:

taskAttribute : (DOMElement -> Task Never msg) -> Html.Attribute msg

focus : DOMElement -> Task SelectorError msg
blur : DOMElement -> Task SelectorError msg
scrollLeft : Float -> DOMElement -> Task SelectorError Float

That way those task could be turned into attributes and elm-html can take care of running them with an appropriate DOM elements. Better yet it could actually recognize such tasks and run them in the same tick to avoid races we've talked about.

It not fully fleshed out idea, but I though I'd share it anyway to get other minds think about it.

@Gozala
Copy link

Gozala commented Aug 2, 2016

To be totally clear I don not suggest that Elm should expose raw DOMElement to Elm programs. It most definitely should not, but there could be a type like DOMElement (or Ref or whatever you want to call it) that is never given to an Elm program but can be used to connect different native APIs in this case connecting elm-html diff / patch process with with tasks like focus / blur that also likely will be implemented in native.

@Gozala
Copy link

Gozala commented Aug 2, 2016

I guess slightly adjusted API would more consistent with Task.perform and more convenient to use:

taskAttribute : (x -> msg) -> (a -> msg) -> (DOMElement -> Task x a) -> Html.Attribute msg

focus : DOMElement -> Task SelectorError a
blur : DOMElement -> Task SelectorError a
scrollLeft : Float -> DOMElement -> Task SelectorError Float

@Gozala
Copy link

Gozala commented Aug 2, 2016

Our production experience with value has been that it's irreparably broken. It has such nasty and unfixable race condition problems, our best practice has become to never use it and instead only to ever use defaultValue.

With value, users who type too fast (or are on older machines, or are using IE) report dropped characters and/or the cursor startlingly jumping to the end of the text field, leading them to suddenly type the wrong thing. Before we switched to defaultValue, our Webdriver-powered integration tests had to fill in form fields with one letter, because Webdriver "types" so fast that if we tried to have it type "Richard" into a form field, the form field would get filled in with some mangled version like "Ricrdha" because of value and its race condition problem, causing the test to fail.

I personally recommend that no one ever use value for input or textarea elements that users can type in, and I think having further APIs modeled after this (especially scrolling and text selection) would run into the same problems.

I commented in the referenced writeup but think it's worth brining this up here as well. In my experience (limited to the single browser engine) issue is not actually related to the race conditions and specifically blamed animation frames. But rather issue is related to imperfect modeling of the state and than unintentional mutation of DOM state. More specifically most of the code I've seen dealing with input fields does not model selection range and by consequence cursor position. It may not be apparent but when you do not model selection what you end up doing is actually modeling selection as being empty at the end of the input.value. So every time input.value is set selection state get's mutated and in a way that messes up input. In my experience tracking selection and modeling it in the state and then updating selection along with value resolved all the issues that seemed like race conditions. Better yet unlike react we kept animation frame based render loop which is not the issue. If your model state matches actual DOM state then later delayed DOM patches are NoOp and there for don't interfere with input value changes at all. I am happy to share more details in a different thread if there is an interest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment