JordanMarr / fable-lit-fullstack-template

A SAFE-style template with Fable.Lit, Fable.Remoting and Giraffe
MIT License
58 stars 1 forks source link

Include a basic CRUD in template #6

Open leolorenzoluis opened 2 years ago

leolorenzoluis commented 2 years ago

I don't have background with web components yet, but it would be ideal and useful if there is a sample application such as the TodoMVC that includes both Server/Client which has create, edit, delete functionality. I'm trying to implement it but I'm still reading through and understanding how it all works.

Maybe instead of just calling a simple rest API for displaying Cats, I believe it would be useful as a starter to have a Cat forms page that shows how it all connects.

JordanMarr commented 2 years ago

I wrote a blog post tutorial that creates a todo with update using Fable.Remoting + Fable.React + SQLProvider. Does this help?

https://jordanmarr.github.io/fsharp/safe-stack-ssdt-starter/

leolorenzoluis commented 2 years ago

It does. However, it's confusing to which one to use. Feels like if I'm using Lit then I'd mix it with Grapnel router library. If I want to use Elmish with Lit then what do I when picking router library? Do I want to mix it with react and leverage the Feliz library? I just want to kick start and get to writing code that builds the web app, and not having to connect the dots for different Fable libraries. I don't care if it's opinionated or not, but I believe if I'm using Lit, I really don't prefer to mix it with React (even if I could).

This is what I did to work around it, but not sure if I'm just hacking around the patterns.

<sl-button href="#" @click={fun _ -> router.navigate("/cat-facts")
                                                 (ChangePage ListCatFacts |> dispatch)} variant={navLinkIsActive ListCatFacts} outline>

but then I get stuck on what happens if I reload the page :)

I think it would be nice if there's a template where I can look at a minimal full blown app mixing with Elmish and Lit with routing.

JordanMarr commented 2 years ago

There should be no need to mix with React.

You should not be calling ChangePage ListCatFacts |> dispatch in your click event handler. That should happen when initializing the Grapnel routes. The click event should just call router.navigate:

<sl-button href="#" @click={fun _ -> router.navigate("/cat-facts")} variant={navLinkIsActive ListCatFacts} outline>

If you want to use Elmish instead of Hook.useEffectOnce, you should just initialize Grapnel router within the Elmish init.

JordanMarr commented 2 years ago

Actually it will be much easier to initialize Grapnel router using Hook.useEffectOnce (even if you are using Elmish):

I have updated App.fs to use the Hook.useElmish in conjunction with Hook.useEffectOnce.

You can use a standard global Elmish loop if you want, but I personally prefer using the Hook.useElmish to contain the Elmish loop within each component.

leolorenzoluis commented 2 years ago

That's how I ended up doing, but do you mind to share how will we implement a parent child elmish in conjunction with Lit components? Do we leverage the Intent pattern and every parent will have to know the dispatch message to be passed to the child? What about if I need to mix it with a message from sidebar to a main page? Will each main page component know about the sidebar message in Elmish then?

There's no similar concept AFAIK for like theming styles in a global similar to react using contexts.

JordanMarr commented 2 years ago

I would just wrap the parent dispatcher in a function and pass it to the child as a command. Would that work for your scenario?

leolorenzoluis commented 2 years ago

I get what you're saying, but not sure how to write it. I have web sockets that the parent component is subscribed to its message. Not sure how to pass the update message down to the child if the child component uses its own Hook.useElmish.

What I used to do was I had 1 big giant MVU architecture when there's like a global state where it knows the current state of the model for different pages. If I'm building a web app with a bunch of components/pages, that could be cumbersome and tedious since it has to know a lot just to push the changes down to the children.

JordanMarr commented 2 years ago

It sounds like you have two options:

1) Componentized Hook.useElmish

Your parent subscribes and updates its own model with latest message. Then the child component is passed the updated model.Message.

match model.CurrentPage with
| Page.ChildPage -> ChildPage.Page(model.Message)

Then within your child component, you would need to watch for the change like this:

[<HookComponent>]
let Page(webSocketMessage: string) =
    let model, dispatch = Hook.useElmish(init, update)

    Hook.useEffectOnChange(webSocketMessage, fun webSocketMessage -> 
        dispatch (HandleWebSocketMessage webSocketMessage)
    )

2) Global Elmish

If you have a lot of child components that rely on shared state, then you may want to go back to using a global Elmish loop instead of using componentized Hook.useElmish.

leolorenzoluis commented 1 year ago

