Giter Site home page Giter Site logo

orus-io / elm-spa Goto Github PK

View Code? Open in Web Editor NEW
45.0 45.0 8.0 105 KB

Pure Elm library to easily build Single Page Applications

Home Page: https://package.elm-lang.org/packages/orus-io/elm-spa/latest/

License: MIT License

Elm 100.00%
elm single-page-app spa

elm-spa's People

Contributors

ccomb avatar cdevienne avatar faide avatar vjousse avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

elm-spa's Issues

Application loses state on hot reload

Hi. It seems like my whole application loses state each time i do some code changes, have you encountered the same issues? The time travel debugger resets and loses state and I get rerouted back to my start page.

Partial Application of mappers

Just looking at the README, seems redundant to apply the same mappers function to each invocation of addProtectedPage, addPublicPage.
Can it not be added to the configuration object?
I'd argue the same could be said for the match functions... Those could just all be one big Route.toPage : Url -> Page function passed to the config.

type Page
    = NotFound
    | Page1
    | Page2
    
toPage : Url -> Page
toPage url =
    case url.path of
        ["stuff"] ->
            Page1
          
        ["things"] ->
            Page2
          
        _ ->
            NotFound 

Or if the Maybe is used as a means of building the NotFound use case into the framework

type Page
    = Page1
    | Page2 { id : Maybe Int }

toPage : Url -> Maybe Page
toPage url =
    case url.path of
        ["stuff"] ->
            Just Page1

        ["things"] ->
            Just (Page2 (parseId url.query))

        _ -> 
            Nothing

Prevent rerender on route change

Is there a way to prevent calling the init function when the route changes (to the same page)?

Something like sending a FlagsChanged message when the route is same with different flags.

Use case would be a search field that reflects its state in the url. Currently it reloads the page on every keystroke.

PS: Thanks for the great package!

Subpages

Hi. I'm having some issues trying to add subpages, do you have any suggestion how to solve it?

Need some help regarding structuring of code

Hi, as always thanks for the great work on elm-spa!

I have an issue, it might just be that I've missed something but I hope maybe you could provide an example of how to do this.

So I have created an own module Confirm.elm which is a dialog with a confirm msg. some use case could be "are you really sure you want to delete this whatver{id}. I want to make a general function in Effect.elm which can be called on whatever page am I on. The problem here is that I struggle with the mapping of messages.

Lets say i have a Page.elm where i have a Msg DeleteWhatever Int, from here when i call the ConfirmDialog from Shared.elm i need some kind of mapping from Page Msg to Shared Msg.

I've been stuck on this for a long time and I really hope that you guys could help me out. I'll try to provide as much as info as i can for you to understand my implementation.

this is the implementation of Confirm.elm

{-| Contains a general type and combinators for a confirmation dialog.

You should prefer to use `init` and builder functions
rather than constructing this manually.

-}
type alias Confirm msg =
    { title : String
    , prompt : String
    , cancelText : String
    , confirmText : String
    , attributes : List (H.Attribute msg)
    , clickOutside : Bool
    , onConfirm : msg
    , onCancel : Maybe msg
    }


{-| Maps a function over `ConfirmConfig`
-}
map : (a -> msg) -> Confirm a -> Confirm msg
map f confirm =
    { title = confirm.title
    , prompt = confirm.prompt
    , confirmText = confirm.confirmText
    , cancelText = confirm.cancelText
    , attributes = List.map (A.map f) confirm.attributes
    , clickOutside = confirm.clickOutside
    , onConfirm = f confirm.onConfirm
    , onCancel = Maybe.map f confirm.onCancel
    }


{-| Set the title of a confirm.
-}
setTitle : String -> Confirm msg -> Confirm msg
setTitle title confirm =
    { confirm | title = title }


{-| Set the prompt text of a confirm.
-}
setPrompt : String -> Confirm msg -> Confirm msg
setPrompt prompt confirm =
    { confirm | prompt = prompt }


{-| Set the cancel button text of a confirm.
-}
setCancelText : String -> Confirm msg -> Confirm msg
setCancelText cancelText confirm =
    { confirm | cancelText = cancelText }


{-| Set the confirm button text of a confirm.
-}
setConfirmText : String -> Confirm msg -> Confirm msg
setConfirmText confirmText confirm =
    { confirm | confirmText = confirmText }


