Skip to content

jeongoon/elmnt-scrollpicker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

An Elm-Ui friendly Scroll Picker

elm install jeongoon/elmnt-scrollpicker

elmnt-scrollpicker provides an scroll picker with some animation. elmnt is stands for Element so you can use the View(widget) as an element in elm-ui.

See it in action here.

Usage

This is low level module, So let me examplain fully. however If you are familiar with other Elm architecture style module, it is easier. Or you have chance to get used to it.

Import

import Element exposing (..)
import Dict exposing (Dict)
                     -- ^ options are stored in Dict
import Elmnt.BaseScrollPicker as ScrollPicker
                                 -- ^ or as you'd like

Make your own Model and Msg

type AppMsg
     = ScrollPickerMessage String
       (ScrollPicker.Msg Int AppMsg)

ScrollPicker.Msg type involves the type of Your own message (AppMsg) and also the type of options that we'd like to pick from. Int is used for Option in this case.

Some states of picker are required to store in internal record. you might need to declare your own message wrapper constructor ScrollPickerMessage is an wrapper constructor (or map function) to create messages which is compatible to your own module message.

Let's say we are making a simple time picker, we need two seprate pickers for hour and minute value. If you want a Dict or List go for it!

type alias Model -- which is your own model
    = { firstPickerState
         : ScrollPicker.MinimalState Int AppMsg
      , secondPickerState
         : ScrollPicker.MinimalState Int AppMsg
      , messageMapWith
         : String -> (ScrollPicker.Msg Int AppMsg) ->
           AppMsg
      , pickerDirection
         : ScrollPicker.Direction -- Horizontal or Vertical
      }

Model

MinimalState shows the minimal states required to work as a scroll picker. And even if you put more fields in your record, all the function will still works with yours. Because the most of API use partial record annotation.

For example setOptions function has the definition like below.

setOptions
    : (vt -> String) -> -- vt stands for 'value type'
      List (vt, Element msg) ->
      { state |
        idString  : String
      , optionIds : List String
      , optionIdToRecordDict : Dict String
                               (Option vt msg)
      } -> -- At least, those fields are required
           -- in the state record
      { state |
        idString  : String
      , optionIds : List String
      , optionIdToRecordDict : Dict String
                               (Option vt msg)
      } --> will return the same structure of
        --  the state record

which makes setOptions can be provided with a subset of MinimalState.

Init

Let's initialise our example model. Each picker model(or state) could be initialised with functions such as initMinimalState, setOptions and setScrollStopCheckTime

exampleInit : () -> ( Model, Cmd AppMsg )
exampleInit flags
    = ( { firstPickerState -- for hour value
              = ScrollPicker.initMinimalState
                "firstScrollPicker" -- id
              |> ScrollPicker.setOptions
                 (String.fromInt)
                 (List.range 1 12
                    |> List.map
                       ( \n ->
                            ( n
                            , n
                              |> ( String.fromInt >> text )
                            )
                       )
                 )
              |> ScrollPicker.setScrollStopCheckTime 75
                 -- ^ a bit more quicker to check

        , secondPickerState -- for minute value
              = ScrollPicker.initMinimalState
                "secondScrollPicker"
              |> ScrollPicker.setOptions
                 (String.fromInt)
                 (List.range 0 59
                    |> List.map
                       ( \n ->
                           ( n
                           , n
                             |> ( String.fromInt
                                    >> String.padLeft 2 '0'
                                    >> text
                                )
                           )
                       )
                 )

        , messageMapWith
            = ScrollPickerMessage
          -- ^ a map function to wrap the picker messages
          --   into the Msg

        , pickerDirection
            = ScrollPicker.Vertical
        }              

Update

In your own update function, you might need to check picker Id and update the matched picker state(or model) accordingly.

exampleUpdate : AppMsg -> Model ->
                ( Model, Cmd AppMsg )
exampleUpdate msg model
    = let update
              = ScrollPicker.updateWith model
      in
      case msg of
          ScrollPickerMessage idString pickerMsg ->
              case idString of
                  "firstScrollPicker" ->
                      let ( firstPickerState, cmd )
                              = update pickerMsg
                                  model.firstPickerState

                          newModel
                              = { model |
                                  firstPickerState
                                      = firstPickerState
                                }
                              
                      in ( case ScrollPicker.anyNewOptionSelected
                                  pickerMsg of

                               Just option ->
                                   { newModel |
                                     hourValue
                                         = option.value
                                   }
                               Nothing ->
                                   newModel
                           , cmd
                         )

                  "secondScrollPicker" ->
                      let ( secondPickerState, cmd )
                              = update pickerMsg
                                  model.secondPickerState

                          newModel
                              = { model |
                                  secondPickerState
                                      = secondPickerState
                                }
                              
                      in ( case ScrollPicker.anyNewOptionSelected
                                  pickerMsg of

                               Just option ->
                                   { newModel |
                                     minuteValue
                                        = option.value
                                   }
                               Nothing ->
                                   newModel
                           , cmd
                         )

                  _ ->
                      ( model, Cmd.none )

And picker model itself does not hold selected option, you also need to check some message Elmnt.BasePickerState.ScrollPickerSuccess or you can use anyNewOptionSelected function like above code.

View

Here is an example

exampleView : Model -> Html AppMsg
exampleView model
    = let
        theme
            = defaultTheme

        palette
            = theme.palette

        pickerHelper
            = ScrollPicker.viewAsElement model theme

   in
       layout [ Background.color
                (palette.on.surface 
                    -- ^ use same color as shade
                    |> palette.toElmUiColor)
              ] <|
           column [ centerX
                  , centerY
                  ]
               [ row [ spacing 1
                     ]
                     [ pickerHelper model.firstPickerState
                     , pickerHelper model.secondPickerState
                     ]

               , el [ paddingEach
                        { top    = 20
                        , right  = 0
                        , bottom = 0
                        , left   = 0
                        }
                    , Font.size
                        ( (modular 16 1.25 8)
                              |> (*) 0.7
                              |> truncate
                        )
                    , Font.color
                          ( palette.secondary
                                |> palette.toElmUiColor )
                    , centerX
                    ] <|
                   text <| "It's " ++
                       ( model.hourValue
                            |> String.fromInt
                       ) ++ ":" ++
                       ( model.minuteValue
                            |> String.fromInt
                            |> String.padLeft 2 '0'
                       )
           ]

Subscriptions

Animation relies on subscriptions. elm-style-animation also does so. subscriptionsWith helps how to subscribe.

exampleSubscriptions : Model -> Sub AppMsg
exampleSubscriptions model
    = model |>
      ScrollPicker.subscriptionsWith
      [ model.firstPickerState
      , model.secondPickerState
      ]

Main

Finally you can wire thems up in the elm architecture.

main : Program () Model AppMsg
main
    = Browser.element
      { init = exampleInit
      , view = exampleView
      , update = exampleUpdate
      , subscriptions = exampleSubscriptions
      }

Testing Environment

I'm a chef and not a professional programmer but have been still using Linux since 2001. My 8 years old laptop can only run Linux smoothly. And I don't have enough chance to check things on Apple product, either.

so, if you need more techincal support on other platform, please contribute your solution.

More Information

Why elm-style-animation? elm-style-animation is not quite designed for low level animation. but you could use the module for any other css-style based animation. So you could possibly add other animation in your app without any additional module.

elm-animation was also considered, and it is pretty straight-foward. However the module cannot live together in the same application due to name colision.