fable-compiler / fable-react

Fable bindings and helpers for React and React Native
MIT License
275 stars 66 forks source link

Add TinyElmishComponent #44

Closed zaaack closed 6 years ago

zaaack commented 6 years ago

Seamlessly stolen from reason-react πŸ˜„ .

Five minutes for an explanation. I know we already have awesome Elmish library, and we should follow the Elm architecture. But in my practice I found sometimes a component with internal state is very useful.

e.g. a ComfirmButton

untitled3

It's just a enhance of a delete button, we use it very often and pass it's state around is a little redundant. But if we implement with React.Component, then calling setState everywhere would make the code unreadable very quickly.

The ReducerComponent/TinyElmishComponent makes it much cleaner for medium-size Component with, we can create a react component still follow the elm architecture like this:

type Msg =
| Increase
| Decrease

type [<Pojo>] State = {
    counter: int
}

type Counter(props) as this =
    inherit TinyElmishComponent<obj, State, Msg>(props)
    do this.setInitState { counter=0 }

    override x.update msg state =
        match msg with
        | Increase -> {state with counter = state.counter+1}
        | Decrease -> {state with counter = state.counter-1}

    member x.render () =
        div [] [
            button [ OnClick (fun _ -> x.dispatch Increase) ] [ str "+" ]
            span [] [str <| string x.state.counter]
            button [ OnClick (fun _ -> x.dispatch Decrease) ] [ str "-" ]
        ]

BTW, TinyElmishComponent maybe a little too long, a new concise name is welcome!

forki commented 6 years ago

Very cool

Am 17.11.2017 04:27 schrieb "Zack Young" notifications@github.com:

Seamlessly stolen from reason-react https://reasonml.github.io/reason-react/docs/en/state-actions-reducer.html#actions-reducer πŸ˜„ .

Five minutes for a explanation. I know we already have awesom Elmish library, and we should follow the Elm architecture. But in my practive I found sometimes a component with internal state is very useful.

e.g. a ComfirmButton

[image: untitled3] https://user-images.githubusercontent.com/5233940/32927774-94b76844-cb89-11e7-9e6d-030aad48d14a.gif

It's just a enhance of a delete button, we use it very often and pass it's state around is a little redundant. But if we implement with React.Component, then calling setState everywhere would make the code unreadable very quickly.

The ReducerComponent/TinyElmishComponent makes it much cleaner for medium-size Component with state.

Then we can create a component with tiny elm architecture like this:

type Msg =| Increase| Decrease type [] State = { counter: int}type Counter(props) as this = inherit TinyElmishComponent<obj, State, Msg>(props) do this.setInitState { counter=0 }

override x.update state msg =
    match msg with
    | Increase -> {state with counter = state.counter+1}
    | Decrease -> {state with counter = state.counter-1}

member x.render () =
    div [] [
        button [ OnClick (fun _ -> x.dispatch Increase) ] [ str "+" ]
        span [] [str <| string x.state.counter]
        button [ OnClick (fun _ -> x.dispatch Decrease) ] [ str "-" ]
    ]

You can view, comment on, or merge this pull request online at:

https://github.com/fable-compiler/fable-react/pull/44 Commit Summary

  • Add TinyElmishComponent

File Changes

Patch Links:

β€” You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/fable-compiler/fable-react/pull/44, or mute the thread https://github.com/notifications/unsubscribe-auth/AADgNJrBKLUD05_XtqjrbfZhwNWwb3HXks5s3P0sgaJpZM4QhfG7 .

alfonsogarciacaro commented 6 years ago

I think this is a very good idea :+1: I use myself stateful React components in my Elmish applications because, as you say, it's a bit cumbersome to put every tiny flag in the global state. I would just suggest that, if we want to provide a helper for this maybe we should try to remove boilerplate and avoid of the need of implementing a class. This is a bit hacky but we could take advantage of the fact you can have class expressions in JS and do something like:

type ElmishComponent =
    interface end

