arowM / tepa / Tepa

This module provides core functionality for TEPA.

⚠⚠⚠⚠⚠⚠ If you haven't read the README, read the README first. If you want to know how to use it first, then learning TEPA is of no value to you. Go home. ⚠⚠⚠⚠⚠⚠

Overview of the application structure

TEPA application is written in the Elm language, and is converted to JavaScript, so you import it to use in your JavaScript (or TypeScript) file. If you are a Parcel user, you can load the Elm file that contains the main function built with application as follows:

index.html:

<html>
  <body>
    <script type="module" src="./index.js"></script>
  </body>
</html>

index.js:

// Specify your file that contains `main` function here.
import { Elm } from "./Main.elm"

Elm.Main.init({
  // Pass some JSON value to TEPA upon initialization.
  flags: {
    "loaded-at": Date.now()
  }
})

Main.elm (Details will be explained later):

import Json.Decode (Value)
import Tepa exposing (AppUrl, NavKey, Program, Promise)
import Tepa.Time as Time

main : Program Memory
main =
    Tepa.application
        { init = init
        , view = view
        , initView = initView
        , onLoad = onLoad
        , onUrlRequest = onUrlRequest
        , onUrlChange = onUrlChange
        }

{- Data type for representing your application state.
-}
type alias Memory =
    { ...
    , ...
    }

{- Data type for representing _Flags_.
-}
type alias Flags =
    { ...
    , ...
    }

{-| Your implementation for `init`.
-}
init : Value -> Promise () (Flags, Memory)
init =
    Debug.todo ""

{-| Your implementation for `onLoad`.
-}
onLoad : Flags -> AppUrl -> NavKey -> Promise Memory ()
onLoad flags url key =
    Debug.todo ""

{-| Your implementation for `onUrlRequest`.
-}
onUrlRequest : Flags -> UrlRequest -> NavKey -> Promise memory ()
onUrlRequest flags url key =
    Debug.todo ""

{-| Your implementation for `onUrlChange`.
-}
onUrlChange : Flags -> AppUrl -> NavKey -> Promise memory ()
onUrlChange flags url key =
    Debug.todo ""

{-| Your implementation for `initView`.
-}
initView : Document
initView layer =
    Debug.todo ""

{-| Your implementation for `view`.
-}
view : Layer Memory -> Document
view layer =
    Debug.todo ""

If you are not familiar with Elm language, we recommend you to check Core Language, Types, and Error Handling section of the Elm guide. API documentation for the core libraries of the language can be found on the package site. Among the core libraries, Platform, Platform.Cmd, Platform.Sub, Process, and Task modules are not used by TEPA, so you can just ignore them.

application : ApplicationProps flags memory -> Program flags memory

Entry point for building your applications.


type alias ApplicationProps flags memory =
{ init : Json.Encode.Value -> Promise () ( flags
, memory )
, onLoad : flags -> AppUrl -> NavKey -> Promise memory ()
, onUrlRequest : flags -> UrlRequest -> NavKey -> Promise memory ()
, onUrlChange : flags -> AppUrl -> NavKey -> Promise memory ()
, initView : Document
, view : flags -> memory -> Document 
}

Property values for your application.


type alias Program flags memory =
Platform.Program Json.Encode.Value (Model flags memory) Msg

A Program describes an TEPA program.

An alias for Platform.Program.


type alias Document =
{ title : String
, body : List Html 
}

This data specifies the <title> and all of the nodes that should go in the <body>. This means you can update the title as your application changes. Maybe your "single-page app" navigates to a "different page", maybe a calendar app shows an accurate date in the title, etc.

This is the TEPA version of Browser.Document.

Key Concepts of TEPA

Memory

TEPA has a single Memory that holds all of the application state.