{-| Set the container attributes of a confirm.
The given attribute list will be applied to the outermost
dialog container.
-}
setAttributes : List (H.Attribute msg) -> Confirm msg -> Confirm msg
setAttributes attributes confirm =
    { confirm | attributes = attributes }


{-| Close the confirm dialog when the user clicks on
the overlay outside the dialog.
-}
closeOnClickOutside : Confirm msg -> Confirm msg
closeOnClickOutside confirm =
    { confirm | clickOutside = True }


{-| Set the confirm action of a confirm.
-}
setOnConfirm : msg -> Confirm msg -> Confirm msg
setOnConfirm onConfirm confirm =
    { confirm | onConfirm = onConfirm }


{-| Set the cancel action of a confirm.
-}
setOnCancel : msg -> Confirm msg -> Confirm msg
setOnCancel onCancel confirm =
    { confirm | onCancel = Just onCancel }


{-| `Confirm` with default values.

Using this function and provided builder functions should
be your preferred approach when creating `Confirm` instances.

-}
init : msg -> Confirm msg
init onConfirm =
    { title = "Confirm"
    , prompt = ""
    , cancelText = "Cancel"
    , confirmText = "Confirm"
    , attributes = []
    , clickOutside = False
    , onConfirm = onConfirm
    , onCancel = Nothing
    }

This is the implementation of main.elm

mappers : ( (a -> b) -> PageView a -> PageView b, (c -> d) -> PageView c -> PageView d )
mappers =
    ( PageView.map, PageView.map )


toDocument : Shared -> PageView (Spa.Msg Shared.Msg pageMsg) -> Document (Spa.Msg Shared.Msg pageMsg)
toDocument shared view =
    View.baseView shared view


main =
    Spa.init
        { defaultView = PageView.defaultView
        , extractIdentity = Shared.identity
        }
        |> Spa.addProtectedPage mappers (Route.matchAppRoot Route.Page1) Page1.page
        |> Spa.addProtectedPage mappers (Route.matchAppRoot Route.Page2) Page2.page
        |> Spa.beforeRouteChange Shared.beforeRouteChange
        |> Spa.application PageView.map
            { init = Shared.init
            , subscriptions = Shared.subscriptions
            , update = Shared.update
            , toRoute = Route.toRoute
            , toDocument = toDocument
            , protectPage = Route.toUrl >> Just >> Route.SignIn >> Route.toUrl
            }
        |> Spa.onUrlRequest Shared.clickedLink
        |> Browser.application

this is PageView.elm

type alias PageView msg =
    { dialogs : List (Dialog msg)
    , view : Html msg
    }


init : Html msg -> PageView msg
init view =
    { dialogs = []
    , view = view
    }


setView : Html msg -> PageView msg -> PageView msg
setView view pageView =
    { pageView | view = view }


setDialogs : List (Dialog msg) -> PageView msg -> PageView msg
setDialogs dialogs pageView =
    { pageView | dialogs = dialogs }


map : (a -> msg) -> PageView a -> PageView msg
map f pageView =
    { dialogs = List.map (Dialog.map f) pageView.dialogs
    , view = H.map f pageView.view
    }


defaultView : PageView msg
defaultView =
    init
        (H.div []
            [ H.text
                "You should not see this page unless you forgot to add pages to your application"
            ]
        )

and this is the baseView in View.elm

baseView : Shared -> PageView (Spa.Msg Shared.Msg pageMsg) -> Document (Spa.Msg Shared.Msg pageMsg)
baseView shared { view, dialogs } =
    { title = ""
    , body =
        viewHeader shared
            :: navigationRow shared.environment shared.currentRoute
            :: H.div [ A.class "page_content_container" ]
                [ view
                ]
            :: Dialog.viewList dialogs
            :: (shared.toasts
                    |> Toast.viewList
                    |> H.map (\( id, msg ) -> Spa.mapSharedMsg (Shared.setToastMsg id msg))
               )
            :: List.map
                (\error ->
                    error
                        |> Dialog.fromError shared.environment.language (Spa.mapSharedMsg Shared.closeErrorDialog)
                        |> Dialog.view
                )
                (List.take 1 shared.errors)
            ++ List.map
                (\confirm ->
                    confirm
                        |> Confirm.setOnCancel (Spa.mapSharedMsg Shared.closeConfirmDialog)
                        |> Confirm.setOnConfirm (Spa.mapSharedMsg Shared.confirmConfirmDialog)
                        |> Dialog.fromConfirm
                        |> Dialog.view
                )
                (List.take 1 shared.confirms |> List.map (\x -> Confirm.map Spa.mapSharedMsg x))
    }