I ended up doing Global elmish. :(

Do you have a sample how to do a quick one with <form>? Do I have to define all Msg if I go that route? I am aware of the Fable.Form library but I can't grok how to define my own custom http handler, so I just want to use a pure HTML from Fable with Lit.

JordanMarr commented 1 year ago

Since there is not currently a useContext hook, I wonder if you could do something as simple as storing the shared context in a mutable static binding and in your child components with

    Hook.useEffectOnChange(AppContext.ctx, fun ctx -> 
        dispatch (UpdateSignedInUser ctx.Username)
    )
JordanMarr commented 1 year ago

Do you have a sample how to do a quick one with <form>? Do I have to define all Msg if I go that route? I am aware of the Fable.Form library but I can't grok how to define my own custom http handler, so I just want to use a pure HTML from Fable with Lit.

Don't create a message to set every field; you can do that, but it obviously results in a lot of clutter and is a maintenance hassle. It's generally better to just create a single message for updating each entity.

type Msg = 
    | SetVehicle veh
    | SetOccupant occ
    // etc...

and then let the input changed event select the actual property:

fun e -> dispatch (SetVehicle { model.Vehicle with PlateNo = e.target.Value })

I don't use any form libs because they are confusing to me, and I like to keep things simple.

Also, I will be adding form validation to the template soon.

leolorenzoluis commented 1 year ago

Same here.

I settled with pure html with Lit only. I don't like learning another library and it's not easy to follow with. Looking forward how you added form validation.

I encountered something with the <form> tag though, not sure if it's just lack of understanding of being a web developer, but with MVU approach. Would you leverage the form attributes such as action and method, or would you just bind the button inside the form to a dispatch message? Because if I use forms then obviously if I click the button then the page will reload/try to go to that action.

JordanMarr commented 1 year ago

No need to specify form action/method since we will be handling everything ourselves via Fable.Remoting. You could use a regular button click, but better to use the conventional form @submit event.

Here is an example:

<form @submit={Ev (fun e -> e.preventDefault(); save())}>

    <sl-input 
        label="Client" 
        class="label-on-left"
        .value={project.Client}
        @sl-change={Ev (fun e -> setProject { project with Client = e.target.Value })}>
    </sl-input>

    <sl-input 
        label="Name" 
        class="label-on-left"
        .value={project.Name}
        @sl-change={Ev (fun e -> setProject { project with Name = e.target.Value })}>
    </sl-input>

    <div style="width: 245px">
        <sl-button type="submit" style="float: right; width: 120px;" variant="primary">Save</sl-button>
        <sl-button style="float: right; width: 120px;" variant="default" @click={Ev (fun e -> close())}>Cancel</sl-button>                    
    </div>

</form>
leolorenzoluis commented 1 year ago

@JordanMarr Awesome. What about having a sidebar and main page implementation? How would you implement that? Would you consider using child routers or would you just rely on the MVU approach where Parent component has the states for both side/main page?

Think of a ListView on the left and when I select something it propagates the right side (main page)?

JordanMarr commented 1 year ago

Grapnel router doesn't appear to support sub routes.

Assuming you have a page with tabs: I would probably just pass the tab name to the page. You can create a DU for your parent page that has a tab string. Then optionally pass in a tab in your route:

    type Page = 
        | ParentPage of tab: string

    Hook.useEffectOnce(fun () -> 
        router.get("/parent-page/:tab", fun (req: Req<{| tab: string |}>) -> 
            dispatch (SetCurrentPage (Page.ParentPage req.``params``.tab))
        )
    )
JordanMarr commented 1 year ago

@leolorenzoluis, new stuff has been added: https://twitter.com/jordan_n_marr/status/1553107245868748804

leolorenzoluis commented 1 year ago

Great stuff! Will check out. Have you tried interfacing with charts? It's either I hand craft the code for canvas manipulation using Fable (which I don't prefer) or use an existing chartJS library (which I use), but there's no guidance when it comes to manipulating it in MVU pattern or even if either Lit/React owns the state.

What I did was to trigger a message that the dynamic manipulation (side effects) happen at the update. Not sure if I'm on the right direction.

JordanMarr commented 1 year ago

Were you able to get it working? I guess it just depends on which library you are using.

The ideal graph library would be a Web Component which would allow you declaratively set properties via the lit .property={} notation. However, if you have to do it via code, then the side-effect approach you are using seems like it could work.

leolorenzoluis commented 1 year ago

Yeah I got it working. I'm just not sure if it's the right pattern to put in the update. One can just rewrite similar functionality from d3 or other chart libraries in Fable to make it friendly with F# 👍