{-| Your own type that represents your application state.

It is common to declare [type alias](https://guide.elm-lang.org/types/type_aliases) for [record](https://guide.elm-lang.org/core_language#records).

-}
type alias Memory =
    { pageState : PageState
    }

Flags

You can pass a JSON value, called Flags, to the TEPA application upon initialization. Common uses are passing in API keys, environment variables, and user data.

The sample index.js in the application document passes { "loaded-at": Data.now() } as flags.

Elm.Main.init({
  // Pass some JSON value to TEPA upon initialization.
  flags: {
    "loaded-at": Date.now()
  }

On the Elm side, the JSON object is converted to Value type which you can decode with the Json.Decode module.

Procedure

In TEPA, you describe application behaviour as Procedure, the time sequence of proccessing.

Promise

To build your procedure, you combine some Promises that represent the eventual completion of an operation and its resulting value. Similar to Promise in JavaScript.


type alias Promise memory result =
Internal.Core.Promise memory result

Primitive Promises

sequence : List (Promise m ()) -> Promise m ()

Sequentially process Procedures.

sample : Promise m ()
sample =
    sequence
        [ executedFirst
        , executedAfterFirstIsResolved
        ]

modify : (m -> m) -> Promise m ()

Construct a Promise that modifies the Memory state.

Note that the update operation, passed as the second argument, is performed atomically; it means that the state of the Memory is not updated by another process during it is read and written by the modify.

none : Promise m ()

Procedure that does nothing.

currentState : Promise m m

Promise that requests current Memory state.

Bind results

currentState is resolved with the current state, but how to retrieve the value? You can use bind to pass the result into another sequence of procedures.

bind : Promise m a -> (a -> List (Promise m ())) -> Promise m ()

Helper function to bind the result of a promise.

import Tepa exposing (Promise)

procedure : Promise m ()
procedure =
    Tepa.bind Tepa.currentState <|
        \state ->
            if state.noGoats then
                [ Tepa.modify <|
                    \m ->
                        { m | message = "Oh my Goat!" }
                , unfortunatelyNoGoatsProcedure
                ]

            else
                [ youAreLuckyProcedure
                ]

bind2 : Promise m a -> Promise m b -> (a -> b -> List (Promise m ())) -> Promise m ()

Run two Promises concurrently, and bind the results to variables when both are complete.

import Tepa exposing (Promise)
import Tepa.Time as Time

sample : Promise m ()
sample =
    Tepa.bind2 Time.now Time.here <|
        \now here ->
            [ yourProcedure now here
            ]

bind3 : Promise m a -> Promise m b -> Promise m c -> (a -> b -> c -> List (Promise m ())) -> Promise m ()

Run three Promises concurrently, and bind the results to variables when all are complete.

If you need to bind more Promises, use bindAll or sync.

bindAll : List (Promise m a) -> (List a -> List (Promise m ())) -> Promise m ()

Run Promises concurrently, and bind the results to variables when all are complete.

succeed : a -> Promise memory a

Build a Promise that is always completed with the given value immediately.

This is usefull for building Promise for concurrent operations with sync.

sync : Promise m a -> Promise m (a -> b) -> Promise m b

Run many Promises concurrently to reduce all results.

type alias Response =
    { resp1 : Resp1
    , resp2 : Resp2
    }

request1 : Promise Memory Event Resp1
request1 =
    Debug.todo ""

request2 : Promise Memory Event Resp2
request2 =
    Debug.todo ""

-- Returns `Response` value when both Promises has been completed.
batched : Promise Memory Event Response
batched =
    succeed Response
        |> sync request1
        |> sync request2

Recursive procedures

It is common technique to call procedures recursively.

formProcedure : Promise Memory ()
formProcedure =
    Tepa.bind submit <|
        \result ->
            case result of
                Err error ->
                    [ Tepa.modify <|
                        \m ->
                            { m | error = error }
                    , formProcedure
                    ]

                Ok body ->
                    [ Debug.todo ""
                    ]

However, this code may cause run out of stack memory. To optimize memory usage, you can use lazy.

formProcedure : Promise Memory ()
formProcedure =
    Tepa.bind submit <|
        \result ->
            case result of
                Err error ->
                    [ Tepa.modify <|
                        \m ->
                            { m | error = error }
                    , Tepa.lazy <|
                        \_ ->
                            formProcedure
                    ]

                Ok body ->
                    [ Debug.todo ""
                    ]

lazy : (() -> Promise m ()) -> Promise m ()

Common helpers

void : Promise m a -> Promise m ()

Wait for a promise to be resolved and just ignore the result.

when : Basics.Bool -> List (Promise m ()) -> Promise m ()

Evaluate the sequence of Procedures only if the first argument is True, otherwise same as none.

unless : Basics.Bool -> List (Promise m ()) -> Promise m ()

Evaluate the sequence of Procedures only if the first argument is False, otherwise same as none.

withMaybe : Maybe a -> (a -> List (Promise m ())) -> Promise m ()

Evaluate the sequence of Procedures returned by the callback function only if the first argument is Just, otherwise same as none.

syncAll : List (Promise m ()) -> Promise m ()

Run Procedures concurrently, and await all to be completed.

forEach : List a -> (a -> List (Promise m ())) -> Promise m ()

Run the Procedure concurrently on each list element, and await all to be completed.

DOM events

To request DOM related operations, you can use Tepa.Dom module.

Page navigation

To navigate to another page, you can use Tepa.Navigation module.

Random values

To request random values, you can use Tepa.Random module.

Handle time

To request time related operations, you can use Tepa.Time module.

HTTP requests

To send HTTP request to the backend server, you can use Tepa.Http module.

Port requests

portRequest : { request : PortRequest Msg, response : PortResponse Msg, portName : String, requestBody : Json.Encode.Value } -> Promise m Json.Encode.Value

Build a Promise to send a port request and receive a single response for it. The port is an concept to allow communication between Elm and JavaScript (or TypeScript). Ports are probably most commonly used for WebSockets and localStorage. You can see WebSocket examples in the sample application.

Here, we use portRequest to get localStorage value safely.

In JavaScript side:

const app = Elm.Main.init({
  flags: {
    "loaded-at": Date.now()
};

app.ports.requestGetLocalName.subscribe((req) => {
  // It is a good practice to surround your code with `requestAnimationFrame` and `try`.
  requestAnimationFrame(() => {
    try {
      app.ports.receiveGetLocalName.send({
        // The `requestId` value, generated by TEPA, links
        // the subscribe port to the relevant send port.
        "id": req.id,
        "body": {
          name: localStorage.getItem(`Name.${req.body.userId}`),
        },
      });
    } catch {
      app.ports.receiveGetLocalName.send({
        "id": req.id,
        "body": {
          name: null,
        },
      });
    }
  });
});

In Elm side:

import Json.Decode as JD
import Json.Encode as JE exposing (Value)
import Tepa exposing (PortRequest, PortResponse, Promise)

port page_foo_get_local_name_request : PortRequest a

port page_foo_get_local_name_response : PortResponse a

type alias LocalNameResponse =
    { name : Maybe String
    }

requestLocalName : String -> Promise Memory Event LocalNameResponse
requestLocalName userId =
    Tepa.portRequest
        { request = page_foo_get_local_name_request
        , response = page_foo_get_local_name_response

        -- Port identifier for testing and debugging which must be
        -- unique among its layer.
        , portName = "get_local_name"
        , requestBody =
            JE.object
                [ ( "userId"
                  , JE.string userId
                  )
                ]
        }

As you can see, port is a mechanism for requesting a task to the JavaScript server running on the client machine. It is the same structure as requesting a task to the backend server via HTTP Web API.


type alias PortRequest msg =
Json.Encode.Value -> Platform.Cmd.Cmd msg


type alias PortResponse msg =
(Json.Encode.Value -> msg) -> Platform.Sub.Sub msg

portStream : { request : PortRequest Msg, response : PortResponse Msg, cancel : PortRequest Msg, portName : String, requestBody : Json.Encode.Value } -> Promise m (Internal.Core.Stream Json.Encode.Value)

Similar to portRequest, but portStream can receive many responses. One of the use cases is to receive WebSocket messages.

Keep in mind that this Promise blocks subsequent Promises, so it is common practice to call asynchronously with the main Promise when you create a new layer. If you call portStream in recursive Promise, it spawns listeners many times!

Streams

Some Promises are resolved with Stream, which is an advanced technique for controlling Promises precisely. See Tepa.Stream for details.

Lower level functions

map : (a -> b) -> Promise m a -> Promise m b

Transform a resulting value produced by a Promise.

liftMemory : { get : m0 -> m1, set : m1 -> m0 -> m0 } -> Promise m1 a -> Promise m0 a

Transform the Promise Memory.

maybeLiftMemory : { get : m0 -> Maybe m1, set : m1 -> m0 -> m0 } -> Promise m1 a -> Promise m0 (Maybe a)

Similar to liftMemory, but for the memory which may not exist. When the memory part is unreachable during execution, it resolved to Nothing.

andThen : (a -> Promise m b) -> Promise m a -> Promise m b

Build a new Promise that evaluate two Promises sequentially.

bindAndThen : Promise m a -> (a -> Promise m b) -> Promise m b

Flipped version of andThen.

You can use bindAndThen to bind some Promise result to a variable:

bindAndThen somePromise <|
    \result ->
        anotherPromise result

bindAndThen2 : Promise m a -> Promise m b -> (a -> b -> Promise m c) -> Promise m c

Run two Promises concurrently, and bind the results to variables when both are complete.

import Tepa exposing (Promise)
import Tepa.Time as Time

sample : Promise m ( Time.Zone, Time.Posix )
sample =
    Tepa.bindAndThen2 Time.now Time.here <|
        \now here ->
            Tepa.succeed ( here, now )

bindAndThen3 : Promise m a -> Promise m b -> Promise m c -> (a -> b -> c -> Promise m d) -> Promise m d

Run three Promises concurrently, and bind the results to variables when all are complete.

If you need to bind more Promises, use bindAndThenAll or sync.

bindAndThenAll : List (Promise m a) -> (List a -> Promise m b) -> Promise m b

Run Promises concurrently, and bind the results to variables when all are complete.

Application Procedures

There are four types of procedures that you specify as the application property.

init

The init Procedure is executed on page load to decode your Flags and set the initial Memory state. It takes a raw Value which you can decode to your Flags using the Json.Decode module.

init : Value -> Promise () ( flags, memory )

Note that its type has () as a Memory type, so you cannot access memory state during initialization process.

onLoad

The onLoad is the main Procedure that is executed on every page load right after init. It takes three arguments:

onLoad : flags -> AppUrl -> NavKey -> Promise memory ()


type alias NavKey =
Internal.Core.NavKey

A navigation key is needed to create navigation procedures exposed by the Tepa.Navigation module.

You only get access to a NavKey when you create your program with application, guaranteeing that your program is equipped to detect these URL changes. If NavKey values were available in other kinds of programs, unsuspecting programmers would be sure to run into some annoying bugs and learn a bunch of techniques the hard way!

This is the TEPA version of Browser.Navigation.Key.

onUrlRequest

You specify the onUrlRequest Procedure for handling page transition requests. It takes three arguments:

onUrlRequest : flags -> UrlRequest -> NavKey -> Promise memory ()


type UrlRequest
    = InternalPath AppUrl
    | ExternalPage String

All links in an application create a UrlRequest. So when you click <a href="/home">Home</a>, it does not just navigate! It notifies onUrlRequest that the user wants to change the URL.

This is the TEPA version of Browser.UrlRequest. Refer to the documentation for more detailed notes.

onUrlChange

Immediately after the URL is changed, onUrlChange Procedure is evaluated. A common use case is to change the page state based on the new URL.

It takes three arguments:

onUrlChange : flags -> AppUrl -> NavKey -> Promise memory ()

In sample application, the onUrlChange function retrieves the session information, such as the logged-in user data, and then calls the main procedure.

Layer

Motivation

The Layer is the concept of an isolated space. You can create a new layer with the newLayer function, execute a procedure on a layer with the onLayer function, delete or overwrite the existing layer with modify.

A main use of the layer is to manage page transition. See that you have the following Page type to represent your page state.

import MyApp.Profile exposing (Profile)
import Page.Home
import Page.Users
import Tepa exposing (Layer)

type alias Memory =
    { page : Page
    , session :
        { mprofile : Maybe Profile
        }
    }

type Page
    = PageNotFound
    | PageHome Page.Home.Memory
    | PageUsers Page.Users.Memory

This approach may seem to work well at first, but it may exhibit unexpected behavior, such as

  1. A user opens the home page.
    • The init Procedure initializing the page in Memory to PageHome param (where param is the initial value of PageHome.Memory).
    • The onLoad Procedure is called, and it execute the Procedure for the home page.
    • The Procedure for the home page is executed to replace the top image to be displayed every 20 seconds (process A).
  2. The user is redirected to the user list page.
    • The onChangeUrl Procedure is called and the page in memory is replaced with PageUsers param (param is the initial value of PageUsers.Memory).
    • At this point, process A is asleep.
  3. The user immediately changes to the home page.
    • The onChangeUrl Procedure is called, and the page in memory is again replaced with the PageHome param (param is the initial value of PageHome.Memory).
    • The onChangeUrl Procedure continues to execute a new Procedure for the home page.
    • A new process (Process B) runs to update the display image every 20 seconds, overlapping with Process A.
    • Now it is time for Process A to refresh the image.
      • Because the memory state is still PageHome, Process A cannot detect that the page has changed in the middle of the process.
    • Process B also refreshes images periodically, so the slideshow now refreshes images at odd times!

The reason for this unexpected behavior is that the process cannot determine that the page state has changed in the middle of a page based only on the memory state. To solve this problem, you can use Layer.

type Page
    = PageNotFound
    | PageHome (Layer Page.Home.MemoryBody)
    | PageUsers (Layer Page.Users.MemoryBody)

The new Page definition above uses Layer to wrap each page memory state. A procedure executed on a Layer will be aborted when the Layer has expired by being overwritten by another Layer. This allows you to avoid running duplicate procedures.


type alias Layer m =
Internal.Core.Layer m

Layer is a concept that deals with a part of the application. It can successfully represent elements that are created or removed during the application runtime. Especially, it matches well with Pages in SPAs. The application itself is also a Layer.

newLayer : m1 -> Promise m (Layer m1)

Layer Memory

On each Layer you define Memory with LayerMemory.


type LayerMemory link body

Memory structure for accessing external Memory space from within Layer.

onLink : Promise link a -> Promise (LayerMemory link body) a

Run Procedures for link part of LayerMemory

onBody : Promise body a -> Promise (LayerMemory link body) a

Run Procedures for body part of LayerMemory

linkSequence : List (Promise link ()) -> Promise (LayerMemory link body) ()

Helper function to run Procedures for link part sequentially.

bodySequence : List (Promise body ()) -> Promise (LayerMemory link body) ()

Helper function to run Procedures for body part sequentially.

modifyLink : (link -> link) -> Promise (LayerMemory link body) ()

Helper function to modify link part.

onLayer : { getLink : memory -> Maybe link, setLink : link -> memory -> memory, getBody : memory -> Maybe (Layer body), setBody : Layer body -> memory -> memory } -> Promise (LayerMemory link body) a -> Promise memory (ResultOnLayer a)

Run Promise on the specified Layer. For example, consider the following situation:

Tepa.bind
    (PageLogin.init msession
        |> Tepa.andThen Tepa.newLayer
    )
<|
    \newLayer ->
        [ Tepa.modify <| \m -> { m | page = PageLogin newLayer }
        , PageLogin.procedure key url
            |> Tepa.onLayer
                { getLink =
                    \m ->
                        Maybe.map
                            (\profile ->
                                { profile = profile
                                }
                            )
                            m.session.mprofile
                , setLink =
                    \link ({ session } as m) ->
                        { m
                            | session =
                                { session
                                    | mprofile = Just link.profile
                                }
                        }
                , getBody =
                    \m ->
                        case m.page of
                            PageLogin layer ->
                                Just layer

                            _ ->
                                Nothing
                , setBody =
                    \layer m ->
                        { m | page = PageLogin layer }
                }
        ]

If the target layer disappears during the execution of a given Promise, the rest of the process for the body part is aborted, and returns BodyExpired. If the link becomes to be unreachable during the execution of a given Promise, the rest of the process for the link part is aborted, and returns LinkExpired.

onChildLayer : { getLink : { link : Maybe link0, body : Maybe body0 } -> Maybe link1, setLink : link1 -> { link : Maybe link0, body : Maybe body0 } -> { link : Maybe link0, body : Maybe body0 }, getBody : { link : Maybe link0, body : Maybe body0 } -> Maybe (Layer body1), setBody : Layer body1 -> { link : Maybe link0, body : Maybe body0 } -> { link : Maybe link0, body : Maybe body0 } } -> Promise (LayerMemory link1 body1) a -> Promise (LayerMemory link0 body0) (ResultOnLayer a)

Similar to onLayer, but run on the parent Layer.

View

The View determines how your application is rendered in the web browser, based only on the current Layer state. You use Tepa.Html and Tepa.Mixin to tell the web browser how to render the page.


type alias Html =
Html Msg


type alias Mixin =
Mixin Msg

Define view

To define view function, you use layerView.

layerView : (ViewContext -> m -> view) -> Layer m -> view


type alias ViewContext =
{ setKey : String -> Mixin
, setKeyAndId : String -> Mixin
, values : Dict String String
, checks : Dict String Basics.Bool
, setKey_ : String -> List (Html.Attribute Msg) 
}

Keys

Key is used to specify a specific View element.

import Tepa exposing (Layer)
import Tepa.Html as Html exposing (Html)
import Tepa.Mixin as Mixin

formView : Layer Memory -> Html
formView =
    Tepa.layerView <|
        \{ setKey, values } _ ->
            Html.node "form"
                [ Mixin.attribute "novalidate" "true"
                ]
                [ Html.node "label"
                    []
                    [ Html.text "Name: "
                    , Html.node "input"
                        [ setKey "form_name"
                        , Mixin.attribute "type" "text"
                        , Mixin.attribute "placeholder" "Sakura-chan"
                        ]
                        []
                    ]
                , Html.button
                    [ setKey "form_submit"
                    ]
                    [ Html.text "Submit"
                    ]
                , errors values
                ]

errors : Dict String String -> Html
errors formValues =
    case Dict.get "form_name" formValues of
        Just "" ->
            Html.text "Name is required."

        _ ->
            Html.text ""

This example uses the setKey of the ViewContext to set the Key named "form_name" to the name input, and "form_submit" to the submit button. In this way, you can retrieve the user input value with the values field of the ViewContext.

Note that Keys on the same Layer must be unique.

For advanced use: You can use setKey alternative for Html.Attribute that elm/html exposes. This means you can use layout libraries like neat-layout or elm-ui.

Handle form value on Procedure

The key is also used to communicate View and Procedure. If you want to get user inputs on the Procedure side, you can use getValue or getValues.

getValue : String -> Promise m (Maybe String)

Get the user's input value for the view element identified by the key string.

Nothing means that the key does not exist or that the value of the element specified by the key has not been changed since the element was rendered.

setValue : String -> String -> Promise m ()

Set the user's input value for the view element identified by the key string.

Note that it only sets initial value, but does not overwrite user input value. It is due to a slightly awkward behavior of the Elm runtime. We plan to improve this behavior in the near future, but for most applications, just setting the default values should be fine.

You can see Page.Chat module of sample application for a real example of resetting user input.

Handle form check state on Procedure

You can get/set checked property values of radio/checkbox elements.

getCheck : String -> Promise m (Maybe Basics.Bool)

Get whether the checkbox/radio specified by the key string is checked.

Nothing means that the checkbox/radio element with the key does not exist or that the checked property value of the element specified by the key has not been changed since the element was rendered.

getChecks : Promise m (Dict String Basics.Bool)

Get all the check states in the Layer as Dict.

sample : Promise Memory ()
sample =
    Tepa.bind Tepa.getChecks <|
        \formChecks ->
            let
                targetCheck =
                    Dict.get "form_name" formChecks
                        |> Maybe.withDefault ""
            in
            [ handleCheck targetCheck
            ]

Handle View events on Procedure

To capture View events on Procedure, you can use awaitViewEvent or awaitCustomViewEvent.

awaitViewEvent : { key : String, type_ : String } -> Promise m ()

Wait for an event of the type specified by type_ to occur for the element specified by key.

viewEventStream : { key : String, type_ : String } -> Promise m (Internal.Core.Stream ())

Tepa.Stream

Freshness of input values

The user input values obtained by the value field of the ViewContext and getValue / getValues in the Procedure are updated whenever the change or blur event of the target element occurs. So if you want to implement something like an incremental search, getting values in this ways will not give you the latest input values. Use search type of input element or capture the input event with awaitCustomViewEvent to handle this situation.

Pseudo-namespace for keys

You may sometimes find yourself using an element many times in a page. In such cases, it is useful to give key a pseudo-namespace.

In the following bad example, a View called userCard is reused many times.

import Tepa exposing (ViewContext)
import Tepa.Html as Html exposing (Html)
import Tepa.Mixin as Mixin

userCards :
    { users : List User
    }
    -> ViewContext
    -> Html
userCards param context =
    Html.node "ol"
        []
        ( List.map
            (\user ->
                userCard
                    { user = user
                    }
                    context
            )
            param.users
        )

userCard :
    { user : User
    }
    -> ViewContext
    -> Html
userCard param ({ setKey } as context) =
    Html.div
        [ Mixin.class "userCard"
        ]
        [ if user.isEditing then
            userNameForm param context
          else
            Html.div []
                [ Html.span
                    [ Mixin.class "userCard_name"
                    ]
                    [ Html.text param.user.name
                    ]
                , Html.button
                    [ Mixin.class "userCard_editButton"
                    , Mixin.attribute "type" "button"
                    , setKey "editButton"
                    ]
                    [ Html.text "Edit"
                    ]
                ]
        ]

type alias User = {
    id : String
    name : String
    isEditing : Bool
}

This will result in duplicate use of the key named "editButton", which cannot be manipulated properly from the Procedure. Furthermore, since the name "editButton" is too generic, it is possible that a key with the same name is accidentally used in a completely unrelated location. In such cases, pushKeyPrefix is useful. When passing a ViewContext from a parent element to a child element, the pushKeyPrefix provides a pseudo-namespace.

userCards param context =
    Html.node "ol"
        []
        (List.map
            (\user ->
                userCard
                    { user = user
                    }
                    (Tepa.pushKeyPrefix ("userCard_" ++ user.id) context)
            )
            param.users
        )

userCard param ({ setKey } as context) =
    Html.div
        [ Mixin.class "userCard"
        ]
        [ if user.isEditing then
            userNameForm param context

          else
            Html.div []
                [ Html.span
                    [ Mixin.class "userCard_name"
                    ]
                    [ Html.text param.user.name
                    ]
                , Html.button
                    [ Mixin.class "userCard_editButton"
                    , Mixin.attribute "type" "button"
                    , setKey ".editButton"
                    ]
                    [ Html.text "Edit"
                    ]
                ]
        ]

In the above example, the actual key name given to the edit button would be "userCard_user01.editButton"; that is, you would be able to access the element from the Procedure as follows:

Procedure.awaitViewEvent
    { key = "userCard_user01.editButton"
    , type_ = "click"
    }

The pushKeyPrefix can be stacked; for example, you can use pushKeyPrefix further within userCard as follows:

userCard param ({ setKey } as context) =
    Html.div
        [ Mixin.class "userCard"
        ]
        [ if user.isEditing then
            userNameForm
                param
                (Tepa.pushKeyPrefix ".userNameForm" context)

          else
            Html.div []
                [ Html.span
                    [ Mixin.class "userCard_name"
                    ]
                    [ Html.text param.user.name
                    ]
                , Html.button
                    [ Mixin.class "userCard_editButton"
                    , Mixin.attribute "type" "button"
                    , setKey ".editButton"
                    ]
                    [ Html.text "Edit"
                    ]
                ]
        ]

In this case, using setKey ".foo" in userNameForm will actually give the key name userCard_user01.userNameForm.foo.

pushKeyPrefix : String -> ViewContext -> ViewContext

Push key prefix for the child element.

Assertion

You may have a situation where you do not want a Promise to result in a certain result, such as when your onLayer result in LayerNotExists on a Layer that should exist at that moment. Assertion is a good practice to detect such logic bugs.

assertionError : String -> Promise memory ()

Cause the scenario test to fail.

import Tepa

sampleProcedure =
    Tepa.bind
        (Tepa.onLayer
            { getLink = getMyLink
            , setLink = setMyLink
            , getBody = getMyBody
            , setBody = setMyBody
            }
            promiseOnLayer
        )
    <|
        \result ->
            case result of
                Tepa.SucceedOnLayer a ->
                    [ procedureOnSuccess
                    ]

                _ ->
                    [ Tepa.assertionError "Layer error on sampleProcedure"
                    , sendErrorLog "Layer error on sampleProcedure"
                    , handleError
                    ]

Scenario

To create user scenarios and generate tests for them, see the Tepa.Scenario module.

Headless

headless : { init : Json.Encode.Value -> Promise () ( flags, memory ), onLoad : flags -> Promise memory () } -> Program flags memory

Build headless application. You can use headless to save your scenario document as a markdown file.

Internal


type alias Msg =
Internal.Core.Msg