so far i have this in Shared.elm and this is where my problems start

type alias Shared =
    { key : Nav.Key
    , identity : Maybe Identity
    , currentRoute : Maybe Route
    , preventReroute : Bool
    , confirms : List (Confirm Msg)
    }


type Msg
    = ReplaceRoute Route
    | BeforeRouteChange Route
    | ShowConfirmDialog (Confirm Msg)
    | CloseConfirmDialog
    | ConfirmHeadConfirmDialog
    | ClickedLink Browser.UrlRequest

update : Msg -> Shared -> ( Shared, Cmd Msg )
update msg shared =
    case msg of
        ShowConfirmDialog confirm ->
            ( { shared | confirms = confirm :: shared.confirms }, Cmd.none )

        CloseConfirmDialog ->
            case shared.confirms of
                confirm :: rest ->
                    ( { shared | confirms = rest }
                    , confirm.onCancel
                        |> Maybe.map TaskUtil.doTask
                        |> Maybe.withDefault Cmd.none
                    )

                [] ->
                    ( shared, Cmd.none )

        ConfirmHeadConfirmDialog ->
            case shared.confirms of
                confirm :: rest ->
                    ( { shared | confirms = rest }, TaskUtil.doTask confirm.onConfirm )

                [] ->
                    ( shared, Cmd.none )






--THIS IS WRONG 
showConfirmDialog : Confirm Msg -> Msg
showConfirmDialog confirm =
    ShowConfirmDialog confirm

and for a use case in Page.elm

        ClickConfirmDialog ->
            let
                confirmDialog =
                    Confirm.init (DeleteWhatever 2000)
                        |> Confirm.setPrompt "Are you sure you want to do this?"
            in
            model |> Effect.withShared (Shared.showConfirmDialog confirmDialog)

Hidden flags

Hi, thanks for developing this framework! ๐Ÿ˜

I have a question about how routes handle flags.

Currently, the flags are defined in a Route.elm module, in which are defined the functions

toUrl : Route -> String

route : Parser (Route -> a) a

that handle rendering and parsing the flags through URL parameters.

My question is: would it be possible to pass flags to each route without exposing them in the URL?

For example, protected pages can receive an Identity and fall back to a specified page if not provided, could that behavior be extended to generic values passed as flags?

Provide url in init

I need the Url in the init function to add the current route to the shared state on page load.

I added this in a local copy of the package and it is just a minor change and works fine.

If there is a better way to do this, let me know ๐Ÿ™‚

Shared view

With ryannhg's elm-spa you can create a shared view that has access to the SharedMsg and to the current route, because the Model and the Msg is in the Main module. Because these are not accessible in this package, this is currently not possible. Official support would be nice to have.

For a better understanding here's what I currently use for the time being. (I'm sure there are better ways to do his):

application :
    { toDocument : shared -> view -> Document (Msg sharedMsg pageMsg)
    , sharedView : Maybe ((sharedMsg -> Msg sharedMsg pageMsg) -> route -> shared -> view -> view)
    }
    -> Builder flags route identity shared sharedMsg view current previous pageMsg
    ->
        { init : flags -> Url -> Nav.Key -> ( Model route shared current previous, Cmd (Msg sharedMsg pageMsg) )
        , view : Model route shared current previous -> Document (Msg sharedMsg pageMsg)
        , update : Msg sharedMsg pageMsg -> Model route shared current previous -> ( Model route shared current previous, Cmd (Msg sharedMsg pageMsg) )
        , subscriptions : Model route shared current previous -> Sub (Msg sharedMsg pageMsg)
        , onUrlRequest : UrlRequest -> Msg sharedMsg pageMsg
        , onUrlChange : Url -> Msg sharedMsg pageMsg
        }
application { toDocument, sharedView } builder =
    let
        view_ =
            case sharedView of
                Just sharedView_ ->
                    \model -> builder.view model |> (\view -> toDocument (modelShared model) (sharedView_ SharedMsg (modelRoute model) (modelShared model) view))

                Nothing ->
                    \model -> builder.view model |> toDocument (modelShared model)
    in
    { init = builder.init
    , view = view_
    , update = builder.update
    , subscriptions = builder.subscriptions
    , onUrlRequest = UrlRequest
    , onUrlChange = UrlChange
    }


modelRoute : Model route shared current previous -> route
modelRoute ( core, _ ) =
    core.currentRoute

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.