We continue to talk about Elm 0.18 .
Elm. Comfortable and awkward
Elm. Comfortable and awkward. Json.Encoder and Json.Decoder
Elm. Comfortable and awkward. Http, Task
In this article we will consider the issues of the Elm application architecture and possible options for implementing the component development approach.
As a task, consider the implementation of a drop-down window that allows a registered user to add a question. In the case of an anonymous user offers to log in or register first.
We also assume that later it may be necessary to implement the reception of other types of user-generated content, but the logic of working with authorized and anonymous users will remain the same.
Source code of a naive implementation . As part of this implementation, we will all be stored in one model.
All data required for authorization and user polling are in the model at the same level. The same situation with messages (Msg).
type alias Model = { user: User , ui: Maybe Ui -- Popup is not open is value equals Nothing , login: String , password: String , question: String , message: String } type Msg = OpenPopup | LoginTyped String | PasswordTyped String | Login | QuestionTyped String | SendQuestion
The interface type is described as the union type Ui, which is used with the Maybe type.
type Ui = LoginUi -- Popup shown with authentication form | QuestionUi -- Popup shown with textarea to leave user question
Thus, ui = Nothing describes the absence of a drop-down window, and Just - the popup is open with a specific interface.
In the update function, a pair, message, and user data are mapped. Depending on this pair, various actions are performed.
update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case (msg, model.user) of
Suppose when you click on the button “Open popup” generated message OpenPopup. The OpenPopup message in the update function is processed in various ways. For an anonymous user, an authorization form is generated, and for an authorized user, a form in which you can leave a question.
update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case (msg, model.user) of -- Anonymous user message handling section (OpenPopup, Anonymous) -> ( { model | ui = Just LoginUi, message = "" }, Cmd.none) -- Authenticated user message handling section (OpenPopup, User userName) -> ( { model | ui = Just QuestionUi, message = "" }, Cmd.none)
Obviously, this approach may have problems with the growth of application functions:
The source code is a convenient implementation . As part of this implementation, we will try to divide the project into independent components. Permissible dependencies between components.
Project structure:
Each component, based on the architecture of the language, should contain:
Each component may contain a subscription (subscription) if necessary.
Fig. 1. Chart activity component
Each component must be initiated, i.e. must be received:
The argument list of the initialization function (init) depends on the logic of the component and can be arbitrary. Initialization functions may be several. Suppose that for the authorization component there are two options for initialization: with a session token and with user data.
The code that uses the component, after initialization, must send commands to the elm runtime using the Cmd.map function.
The update component function must be called for each component message. As a result, the function returns a triple:
The code that uses the component, after mutation, must update the component model and transfer the commands to the Elm runtime using the Cmd.map function.
The required arguments of the update function, in accordance with the Elm application architecture:
If necessary, the list of arguments can be added.
The view function is called at the moment when it is necessary to insert the component view into the overall application view.
The required argument of the view function should be the component model. If necessary, the list of arguments can be added.
The result of the view function must be passed to the Html.map function.
The example describes two components: Auth and Question . Components described above principles. Consider how they can be integrated into the application .
First, let's define how our application should work. On the screen there is a button, when clicked on which:
To describe the application you need:
type alias Model = { user: User , ui: Maybe Ui } type Ui = AuthUi Component.Auth.Model | QuestionUi Component.Question.Model
The model contains information about the user (user) and the type of the current interface (ui). The interface can either be in the default state (Nothing) or one of the components Just a.
To describe the components, we use the Ui type, which links (tags) each component model with a specific variant from the set type. For example, the AuthUi tag links the authorization model (Component.Auth.Model) with the application model.
type Msg = OpenPopup | AuthMsg Component.Auth.Msg | QuestionMsg Component.Question.Msg
In messages, you need to tag all component messages and include them in application messages. The AuthMsg tag and QuestionMsg link the messages of the authorization component and the user’s request, respectively.
An OpenPopup message is required to process a request to open an interface.
main : Program Never Model Msg main = Html.program { init = init , update = update , subscriptions = subscriptions , view = view }
An application entry point is described typically for an Elm application.
init : ( Model, Cmd Msg ) init = ( initModel, Cmd.none ) initModel : Model initModel = { user = Anonymous , ui = Nothing }
The initialization function creates a starting model and does not require the execution of commands.
update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case (msg, model.ui) of (OpenPopup, Nothing) -> case Component.Auth.init model.user of (authModel, commands, Just (Component.Auth.Authenticated userData)) -> let (questionModel, questionCommands, _) = Component.Question.init userData in ( { model | ui = Just <| QuestionUi questionModel, user = User userData }, Cmd.batch [Cmd.map AuthMsg commands, Cmd.map QuestionMsg questionCommands] ) (authModel, commands, _) -> ( { model | ui = Just <| AuthUi authModel }, Cmd.map AuthMsg commands ) (AuthMsg authMsg, Just (AuthUi authModel)) -> case Component.Auth.update authMsg authModel of (_, commands, Just (Component.Auth.Authenticated userData)) -> let (questionModel, questionCommands, _) = Component.Question.init userData in ( { model | ui = Just <| QuestionUi questionModel, user = User userData }, Cmd.batch [Cmd.map AuthMsg commands, Cmd.map QuestionMsg questionCommands] ) (newAuthModel, commands, _) -> ( { model | ui = Just <| AuthUi newAuthModel }, Cmd.map AuthMsg commands ) (QuestionMsg questionMsg, Just (QuestionUi questionModel)) -> case Component.Question.update questionMsg questionModel of (_, commands, Just (Component.Question.Saved record)) -> ( { model | ui = Nothing }, Cmd.map QuestionMsg commands ) (newQuestionModel, commands, _) -> ( { model | ui = Just <| QuestionUi newQuestionModel }, Cmd.map QuestionMsg commands ) _ -> ( model, Cmd.none )
Since the model and messages to the application are connected; we will process the message pair (Msg) and the interface type (model.ui: Ui).
update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case (msg, model.ui) of
If the OpenPopup message is received and the default interface is specified in the model (model.ui = Nothing), then we initialize the Auth component. If the Auth component reports that the user is authorized, we initialize the Question component and save it to the application model. Otherwise, save the component model to the application model.
(OpenPopup, Nothing) -> case Component.Auth.init model.user of (authModel, commands, Just (Component.Auth.Authenticated userData)) -> let (questionModel, questionCommands, _) = Component.Question.init userData in ( { model | ui = Just <| QuestionUi questionModel, user = User userData }, Cmd.batch [Cmd.map AuthMsg commands, Cmd.map QuestionMsg questionCommands] ) (authModel, commands, _) -> ( { model | ui = Just <| AuthUi authModel }, Cmd.map AuthMsg commands )
If a message is received with the AuthMsg a tag and the authorization interface is specified in the model (model.ui = Just (AuthUi authModel)), then we pass the component message and the component model to the Auth.update function. As a result, we get a new component model, commands and results.
If the user is authorized, we initialize the Question component, otherwise we update the interface data in the application model.
(AuthMsg authMsg, Just (AuthUi authModel)) -> case Component.Auth.update authMsg authModel of (_, commands, Just (Component.Auth.Authenticated userData)) -> let (questionModel, questionCommands, _) = Component.Question.init userData in ( { model | ui = Just <| QuestionUi questionModel, user = User userData }, Cmd.batch [Cmd.map AuthMsg commands, Cmd.map QuestionMsg questionCommands] ) (newAuthModel, commands, _) -> ( { model | ui = Just <| AuthUi newAuthModel }, Cmd.map AuthMsg commands )
Similarly to the Auth component, messages for the Question component are processed. If the question is successfully placed, the interface changes to the default (model.ui = Nothing).
(QuestionMsg questionMsg, Just (QuestionUi questionModel)) -> case Component.Question.update questionMsg questionModel of (_, commands, Just (Component.Question.Saved record)) -> ( { model | ui = Nothing }, Cmd.map QuestionMsg commands ) (newQuestionModel, commands, _) -> ( { model | ui = Just <| QuestionUi newQuestionModel }, Cmd.map QuestionMsg commands )
All other cases are ignored.
_ -> ( model, Cmd.none )
view : Model -> Html Msg view model = case model.ui of Nothing -> div [] [ div [] [ button [ Events.onClick OpenPopup ] [ text "Open popup" ] ] ] Just (AuthUi authModel) -> Component.Auth.view authModel |> Html.map AuthMsg Just (QuestionUi questionModel) -> Component.Question.view questionModel |> Html.map QuestionMsg
The view function, depending on the type of interface (model.ui), either generates the default interface or calls the component view function and maps the message type of the component to the message type of the application (Html.map).
subscriptions : Model -> Sub Msg subscriptions model = Sub.none
No subscription.
This example is slightly more convenient, but rather naive. What is missing:
Source: https://habr.com/ru/post/424341/
All Articles