[<Emit("""(class extends $0 {
  constructor(props) { super(props); this.state = $1(); }
  render() { return $3(this.state, msg => this.setState($2(msg, this.state))) }
})""")>]
let private mkElmishComponentPrivate
    (reactComponent: System.Type)
    (init: unit->'S)
    (update: 'Msg->'S->'S)
    (view: 'S->('Msg->unit)->ReactElement): ElmishComponent = jsNative

let mkElmishComponent (init: unit->'S)
                      (update: 'Msg->'S->'S)
                      (view: 'S->('Msg->unit)->ReactElement): ElmishComponent =
    mkElmishComponentPrivate (typedefof<React.Component<obj,'S>>) init update view

let inline renderElmishComponent (com: ElmishComponent): ReactElement =
    createElement(com, null, [])

let inline mountElmishComponent (domElId: string) (com: ElmishComponent): unit =
    ReactDom.render(createElement(com, null, []), Browser.document.getElementById("elmish-app"))

This way we can have a full Elmish app (well, sort of) with:

type [<Pojo>] Model = { counter: int }
type Msg = Increment | Decrement

let init() : Model = { counter = 0 }

let update (msg:Msg) (model:Model) =
    match msg with
    | Increment -> { counter = model.counter + 1 }
    | Decrement -> { counter = model.counter - 10 }

open Fable.Helpers.React.Props
module R = Fable.Helpers.React

let view model dispatch =
  R.div []
      [ R.button [ OnClick (fun _ -> dispatch Decrement) ] [ R.str "-" ]
        R.div [] [ R.str (sprintf "%i" model.counter) ]
        R.button [ OnClick (fun _ -> dispatch Increment) ] [ R.str "+" ] ]

mkElmishComponent init update view
|> mountElmishComponent "elmish-app"

I'm also open to naming and whether this should go in a separated module, etc. I would also like to know @et1975 and @MangelMaxime opinion about if we should use or avoid the term Elmish to avoid confusion.

zaaack commented 6 years ago

@alfonsogarciacaro You made it further, now it looks more like https://github.com/evancz/react-elm-components , which I'm also thinking about porting to fable-elmish πŸ˜„ . And I'm looking forward to @et1975 and @MangelMaxime 's opinion, too!

MangelMaxime commented 6 years ago

Not sure of the benefit of this but could be a good addition probably :)

After, I don't really like branding it with Elmish in the name because you case use it without Elmish if you want. Probably something like ReactiveComponent or related to FRP would be better but not sure.

alfonsogarciacaro commented 6 years ago

Sorry @zaaack I got some ideas inspired by your PR so I submitted another one #46 πŸ˜…

I agree that using Elmish may be confusing so I changed to StatefulCom while keeping a similar syntax. I also removed the restriction that the state must be a Pojo type and include also props and children. This may make the view function a bit more complicated but it gives more flexibility to integrate the component in a bigger app.

let coloured (color: string) (content: obj) =
    R.span [Style [BackgroundColor color]] [R.str (string content)]

let view (props: Props) children model dispatch =
  R.div []
      [ yield R.button [ OnClick (fun _ -> dispatch Decrement) ] [ R.str "-" ]
        yield coloured props.color model.counter
        yield! children
        yield R.button [ OnClick (fun _ -> dispatch Increment) ] [ R.str "+" ] ]

let com = makeStatefulCom init update view

R.div [] [
    renderStatefulCom com { color="red" } []
    renderStatefulCom com { color="blue" } [
        coloured "yellow" "foo"
        coloured "green" "bar"]
]
|> mountById "elmish-app"
zaaack commented 6 years ago

@alfonsogarciacaro One more question, statefulComponent is already a widely known concept in react world (to be distinguished from stateless component), maybe it sounds a little confusing? .

alfonsogarciacaro commented 6 years ago

Well, it's a helper to create stateful components :) I just had a look to the React bindings and didn't seem to conflict with anything. But you're a right, it may lead the user to think it corresponds 1:1 to "stateful components" as explained in React tutorials. Any suggestion for alternatives?

zaaack commented 6 years ago

@alfonsogarciacaro I think ReactiveComponent from @MangelMaxime is Ok for me. Just thinking about a not much hacky way to implement your init/update/view architecture, by passing the functions to props, since we can wrap the Props type just like State.

Maybe a little late? ; )


type [<Pojo>] Props<'P, 'S, 'Msg> = {
    props: 'P
    update: 'Msg -> 'S -> 'S
    view: Model<'P, 'S> -> ('Msg->unit) -> ReactElement
    init: 'P -> 'S
}

and [<Pojo>] State<'T> = {
    value: 'T
}

and [<Pojo>] Model<'P, 'S> = {
    props: 'P
    state: 'S
    children: ReactElement list
}

and ReactiveComp<'P, 'S, 'Msg>(props) as this=
    inherit Component<Props<'P, 'S, 'Msg>, State<'S>>(props)
    do this.setInitState { value = props.init(props.props) }

    member x.dispatch msg =
        x.setState <| {value = props.update msg x.state.value }

    member x.render() =
        let model =
            { state = x.state.value
              props = props.props
              children = x.children |> Array.toList }
        props.view model x.dispatch

let renderStatefulCom<'P, 'S, 'Msg>
        (init: 'P -> 'S)
        (update: 'Msg -> 'S -> 'S)
        (view: Model<'P, 'S> -> ('Msg->unit) -> ReactElement)
        (props: 'P)
        children =
    com<ReactiveComp<'P, 'S, 'Msg>, Props<'P, 'S, 'Msg>, State<'S>>
        { props=props; update=update; view=view; init=init }
        children

module Counter =

    type Msg =
    | Increase
    | Decrease

    let init props = props

    let update msg state =
        match msg with
        | Increase -> state + 1
        | Decrease -> state - 1

    let view model dispatch =
        div [] [
            button [ OnClick (fun _ -> dispatch Increase) ] [ str "+" ]
            span [] [str <| string model.state]
            button [ OnClick (fun _ -> dispatch Decrease) ] [ str "-" ]
            div [] model.children
        ]
    let render =
        renderStatefulCom init update view
et1975 commented 6 years ago

EDIT: Sorry, misread the repo :) Please ignore.

alfonsogarciacaro commented 6 years ago

@zaaack Hehe, you ruined my hacky way :wink: Just kidding, this is a much better way (it's actually me who always discourages the use of the Emit attribute). Can you please update the PR so we can merge it? Just one quick note though, be careful when referencing props in the render method. If you use props.props, F# will capture the instance passed to the constructor so it won't contain the updated values, x.props must be used instead.

Don't worry about being late, I just merged the other PR to prevent conflicts, but it hasn't been published yet. Also, ReactiveComponent (or ReactiveCom for short) sounds good! :+1:

zaaack commented 6 years ago

@alfonsogarciacaro Thanks for point it out, didn't notice the differences of the props from the constructor. Updated, and added a module to prevent polluting the global namespace.

And your idea is great πŸ‘ , it looks much clear and concise now then original OO style of components.

alfonsogarciacaro commented 6 years ago

Awesome, thank you!