1

In my Fable app with Elmish I have a view that uses react-slick and a button that should be able to change the slide number on click:

Fable.Import.Slick.slider
  [ InitialSlide model.SlideNumber
    AfterChange (SlideTo >> dispatch) ]
  children

Button.button
  [ Button.OnClick (fun _ev -> dispatch (SlideTo 5)) ]
  [ str "Go to slide 5" ]

The react component for the slider is defined by react-slick.
The fable wrapper I had to write on my own, so it's not complete because I only defined the properties I need.

module Fable.Import.Slick

open Fable.Core
open Fable.Core.JsInterop
open Fable.Helpers
open Fable.Helpers.React.Props
open Fable.Import.React

type SliderProps =
    | InitialSlide of int
    | AfterChange of (int -> unit)
    interface IHTMLProp

let slickStyles = importAll<obj> "slick-carousel/slick/slick.scss"
let slickThemeStyles = importAll<obj> "slick-carousel/slick/slick-theme.scss"
let Slider = importDefault<ComponentClass<obj>> "react-slick/lib/slider"
let slider (b: IHTMLProp list) c = React.from Slider (keyValueList CaseRules.LowerFirst b) c

So while react-slick defines a property InitialSlide to set the slide that should initially be shown, there's no property to update the slide afterwards. There is a method slickGoTo that should do what I want. But I don't know how or where to call that method while still being compliant with Elmish.

I could imagine that I have to extend the react component and listen to the model property and then call slickGoTo whenever that property changes. But I don't know if that's possible.

So my question is: How can I have a button that changes the slide number on click using slickGoTo that is defined by the component while not hacking the Elmish architecture?

Johannes Egger
  • 3,874
  • 27
  • 36
  • Nope, that doesn't work. I think it's because the component doesn't *observe* property changes. Are you familiar with react-slick? Or are react properties typically observed by the component? – Johannes Egger Jan 13 '19 at 08:12
  • `initialSlide` is only used [once](https://github.com/akiran/react-slick/blob/master/src/inner-slider.js#L38) I think. – Johannes Egger Jan 13 '19 at 09:30

3 Answers3

1

To be able to call the method slickGoTo you need to get a reference to the slider object. To do that use the Ref prop that expects a function of type (Browser.Element -> unit). It gets called once. Save that element somewhere, perhaps in your model:

type Model = {
    ...
    slideNumber : int
    sliderRef   : Browser.Element option
    ...
}

let gotoSlide sliderRef n = 
    match sliderRef with None ->
    | None -> () 
    | Some slider -> slider?slickGoTo n

That way you can call gotoSlide from your update function.

type Msg =
| SetSliderRef of Browser.Element
| CurrentSlide of int
| GotoSlide    of int
...

let update msg model =
    match msg with
    | SetSliderRef e -> { model with sliderRef   = Some e }  , []
    | CurrentSlide n -> { model with slideNumber = n      }  , []
    | GotoSlide    n -> gotoSlide model.sliderRef n ;   model, []
    ...

Create your slide like this:

slider 
    [ Ref          (SetSliderRef >> dispatch) 
      AfterChange  (CurrentSlide >> dispatch)
      InitialSlide 0
    ]
    slides

I have not tested any of this, so take with a grain of salt.

AMieres
  • 4,944
  • 1
  • 14
  • 19
  • Thanks. That works. However that's what I meant with *hacking the Elmish architecture* - I should have been more clear about that. I don't want to bring a mutable `Browser.Element` into the otherwise immutable state. It feels a bit dirty, doesn't it? – Johannes Egger Jan 13 '19 at 12:50
  • If you add it to the model then it is not mutable (check the updated version). Still, you are using a side-effect call in the update function which makes it impure, but that is unavoidable since the slider forces us to do that. You could create a react-component to encapsulate the dirty part but it is too much work for little gain. – AMieres Jan 13 '19 at 13:02
  • But `Browser.Element` is mutable, so I can't reliably do e.g. time-travel-debugging. It just feels dirty to add a mutable thing to the state. The side-effect-call could be put in a `Cmd` which makes it slightly cleaner IMO. I wonder if [`ReactiveCom`](https://github.com/fable-compiler/fable-react/blob/master/src/Fable.React/Fable.Helpers.React.fs#L1264) could wrap that ugly part. Your hint using `Ref` might help here, I'll check that. – Johannes Egger Jan 13 '19 at 15:11
  • It is not mutable in the model (state). The whole state changes but not any specific element in it. It gets replaced by a new state every time update is called. So no, it is not mutable. – AMieres Jan 13 '19 at 15:13
  • But [`Browser.Element`](https://github.com/fable-compiler/fable-import/blob/master/Browser/Fable.Import.Browser.fs#L2496) defines a lot of properties that are mutable (`id`, `classList`, etc.), so if you change that object, it changes in all the state snapshots because it's the same reference, isn't it? – Johannes Egger Jan 13 '19 at 16:50
  • 1
    True, but the value you are holding and that you care about is the reference to the object and that is immutable. To make it more clear you could instead store the function `gotoSlide` already curried with the reference to `sliderRef` that way is clear that you cannot alter anything inside the object except for the side effects of calling the function. Conceptually is the same. – AMieres Jan 13 '19 at 17:08
  • The immutable reference is a good argument. I still don't completely like the idea of storing a complete dom node inside the Elmish state (too much unnecessary data as well as more difficult serialization) and I also have some concerns about storing a function with a side-effect inside my state. But I will keep it in mind as a last resort so to say ;-) – Johannes Egger Jan 13 '19 at 18:35
  • Alright! If you find a better solution don't forget to post it. – AMieres Jan 13 '19 at 18:48
  • Alternatively you can store the object reference in a mutable global variable outside the model. Looks more like a hack but it doesn't mess with your state keeping it serializable. Maybe that is more appropriate given the nature of the fix. – AMieres Jan 13 '19 at 18:54
  • OK thanks. I think I just found one. I'll post it as an answer because it's too long to explain here. But please have a look at it because it doesn't seem super-elegant. – Johannes Egger Jan 13 '19 at 19:01
1

An alternative to storing the reference to the slider object in the model is to use a mutable variable:

let mutable sliderRef   : Browser.Element option = None

let gotoSlide n = 
    match sliderRef with None ->
    | None -> () 
    | Some slider -> slider?slickGoTo n

the rest is similar:

type Msg =
| CurrentSlide of int
| GotoSlide    of int
...

let update msg model =
    match msg with
    | CurrentSlide n -> { model with slideNumber = n      }  , []
    | GotoSlide    n -> gotoSlide n ;                   model, []

Create your slider like this:

slider 
    [ Ref          (fun slider = sliderRef <- Some slider) 
      AfterChange  (CurrentSlide >> dispatch)
      InitialSlide 0
    ]
    slides
AMieres
  • 4,944
  • 1
  • 14
  • 19
  • I finally use this one now. Although I don't completely like it, I do like the fact that I can call `slickGoTo` and animate the sliding (`slider?slickGoTo(n, false)`). – Johannes Egger Jan 16 '19 at 15:05
0

I just found that within React you previously solved such things using componentWillReceiveProps - that sounds a lot like what I want IMO. But this is now obsolete and there are two recommendations based on my usage:

If you used componentWillReceiveProps to “reset” some state when a prop changes, consider either making a component fully controlled or fully uncontrolled with a key instead.

I don't think I can implement the Fully controlled version (this has to be done by the author of the component, right?), but the fully uncontrolled with a key seems to work. So my slider view now looks like this:

Fable.Import.Slick.slider
    [ InitialSlide model.SlideNumber
      AfterChange (SlideTo >> dispatch)
      Key (string model.SlideNumber) ]

According to the docs everytime the Key changes, the component is recreated. Now this sounds like a lot of overhead and has its own drawbacks (animating the slide doesn't work) but currently it's the simplest solution. Any concerns?

Johannes Egger
  • 3,874
  • 27
  • 36
  • It is ugly but it maybe alright if it works for your solution. Watch out for memory leakage. I would say test both options (global mutable and this) see which one performs and feels better then go with that. – AMieres Jan 13 '19 at 19:22
  • Global mutable and this is the same type of uglyness, IMO. But I think I'll implement one of them. Thanks for your help. I'll wait a bit longer before accepting an answer. – Johannes Egger Jan 13 '19 at 20:45
  • Maybe you can add the idea with the global mutable as an answer. Currently I prefer that. – Johannes Egger Jan 13 '19 at 20:47