Elm. Comfortable and awkward. Composition

    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.


    Awkward composition


    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 isnot 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.


    typeUi
      = 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:


    1. there is no grouping of data in the model and messages. Everything lies in one plane. Thus, there are no component boundaries, a change in the logic of one part is more likely to affect the rest;
    2. code reuse is possible according to the copy-paste principle with all the ensuing consequences.

    Convenient composition


    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:


    1. in the Type folder, user-defined types are declared;
    2. Custom components are declared in the Component folder;
    3. the file Main.elm input point of the project;
    4. The login.json and questions.json files are used as test data of the server response to authorization and saving information about the question, respectively.

    Custom components


    Each component, based on the architecture of the language, should contain:


    1. Model (Model);
    2. messages (Msg);
    3. result of performance (Return);
    4. initialization function (init);
    5. mutation function (update);
    6. view function.

    Each component may contain a subscription (subscription) if necessary.


    image
    Fig. 1. Chart activity component


    Initialization


    Each component must be initiated, i.e. must be received:


    1. model;
    2. command or list of commands that should initialize the state of the component;
    3. result of execution. The result of the execution at the time of initialization may need to be valid for checking the user's authorization, as in the examples for this article.

    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 .


    Mutation


    The update component function must be called for each component message. As a result, the function returns a triple:


    1. New model or new state (Model);
    2. command or command list for Elm runtime (Cmd Msg). As commands, there may be commands to execute HTTP requests, interact with ports, and so on;
    3. the result of the execution (Maybe Return). The type Maybe has two states Nothing and Just a. In our case, Nothing - no result, Just a - there is a result. For example, for authorization, the result could be Just (Authenticated UserData) - the user is authorized with UserData data.

    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:


    1. message (Msg);
    2. model (Model).

    If necessary, the list of arguments can be added.


    Representation


    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 .


    Application integration


    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:


    1. for an unauthorized user, the authorization form is displayed; after authorization, the form for placing the question;
    2. For an authorized user, the question posting form is displayed.

    To describe the application you need:


    1. Model (Model);
    2. messages (Msg);
    3. application start point (main);
    4. initialization function (init);
    5. mutation function;
    6. presentation function;
    7. subscription function.

    Model


    typealias 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 of type. For example, the AuthUi tag associates an authorization model (Component.Auth.Model) with an application model.


    Messages


    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 function


    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.


    Initialization function


    init : ( Model, CmdMsg )
    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.


    Mutation function


    Function source code
    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

    Work logic


    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) ->
         caseComponent.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.mapAuthMsg commands, Cmd.mapQuestionMsg questionCommands] )
           (authModel, commands, _) ->
             ( { model | ui = Just <| AuthUi authModel }, Cmd.mapAuthMsg 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 model of the component, the team and the result.


    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)) ->
         caseComponent.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.mapAuthMsg commands, Cmd.mapQuestionMsg questionCommands] )
           (newAuthModel, commands, _) ->
             ( { model | ui = Just <| AuthUi newAuthModel }, Cmd.mapAuthMsg 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 )

    Presentation function


    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), generates either 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).


    Subscription feature


    subscriptions : Model -> Sub Msg
    subscriptions model =
     Sub.none

    No subscription.


    Further


    This example is slightly more convenient, but rather naive. What is missing:


    1. blocking interaction with the application during the boot process;
    2. data validation. Requires a separate conversation;
    3. really a drop-down box with the ability to close.

    Also popular now: