Elm architecture usage with elm-mdl library - elm

What is the recommended way to have mdl components affect the layout of the page in a single-page application using elm and elm-mdl?
Can we pass the layout messages, like Model.Layout.ToggleDrawer directly to model.mdl (in the standard setup), and if yes, how?
Or should we maintain a separated record element in the model, like model.layout : Material.Layout.Model, to which we forward these messages? But in this case, how to initialize the view?
Context
I am using elm-mdl version 8.1.0, with elm version 0.18. I am trying to layout the basic architecture for a single-page application with elm, and this library. I have taken inspiration from here and there, as well as this ticket, but I have not seen what I was looking for, or understood it if it was there.
Examples of what I tried
For re-usability, the main model of my application is the only one containing a Material.Model entry:
type alias Model =
{ drawer : MyDrawer
, ...
, mdl : Material.Model
}
From the component MyDrawer, I want to define a button that will send a ToggleSignal. I have a case in my main update method that forwards this signal to the Model.Layout.update method, using the Model.Layout.ToggleSignal. However, the return type of this call is a Material.Layout.Model, which I don't have in my own Model.
If I define add a layout : Material.Layout.Model to my own model, I can forward the calls to this element, but how do I initialize the view? My view so far is this:
view : Model -> Html Msg
view model =
Layout.render Mdl
model
[ Layout.fixedHeader
, ...
According to the signature of Layout.render, since my model contains a layout field, this should be taken into account. The relevant part of my update method is
ToggleDrawer ->
let
( updatedLayout, cmd ) = Material.Layout.update Material.Layout.ToggleDrawer model.layout
( updatedDrawer, _ ) = Drawer.update subMsg model.drawer
in
( { model | drawer = updatedDrawer, layout = updatedLayout }, Cmd.map MdlLayout cmd )
And yet, when clicking on the button, the drawer does not hide.
Thank you in advance for any help about this - the library and the language are such a joy to use already!

The Material.Layout.render function retrieves its model from model.mdl.layout, not from model.layout.
A good way to toggle the drawer is to use toggleDrawer to issue a suitable command in your update function, like so:
ToggleDrawer ->
let
( updatedDrawer, _ ) = Drawer.update subMsg model.drawer
in
( { model | drawer = updatedDrawer, layout = updatedLayout }
, Layout.toggleDrawer Mdl
)

Related

onClick message causes unwanted refreshing of the page

I'm trying to build a website in Elm and it includes a couple links to change the language:
div [ class "language" ][ a [ class englishClass, onClick (ChangeLanguage English) ] [ text "EN" ]
, a [ class germanClass, onClick (ChangeLanguage German) ] [ text "DE" ]
, a [ class italianClass, onClick (ChangeLanguage Italian) ] [ text "IT" ]
]
My Update looks like this:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
... -- Stuff for URLs
ChangeLanguage language ->
( { model | language = language }
, Cmd.none
)
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
| ChangeLanguage Language
This is my model:
type alias Model =
{ key : Nav.Key
, url : Url.Url
, language : Language
}
And this is my init function, which (at least in my mind) defaults to English as a language:
init : flags -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init _ url key = ( Model key url English, Cmd.none )
The problem:
Whenever I click the links that change the language, they do seem to work: I see the language of the paragraphs in the page change, but then several unwanted things happen:
The page refreshes. Since I already saw the language change across the page, it's clear this is not needed so I'd like to avoid it.
As a consequence of 1, the viewport is brought all the way to the top of the page again.
The language changes to the default English again!
How could I avoid 1 (and thus 2) from happening?
And what am I missing to make it so the change is maintained across the site (at least when refreshing the page, since I haven't tried working with sessions or cookies yet)?
I consider this behaviour a bug in Elm, and have filed an issue for it, as have others with associated PRs. But in the year and a half since have received no attention from anyone in a position to actually do something about it, which is unfortunately par for the course with Elm.
The problem is that Elm "hijacks" onClick on a elements to create navigation events so that anchors can be handled inside Elm. When using Browser.application, clicking an a element will have Elm call onUrlReuqest with a value of Browser.UrlRequest, which will class the URL as either Internal or External and require you to make a decision on what to do with it.
The problem at the root of this is that omitting href will generate an External UrlRequest with the URL being an empty string. By default, and by the usual handling of External, this will tell the browser to load the URL as usual, thus refreshing the page. But it also suggests a possible workaround is to special-case External "":
update msg model =
case msg of
UrlRequested (Browser.Internal _) ->
( model, Cmd.none )
UrlRequested (Browser.External "") ->
( model, Cmd.none )
UrlRequested (Browser.External url) ->
( model, Browser.Navigation.load url )
UrlChanged _ ->
( model, Cmd.none )
Another workaround is to add href="#" to the a elements, which will correctly classify them as Internal

MVVM pattern in NativeScript - how to use one?

The Problem
I just cannot figure out the view model in NativeScript
I am having a hard time understanding how view-models work in NativeScript. I understand the high level concept - that the MVVM pattern allows us to create observable objects - and our UI is updated when values change.
Here is a simple example:
main-page.js
var createViewModel = require("./main-view-model").createViewModel;
function onNavigatingTo(args) {
var page = args.object;
page.bindingContext = createViewModel();
}
exports.onNavigatingTo = onNavigatingTo;
main-view-model.js
var Observable = require("tns-core-modules/data/observable").Observable;
function getMessage(counter) {
if (counter <= 0) {
return "Hoorraaay! You unlocked the NativeScript clicker achievement!";
} else {
return counter + " taps left";
}
}
function createViewModel() {
var viewModel = new Observable();
viewModel.counter = 42;
viewModel.message = getMessage(viewModel.counter);
viewModel.onTap = function() {
this.counter--;
this.set("message", getMessage(this.counter));
}
return viewModel;
}
exports.createViewModel = createViewModel;
I understand , some what, what is happening. But not everything.
Questions I Have ...
How would you add a new function , for instance, an email validation function? Would it go into the View Model page, or just plain Javscript page?
Let's say I added a new textfield to the UI. I have a tap function. Where does my function go?
So in this case, everything related to the UI should go in the createViewModel function? Is that correct?
I have also seen in sample apps, where the developer doesn't use view models at all - it appears he just creates it as an observable object.
Thank you for looking. I know I am close to understanding, but that bindingContext and the viewmodel has me a bit confused. [ I have read everything in NS docs ]
John
The answer is either of it should work. You may put the validation or tap function in view model or in the code behind file, it's upto you to decide which works best for you.
If you put it in the view model, you will use event binding (tap="{{ functionName }}" Or if you put it in code behind file, you will just export the function name and simply refer the function name on XML (tap="functionName").
By giving this flexibility you are allowed to separate your code, keep the files light weighted.

Elm and VSCode: Formatter messes up spacing

So I started learning Elm today. I use VSCode as my editor.
I followed the docs for the set up and installed elm as well as el-format via npm install -g elm elm-format. I also installed the VSCode Elm extension.
Next, in my settings.json I set:
"[elm]": {
"editor.formatOnSave": true
},
Then I went on with the tutorial. In it the code is formatted like this:
import Browser
import Html exposing (Html, Attribute, div, input, text)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
-- MAIN
main =
Browser.sandbox { init = init, update = update, view = view }
-- MODEL
type alias Model =
{ content : String
}
init : Model
init =
{ content = "" }
-- UPDATE
type Msg
= Change String
update : Msg -> Model -> Model
update msg model =
case msg of
Change newContent ->
{ model | content = newContent }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Text to reverse", value model.content, onInput Change ] []
, div [] [ text (String.reverse model.content) ]
]
But when I hit safe, it formats the code like this:
module Main exposing (Model, Msg(..), init, main, update, view)
import Browser
import Html exposing (Attribute, Html, div, input, text)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
-- MAIN
main =
Browser.sandbox { init = init, update = update, view = view }
-- MODEL
type alias Model =
{ content : String
}
init : Model
init =
{ content = "" }
-- UPDATE
type Msg
= Change String
update : Msg -> Model -> Model
update msg model =
case msg of
Change newContent ->
{ model | content = newContent }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Text to reverse", value model.content, onInput Change ] []
, div [] [ text (String.reverse model.content) ]
]
So it adds extra lines, and extra module Main exposing ... and doubles the number of spaces. I tried setting spaces to 2 again using the footer in VSCode, but that didn't help.
My questions are:
Is it okay that saving adds the extra module Main ...?
Is having 2 spaces best practice / community standard, or 4?
If it is 2 (like it is in the tutorial's default code) how can I get my formatter to respect that?
If it is not, why has the tutorial the non-standard indentation?
First of all, this question is both too broad and primarily opinion-based and will probably be closed because of that. It would have been more suited for the forums I think.
That said, I'm going to try to answer it as best as I can anyway, since it's here:
Yes? Most modules won't be very useful without exposing something and it's good practice to be explicit about what's being exposed.
elm-format is the community standard, so 4 it is.
You can't. This is by design. It has also been discussed to death in various fora. Here's one issue discussing it
You'd have to ask Evan about that. It might be related to formatting for the web, or just Evan being lazy.

Changing global state from child component

I was wondering what would be the preferred way for modifying global state from a child component using TEA.
My use case is having a global loading indicator showing while any json request is in progress.
A google search lead to me this article, but am not 100% this is the best solution. Any other suggestions?
A bit more background:
I have an SPA with various components and a "Loader" component. The loader component is supposed to handle transitions between views.
For example, let's say I have 3 views: (A) Dashboard (B) Accounts (C) Transaction Details.
When the user is on (A) and clicks on a Transaction I want (A) to send notify the Loader that it needs to load the transaction resource and one done update the Browser Location so the the root component can handle the routing.
The Loader should display a loading bar on the top of the page while loading the resource. Once done, it will update the Location bar, so the root component can handle the routing.
In essence what I am trying to avoid is having display the (C) page, before the resource for this is loaded.
A structure like below could hopefully get you going.
More on scaling Elm here (highly recommended)
In the example below you keep all data in your top-level model, and your messages as well.
Such a setup has no parent-child relationships. All views use the same top-level model, and all views produce the same type of messages.
This example navigates to the transactions page as soon as transactions come in.
type alias Model =
{ accounts: List Account
, transactions: Transactions
, currentPage : Page
, message : String
}
type Transactions = Idle | Loading | GotEm (List Transaction)
type Page = DashboardPage | AccountsPage | TransactionsPage
type Msg = ShowDashboard | ShowAccounts | ShowTransactions | GotJSONData
update msg model =
case msg of
ShowTransactions ->
case model.transactions of
GotEm transactions ->
{ model
| currentPage = TransactionsPage
, message = ""
} ! []
Idle ->
{ model
| currentPage = DashboardPage
| transactions = Loading
, message = "Loading transactions"
} ! [ API.getTransactions model GotJSONData ]
Loading ->
model ! []
GotJSONData transactions ->
{ model
| transactions = GotEm transactions
, message = ""
, currentPage = TransactionsPage
}
view model =
case model.currentPage of
DashboardPage ->
DashboardPage.view model
-- DASHBOARD module
view model =
div []
[ ...
, button [ onClick ShowTransactions ] [ text "Show transactions"]
]

Elm Architecture and tasks

UPDATE : This has now been covered in the Elm Architecture documentation.
--
I don't understand how you tie the Elm Architecture and Tasks.
-- Action is an enumeration of possible actions
type Action = ..
-- Model is a Model that evolves in time
model : Signal Model
-- View turns a model into Html, potentially sending actions to an address
view : Address Action -> Model -> Html
-- Update turns a model and an action into another model
update : Action -> Model -> Model
-- Main waits for new values of the model to be emitted, and pass then to the view action
main : Signal Html
main =
Signal.map (view actions.address) model
I'm trying to model this:
when a user click on a button, a "DoSomethingAction" is emitted
this action should start a Task, handled by some port
when the task is done, it should emit another Action ("SomethingDoneAction"), with the result
Is that the right way to go?
Which functions should I change (update, main)?
I understand this is what is alluded to here, but the explanation is not too clear and I don't understand what needs to be changed.
So any complete example / or more explanation would be welcome.
EDIT :
A more complete version (that "kinda" works) is available here : http://share-elm.com/sprout/55bf3c3ce4b06aacf0e8ba17
I'm a few steps from having it working, it seems, since the Task is not always run when I click on the button (but hopefully I'm getting somewhere.)
While it is very possible to perform Tasks when a button is pressed, it is often not needed. What you probably want is to send a message to the Action signal, then with that update the Model, then with that the view may change. This is standard Elm architecture.
If you really want to perform Tasks, you could do the following:
type Action = NoOp | ButtonClicked | SetText String
type alias Model =
{ tasks : Task Http.Error String
, text : String
}
init : Model
init =
{ task = Task.succeed "Fetching..."
, text = ""
}
-- We can use, for example, the Http.get task
update : Action -> Model -> Model
update action model =
case action of
ButtonClicked ->
{ model | task <-
Http.getString "http://example.org/some-text.txt"}
SetText t ->
{ model | text <- t }
_ -> model
view : Address Action -> Model -> Html
view address model =
div []
[ div
[ class "btn btn-default"
, onClick address ButtonClicked
]
[ text "Click me!" ]
, h3 "Here is what was fetched:"
, p [] [ text model.text ]
]
-- now possibly in another file
actions : Signal.Mailbox Action
actions : Signal.mailbox NoOp
model : Signal Model
model = Signal.foldp init update actions.signal
-- This is what actually performs the Tasks
-- The Elm task example also details how to send
port taskPort : Signal (Task Http.Error String)
port taskPort =
((.task) <~ model) `andThen`
(Signal.send actions.address << SetText)
main : Signal Html
main = view actions.address <~ model
Note that you can use Signal.dropRepeats if you want to perform the task only when the task changes.
The way to do it is to have the update of the model mapped to a response signal that receive the results of what you want your model to react to.
The view sends the tasks to a query/requests mailbox.
The port would be a map on the query signal that executes the tasks andThen sends the results to the response mailbox.
If you want to see this in action take a look at this file (I've highlighted the relevant parts)
https://github.com/pdamoc/elmChallenges/blob/master/challenge4.elm#L72-L94
Please be advised that what you see there is a little bit more complicated than what you need because it also implements a mechanism to execute the tasks only when the user stops typing.
This is answered in the doc :
https://github.com/evancz/elm-architecture-tutorial#user-content-example-5-random-gif-viewer