diff --git a/.travis.yml b/.travis.yml index d7bb1e5..d8b2d34 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,7 @@ install: - npm install -g elm - elm-package install -y script: + - elm-make --yes examples/Component.elm + - elm-make --yes examples/Component-TEA.elm - elm-make --yes examples/Demo.elm + diff --git a/Makefile b/Makefile index 6e6aa98..2f8e86f 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ PAGES=../elm-mdl-gh-pages -elm.js: +comp: + elm-make examples/Component.elm --warn --output elm.js + +demo: elm-make examples/Demo.elm --warn --output elm.js wip-pages : @@ -11,14 +14,14 @@ pages : elm-make examples/Demo.elm --output $(PAGES)/elm.js (cd $(PAGES); git commit -am "Update."; git push origin gh-pages) -clean : +cleanish : rm -f elm.js index.html -veryclean : +clean : rm -rf elm-stuff/build-artifacts distclean : clean rm -rf elm-stuff -.PHONY : pages elm.js clean veryclean distclean +.PHONY : pages elm.js clean cleanish distclean diff --git a/README.md b/README.md index 231e2da..5024fd3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Material Design Components in Elm +[![Build Status](https://travis-ci.org/debois/elm-mdl.svg?branch=master)](https://travis-ci.org/debois/elm-mdl) + Port of Google's [Material Design Lite](https://www.getmdl.io/) CSS/JS implementation of the diff --git a/elm-package.json b/elm-package.json index cdc296f..07b2ea9 100644 --- a/elm-package.json +++ b/elm-package.json @@ -9,13 +9,14 @@ ], "exposed-modules": [ "Material", - "Material.Style", + "Material.Style", "Material.Color", "Material.Icon", "Material.Button", "Material.Textfield", "Material.Layout", - "Material.Grid" + "Material.Grid", + "Material.Component" ], "dependencies": { "debois/elm-dom": "1.0.0 <= v < 2.0.0", @@ -23,7 +24,8 @@ "evancz/elm-effects": "2.0.1 <= v < 3.0.0", "evancz/elm-html": "4.0.2 <= v < 5.0.0", "evancz/elm-markdown": "2.0.1 <= v < 3.0.0", - "evancz/start-app": "2.0.2 <= v < 3.0.0" + "evancz/start-app": "2.0.2 <= v < 3.0.0", + "sporto/hop": "3.0.0 <= v < 4.0.0" }, "elm-version": "0.16.0 <= v < 0.17.0" -} +} \ No newline at end of file diff --git a/examples/Component-TEA.elm b/examples/Component-TEA.elm new file mode 100644 index 0000000..b7e4f18 --- /dev/null +++ b/examples/Component-TEA.elm @@ -0,0 +1,146 @@ +import StartApp +import Html exposing (..) +import Html.Attributes exposing (href, class, style) +import Effects exposing (Effects, Never) +import Task exposing (Task) + +import Material.Button as Button +import Material.Scheme + + +-- MODEL + + +type alias Model = + { count : Int + , increaseButtonModel : Button.Model + , resetButtonModel : Button.Model + } + + +model : Model +model = + { count = 0 + , increaseButtonModel = Button.model True -- With ripple animation + , resetButtonModel = Button.model False -- Without ripple animation + } + + +-- ACTION, UPDATE + + +type Action + = IncreaseButtonAction Button.Action + | ResetButtonAction Button.Action + + +increase : Model -> Model +increase model = + { model | count = model.count + 1 } + + +reset : Model -> Model +reset model = + { model | count = 0 } + + + +update : Action -> Model -> (Model, Effects Action) +update action model = + case Debug.log "" action of + IncreaseButtonAction action' -> + let + (submodel, fx) = + Button.update action' model.increaseButtonModel + model' = + case action' of + Button.Click -> + increase model + _ -> + model + in + ( { model' | increaseButtonModel = submodel } + , Effects.map IncreaseButtonAction fx + ) + + ResetButtonAction action' -> + let + (submodel, fx) = + Button.update action' model.resetButtonModel + model' = + case action' of + Button.Click -> + reset model + _ -> + model + in + ( { model' | resetButtonModel = submodel } + , Effects.map ResetButtonAction fx + ) + + +-- VIEW + + +view : Signal.Address Action -> Model -> Html +view addr model = + div + [ style + [ ("margin", "auto") + , ("padding-left", "5%") + , ("padding-right", "5%") + ] + ] + [ text ("Current count: " ++ toString model.count ) + , Button.flat + (Signal.forwardTo addr IncreaseButtonAction) + model.increaseButtonModel + [] + [ text "Increase" ] + , Button.flat + (Signal.forwardTo addr ResetButtonAction) + model.resetButtonModel + [] + [ text "Reset" ] + ] + |> Material.Scheme.top + + +{- The remainder of this file is Elm/StartApp boilerplate. +-} + + +-- SETUP + + +init : (Model, Effects.Effects Action) +init = (model, Effects.none) + + +inputs : List (Signal.Signal Action) +inputs = + [ + ] + + +app : StartApp.App Model +app = + StartApp.start + { init = init + , view = view + , update = update + , inputs = inputs + } + + +main : Signal Html +main = + app.html + + +-- PORTS + + +port tasks : Signal (Task Never ()) +port tasks = + app.tasks diff --git a/examples/Component.elm b/examples/Component.elm new file mode 100644 index 0000000..bc394f5 --- /dev/null +++ b/examples/Component.elm @@ -0,0 +1,158 @@ +import StartApp +import Html exposing (..) +import Html.Attributes exposing (href, class, style) +import Effects exposing (Effects, Never) +import Task exposing (Task) + +import Material +import Material.Scheme +import Material.Button as Button + + +-- MODEL + + +type alias Model = + { count : Int + , mdl : Material.Model Action + -- Boilerplate: mdl is the Model store for any and all MDL components you need. + } + + +model : Model +model = + { count = 0 + , mdl = Material.model + -- Boilerplate: Always use this initial MDL model store. + } + + +-- ACTION, UPDATE + + +type Action + = Increase + | Reset + | MDL (Material.Action Action) + -- Boilerplate: Action for MDL actions (ripple animations etc.). + + +update : Action -> Model -> (Model, Effects Action) +update action model = + case Debug.log "" action of + Increase -> + ( { model | count = model.count + 1 } + , Effects.none + ) + + Reset -> + ( { model | count = 0 } + , Effects.none + ) + + {- Boilerplate: MDL action handler. It should always look like this, except + you can of course choose to put its saved model someplace other than + model.mdl. + -} + MDL action' -> + let (mdl', fx) = + Material.update MDL action' model.mdl + in + ( { model | mdl = mdl' } , fx ) + + +-- VIEW + + +type alias Mdl = Material.Model Action + + +{- We construct the instances of the Button component that we need, one +for the increase button, one for the reset button. First, the increase +button. The arguments are: + + - An instance id (the `0`). Every component that uses the same model collection + (model.mdl in this file) must have a distinct instance id. + - An Action creator (`MDL`), lifting MDL actions to your Action type. + - A button view (`flat`). + - An initial model (`(Button.model True)`---a button with a ripple animation. + - A list of observations you want to make of the button (final argument). + In this case, we hook up Click events of the button to the `Increase` action + defined above. +-} +increase : Button.Instance Mdl Action +increase = + Button.instance 0 MDL Button.flat (Button.model True) + [ Button.fwdClick Increase ] + + +{- Next, the reset button. This one has id 1, does not ripple, and forwards its +click event to our Reset action. +-} +reset : Button.Instance Mdl Action +reset = + Button.instance 1 MDL Button.flat (Button.model False) + [ Button.fwdClick Reset ] + + +{- Notice that we did not have to add increase and reset separately to model.mdl, +and we did not have to add to our update actions to handle their internal events. +-} + + +view : Signal.Address Action -> Model -> Html +view addr model = + div + [ style + [ ("margin", "auto") + , ("padding-left", "5%") + , ("padding-right", "5%") + ] + ] + [ text ("Current count: " ++ toString model.count ) + , increase.view addr model.mdl [] [ text "Increase" ] + , reset.view addr model.mdl [] [ text "Reset" ] + -- Note that we use the .view function of our component instances to + -- actually render the component. + ] + |> Material.Scheme.top + + +{- The remainder of this file is Elm/StartApp boilerplate. +-} + + +-- SETUP + + +init : (Model, Effects.Effects Action) +init = (model, Effects.none) + + +inputs : List (Signal.Signal Action) +inputs = + [ + ] + + +app : StartApp.App Model +app = + StartApp.start + { init = init + , view = view + , update = update + , inputs = inputs + } + + +main : Signal Html +main = + app.html + + +-- PORTS + + +port tasks : Signal (Task Never ()) +port tasks = + app.tasks diff --git a/examples/Demo.elm b/examples/Demo.elm index b6e8d1d..ad6e9eb 100644 --- a/examples/Demo.elm +++ b/examples/Demo.elm @@ -1,6 +1,7 @@ +module Main (..) where import StartApp import Html exposing (..) -import Html.Attributes exposing (href, class, style) +import Html.Attributes exposing (href, class, style, key) import Signal exposing (Signal) import Effects exposing (..) import Task @@ -8,20 +9,57 @@ import Signal import Task exposing (Task) import Array exposing (Array) -import Material.Color as Color -import Material.Layout as Layout exposing (defaultLayoutModel) +import Hop +import Hop.Types +import Hop.Navigate exposing (navigateTo) +import Hop.Matchers exposing (match1) -import Material exposing (lift, lift') +import Material.Color as Color +import Material.Layout +import Material.Layout as Layout exposing (defaultLayoutModel) +import Material.Helpers exposing (lift, lift') import Material.Style as Style -import Material +import Material.Scheme as Scheme import Demo.Buttons import Demo.Grid import Demo.Textfields import Demo.Snackbar import Demo.Badges +import Demo.Elevation + --import Demo.Template + +-- ROUTING + + +type Route + = Tab Int + | E404 + + +type alias Routing = + ( Route, Hop.Types.Location ) + +route0 : Routing +route0 = + ( Tab 0, Hop.Types.newLocation ) + + +router : Hop.Types.Router Route +router = + Hop.new + { notFound = E404 + , matchers = + ( match1 (Tab 0) "/" + :: (tabs |> List.indexedMap (\idx (_, path, _) -> + match1 (Tab idx) ("/" ++ path)) + ) + ) + } + + -- MODEL @@ -36,6 +74,7 @@ layoutModel = type alias Model = { layout : Layout.Model + , routing : Routing , buttons : Demo.Buttons.Model , textfields : Demo.Textfields.Model , snackbar : Demo.Snackbar.Model @@ -46,39 +85,79 @@ type alias Model = model : Model model = { layout = layoutModel + , routing = route0 , buttons = Demo.Buttons.model , textfields = Demo.Textfields.model , snackbar = Demo.Snackbar.model - --, template = Demo.Template.model + --, template = Demo.Template.model } + -- ACTION, UPDATE type Action - = LayoutAction Layout.Action + -- Hop + = ApplyRoute ( Route, Hop.Types.Location ) + | HopAction () + -- Tabs + | LayoutAction Layout.Action | ButtonsAction Demo.Buttons.Action | TextfieldAction Demo.Textfields.Action | SnackbarAction Demo.Snackbar.Action - --| TemplateAction Demo.Template.Action +--| TemplateAction Demo.Template.Action -update : Action -> Model -> (Model, Effects.Effects Action) +nth : Int -> List a -> Maybe a +nth k xs = + List.drop k xs |> List.head + + +update : Action -> Model -> ( Model, Effects Action ) update action model = - case Debug.log "Action: " action of - LayoutAction a -> lift .layout (\m x->{m|layout =x}) LayoutAction Layout.update a model + case action of + LayoutAction a -> + let + ( lifted, layoutFx ) = + lift .layout (\m x -> { m | layout = x }) LayoutAction Layout.update a model + routeFx = + case a of + Layout.SwitchTab k -> + nth k tabs + |> Maybe.map (\(_, path, _) -> Effects.map HopAction (navigateTo path)) + |> Maybe.withDefault Effects.none + _ -> + Effects.none + in + ( lifted, Effects.batch [ layoutFx, routeFx ] ) + + ApplyRoute route -> + ( { model + | routing = route + , layout = setTab model.layout (fst route) + } + , Effects.none + ) + + HopAction _ -> + ( model, Effects.none ) + ButtonsAction a -> lift .buttons (\m x->{m|buttons =x}) ButtonsAction Demo.Buttons.update a model - TextfieldAction a -> lift' .textfields (\m x->{m|textfields=x}) Demo.Textfields.update a model + + TextfieldAction a -> lift .textfields (\m x->{m|textfields=x}) TextfieldAction Demo.Textfields.update a model + SnackbarAction a -> lift .snackbar (\m x->{m|snackbar =x}) SnackbarAction Demo.Snackbar.update a model + --TemplateAction a -> lift .template (\m x->{m|template =x}) TemplateAction Demo.Template.update a model + -- VIEW -type alias Addr = Signal.Address Action - +type alias Addr = + Signal.Address Action drawer : List Html @@ -86,11 +165,11 @@ drawer = [ Layout.title "Example drawer" , Layout.navigation [ Layout.link - [ href "https://www.getmdl.io/components/index.html" ] - [ text "MDL" ] + [ href "https://github.com/debois/elm-mdl" ] + [ text "github" ] , Layout.link - [ href "https://www.google.com/design/spec/material-design/introduction.html"] - [ text "Material Design"] + [ href "http://package.elm-lang.org/packages/debois/elm-mdl/latest/" ] + [ text "elm-package" ] ] ] @@ -112,31 +191,50 @@ header = ] -tabs : List (String, Addr -> Model -> List Html) +tabs : List (String, String, Addr -> Model -> Html) tabs = - [ ("Snackbar", \addr model -> - [Demo.Snackbar.view (Signal.forwardTo addr SnackbarAction) model.snackbar]) - , ("Textfields", \addr model -> - [Demo.Textfields.view (Signal.forwardTo addr TextfieldAction) model.textfields]) - , ("Buttons", \addr model -> - [Demo.Buttons.view (Signal.forwardTo addr ButtonsAction) model.buttons]) - , ("Grid", \addr model -> Demo.Grid.view) - , ("Badges", \addr model -> Demo.Badges.view ) + [ ("Buttons", "buttons", \addr model -> + Demo.Buttons.view (Signal.forwardTo addr ButtonsAction) model.buttons) + , ("Badges", "badges", \addr model -> Demo.Badges.view ) + , ("Elevation", "elevation", \addr model -> Demo.Elevation.view ) + , ("Grid", "grid", \addr model -> Demo.Grid.view) + , ("Snackbar", "snackbar", \addr model -> + Demo.Snackbar.view (Signal.forwardTo addr SnackbarAction) model.snackbar) + , ("Textfields", "textfields", \addr model -> + Demo.Textfields.view (Signal.forwardTo addr TextfieldAction) model.textfields) {- , ("Template", \addr model -> [Demo.Template.view (Signal.forwardTo addr TemplateAction) model.template]) -} ] -tabViews : Array (Addr -> Model -> List Html) -tabViews = List.map snd tabs |> Array.fromList + +e404 : Addr -> Model -> Html +e404 _ _ = + div + [ + ] + [ Style.styled Html.h1 + [ Style.cs "mdl-typography--display-4" + , Color.background Color.primary + ] + [] + [ text "404" ] + ] + + +tabViews : Array (Addr -> Model -> Html) +tabViews = List.map (\(_,_,v) -> v) tabs |> Array.fromList tabTitles : List Html -tabTitles = List.map (fst >> text) tabs +tabTitles = + List.map (\(x,_,_) -> text x) tabs + stylesheet : Html -stylesheet = Style.stylesheet """ +stylesheet = + Style.stylesheet """ blockquote:before { content: none; } blockquote:after { content: none; } blockquote { @@ -150,65 +248,83 @@ stylesheet = Style.stylesheet """ */ } p, blockquote { - max-width: 33em; - font-size: 13px; + max-width: 40em; + } + + h1, h2 { + /* TODO. Need typography module with kerning. */ + margin-left: -3px; } """ +setTab : Layout.Model -> Route -> Layout.Model +setTab layout route = + let + idx = + case route of + Tab k -> k + E404 -> -1 + in + { layout | selectedTab = idx } + view : Signal.Address Action -> Model -> Html view addr model = - let top = - div - [ style - [ ("margin", "auto") - , ("padding-left", "5%") - , ("padding-right", "5%") + let + top = + div + [ style + [ ( "margin", "auto" ) + , ( "padding-left", "8%" ) + , ( "padding-right", "8%" ) ] - ] - ((Array.get model.layout.selectedTab tabViews - |> Maybe.withDefault (\addr model -> - [div [] [text "This can't happen."]] - ) - ) addr model) - + , key <| toString (fst model.routing) + ] + [ (Array.get model.layout.selectedTab tabViews + |> Maybe.withDefault e404) + addr + model + ] in Layout.view (Signal.forwardTo addr LayoutAction) model.layout { header = header , drawer = drawer , tabs = tabTitles - , main = [ top ] + , main = [ stylesheet, top ] } {- The following line is not needed when you manually set up your html, as done with page.html. Removing it will then fix the flicker you see on load. -} - |> Material.topWithScheme Color.Teal Color.Red + |> Scheme.topWithScheme Color.Teal Color.Red -init : (Model, Effects.Effects Action) -init = (model, Effects.none) +init : ( Model, Effects.Effects Action ) +init = + ( model, Effects.none ) inputs : List (Signal.Signal Action) inputs = [ Layout.setupSignals LayoutAction + , Signal.map ApplyRoute router.signal ] app : StartApp.App Model app = - StartApp.start - { init = init - , view = view - , update = update - , inputs = inputs - } + StartApp.start + { init = init + , view = view + , update = update + , inputs = inputs + } + main : Signal Html main = - app.html + app.html -- PORTS @@ -216,4 +332,9 @@ main = port tasks : Signal (Task.Task Never ()) port tasks = - app.tasks + app.tasks + + +port routeRunTask : Task () () +port routeRunTask = + router.run diff --git a/examples/Demo/Badges.elm b/examples/Demo/Badges.elm index 7989f17..80b8a57 100644 --- a/examples/Demo/Badges.elm +++ b/examples/Demo/Badges.elm @@ -2,34 +2,86 @@ module Demo.Badges (..) where import Html exposing (..) import Material.Badge as Badge -import Material.Style exposing (..) +import Material.Style as Style exposing (styled) import Material.Icon as Icon +import Material.Grid exposing (..) + +import Demo.Page as Page -- VIEW -view : List Html +c : List Html -> Cell +c = cell [ size All 4 ] + +view : Html view = - [ div - [] - [ p [] [] - , styled span [ Badge.withBadge "2" ] [] [ text "Span with badge" ] - , p [] [] - , styled span [ Badge.withBadge "22", Badge.noBackground ] [] [ text "Span with no background badge" ] - , p [] [] - , styled span [ Badge.withBadge "33", Badge.overlap ] [] [ text "Span with badge overlap" ] - , p [] [] - , styled span [ Badge.withBadge "99", Badge.overlap, Badge.noBackground ] [] [ text "Span with badge overlap and no background" ] - , p [] [] - , styled span [ Badge.withBadge "♥" ] [] [ text "Span with HTML symbol - Black heart suit" ] - , p [] [] - , styled span [ Badge.withBadge "→" ] [] [ text "Span with HTML symbol - Rightwards arrow" ] - , p [] [] - , styled span [ Badge.withBadge "Δ" ] [] [ text "Span with HTML symbol - Delta" ] - , p [] [] - , span [] [ text "Icon with badge" ] - , Icon.view "face" [ Icon.size24, Badge.withBadge "33", Badge.overlap ] [] - , Icon.view "face" [ Icon.size48, Badge.withBadge "33", Badge.overlap ] [] + [ grid + [] + [ c [Style.span [ Badge.withBadge "2" ] [text "Badge"] ] + , c [Style.span + [ Badge.withBadge "22", Badge.noBackground ] + [ text "No background" ] + ] + , c [Style.span + [ Badge.withBadge "33", Badge.overlap ] + [ text "Overlap" ] + ] + , c [Style.span + [ Badge.withBadge "99", Badge.overlap, Badge.noBackground ] + [ text "Overlap, no background" ] + ] + , c [Style.span + [ Badge.withBadge "♥" ] + [ text "Symbol" ] + ] + , c [ Icon.view "flight_takeoff" [ Icon.size24, Badge.withBadge "33", Badge.overlap ] [] ] ] ] + |> Page.body "Badges" srcUrl intro references + + + +intro : Html +intro = + Page.fromMDL "http://www.getmdl.io/components/#badges-section" """ +> The Material Design Lite (MDL) badge component is an onscreen notification +> element. A badge consists of a small circle, typically containing a number or +> other characters, that appears in proximity to another object. A badge can be +> both a notifier that there are additional items associated with an object and +> an indicator of how many items there are. +> +> You can use a badge to unobtrusively draw the user's attention to items they +> might not otherwise notice, or to emphasize that items may need their +> attention. For example: +> +> - A "New messages" notification might be followed by a badge containing the +> number of unread messages. +> - A "You have unpurchased items in your shopping cart" reminder might include +> a badge showing the number of items in the cart. +> - A "Join the discussion!" button might have an accompanying badge indicating the +> number of users currently participating in the discussion. +> +> A badge is almost +> always positioned near a link so that the user has a convenient way to access +> the additional information indicated by the badge. However, depending on the +> intent, the badge itself may or may not be part of the link. +> +> Badges are a new feature in user interfaces, and provide users with a visual clue to help them discover additional relevant content. Their design and use is therefore an important factor in the overall user experience. +> +""" + + +srcUrl : String +srcUrl = + "https://github.com/debois/elm-mdl/blob/master/examples/Demo/Badges.elm" + + +references : List (String, String) +references = + [ Page.package "http://package.elm-lang.org/packages/debois/elm-mdl/latest/Material-Badge" + --, Page.mds "https://www.google.com/design/spec/components/buttons.html" + , Page.mdl "https://www.getmdl.io/components/#badges-section" + ] + diff --git a/examples/Demo/Buttons.elm b/examples/Demo/Buttons.elm index eedced0..2c5a292 100644 --- a/examples/Demo/Buttons.elm +++ b/examples/Demo/Buttons.elm @@ -10,15 +10,19 @@ import Material.Grid as Grid import Material.Icon as Icon import Material.Style exposing (Style) +import Demo.Page as Page + -- MODEL type alias Index = (Int, Int) + type alias View = Signal.Address Button.Action -> Button.Model -> List Style -> List Html -> Html + type alias View' = Signal.Address Button.Action -> Button.Model -> Html @@ -69,7 +73,8 @@ model = -- ACTION, UPDATE -type Action = Action Index Button.Action +type Action + = Action Index Button.Action type alias Model = @@ -79,20 +84,23 @@ type alias Model = update : Action -> Model -> (Model, Effects.Effects Action) -update (Action idx action) model = - Dict.get idx model.buttons - |> Maybe.map (\m0 -> - let - (m1, e) = Button.update action m0 - in - ({ model | buttons = Dict.insert idx m1 model.buttons }, Effects.map (Action idx) e) - ) - |> Maybe.withDefault (model, Effects.none) +update action model = + case action of + Action idx action -> + Dict.get idx model.buttons + |> Maybe.map (\m0 -> + let + (m1, e) = Button.update action m0 + in + ({ model | buttons = Dict.insert idx m1 model.buttons }, Effects.map (Action idx) e) + ) + |> Maybe.withDefault (model, Effects.none) -- VIEW + view : Signal.Address Action -> Model -> Html view addr model = buttons |> List.concatMap (\row -> @@ -125,4 +133,39 @@ view addr model = ] ) ) - |> Grid.grid [] + |> Grid.grid [] + |> flip (::) [] + |> Page.body "Buttons" srcUrl intro references + +intro : Html +intro = + Page.fromMDL "https://www.getmdl.io/components/#buttons-section" """ +> The Material Design Lite (MDL) button component is an enhanced version of the +> standard HTML `