arowM/tepa/5.0.1

NOTE: the latest version is 6.0.0

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 : 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 =
Program 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 =
Promise memory result

The main difference from Promise in JavaScript is that Promise in TEPA does not be rejected. When you represent rejected state, just return Result value as its result. The rejection of JavaScript Promise is a sort of Exception, which drops context.

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
                ]
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
            ]
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 : Bool -> List (Promise m ()) -> Promise m ()

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

unless : 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.

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

{ request : PortRequest Msg
, response : PortResponse Msg
, portName : String
, requestBody : Value
}
-> Promise m 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 =
Value -> Cmd msg
type alias PortResponse msg =
(Value -> msg) -> Sub msg
{ request : PortRequest Msg
, response : PortResponse Msg
, cancel : PortRequest Msg
, portName : String
, requestBody : Value
}
-> Promise m (Stream Value)

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

  • request: Request JavaScript port server for some tasks.
  • response: Receive responses for the request from JavaScript port server.
  • cancel: Called on the current Layer expires or the Stream ends. The main use is to free resources, such as closing WebSocket connections.

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.

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

Transform the Promise Memory.

{ 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 )
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 ()
  • Flags: Flags value decoded by the init Procedure.

  • Application URL: The initial URL requested by a user.

    TEPA uses the AppUrl type to represent internal URLs.

  • Navigation Key: Required by functions exposed by Tepa.Navigation.

onUrlRequest

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

onUrlRequest : flags -> UrlRequest -> NavKey -> Promise memory ()
  • Flags: Flags value decoded by the init Procedure.
  • Requested URL: UrlRequest value that indecates requested URL.
  • Navigation Key: Required by functions exposed by Tepa.Navigation.
= 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.

In most cases, you will declare the function as follows:

onUrlRequest : Flags -> Tepa.UrlRequest -> NavKey -> Promise Memory ()
onUrlRequest _ urlRequest key =
    case urlRequest of
        Tepa.InternalPath url ->
            Nav.pushPath key url


        Tepa.ExternalPage href ->
            Nav.load href

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 ()
  • Flags: Flags value decoded by the init Procedure.
  • New URL: Loaded new URL.
  • Navigation Key: Required by functions exposed by Tepa.Navigation.

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

The LayerMemory has two parameters:

  • link: External Memory state which this Layer depends on.
  • body: The Memory body for the Layer.

For example, you can declare Memory for the home page as follows:

module Page.Home exposing
    ( Memory
    , MemoryBody
    )


import MyApp.Profile exposing (Profile)
import Tepa exposing (LayerMemory, Promise)


{-| Memory structure for home page.
-}
type alias Memory =
    LayerMemory
        MemoryLink
        MemoryBody


{-| Link to the external memory spaces.
The main use is to share the application state between pages.
-}
type alias MemoryLink =
    { profile : Profile
    }


{-| Memory body
-}
type alias MemoryBody =
    { clicked : Bool
    }

Now you can declare your Procedures.

init : Promise m MemoryBody
init =
    Tepa.succeed
        { clicked = False
        }


onLoad : Promise Memory ()
onLoad =
    Tepa.bind
        (Tepa.awaitViewEvent
            { key = "myButton"
            , type_ = "click"
            }
        )
    <|
        \() ->
            [ Tepa.bodySequence
                [ Tepa.modify <| \m -> { m | clicked = True }
                ]


            -- This operation affects other pages.
            , Tepa.linkSequence
                [ Tepa.modify <|
                    \({ profile } as m) ->
                        { m
                            | profile =
                                { profile | hasClickedHomeButton = True }
                        }
                ]
            ]
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.

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

Helper function to modify link part.

To call Procedures for LayerMemory on the Procedures for the parent Memory, you can use 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.

= SucceedOnLayer a
| BodyExpired
| LinkExpired

Represents result for onLayer.

  • SucceedOnLayer: The Promise has been successfully resolved.
  • BodyExpired: The layer has been expired during execution.
  • LinkExpired: The link which the layer depends on has been expired during execution.

To call Procedures for LayerMemory on the Procedures for the parent LayerMemory, you can use 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

The application function takes two types of Views: initView for the View during the initialization on page load, and view for Views after the initialization process.

Define view

To define view function, you use layerView.

layerView : (ViewContext -> m -> view) -> Layer m -> view
type alias ViewContext =
{ setKey : String -> Mixin
, values : Dict String String
, checks : Dict String Bool
, setKey_ : String -> List (Attribute Msg)
}
  • setKey: Set a key to the element.

  • values: Current values of the control elements, keyed by its key strings set with setKey.

  • checks: Current check state of the radio/check elements, keyed by its key strings set with setKey.

  • setKey_: (For advanced use) setKey for elm/html nodes.

import Tepa exposing (Document, Layer)


view : Layer Memory -> Document
view =
    Tepa.layerView <|
        \_ state ->
            { title = "Sample App"
            , body =
                [ case state.page of
                    PageNotFound ->
                        pageNotFoundView


                    PageLogin pageLogin ->
                        PageLogin.view pageLogin


                    PageHome pageHome ->
                        PageHome.view pageHome
                ]
            }

The state field of the ViewContext indicates current memory state of the Layer.

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.

getValues : Promise m (Dict String String)

Get all the user input values in the Layer as Dict.

sample : Promise Memory ()
sample =
    Tepa.bind Tepa.getValues <|
        \formValues ->
            let
                targetValue =
                    Dict.get "form_name" formValues
                        |> Maybe.withDefault ""
            in
            [ handleValue targetValue
            ]

The setValue is used to overwrite or initiate the input value from the Procedure side.

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.

Note that the Procedure can only get/set values in Views on the same Layer that the Procedure is executed.

Handle form check state on Procedure

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

getCheck : String -> Promise m (Maybe 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 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
            ]
setCheck : String -> Bool -> Promise m ()

Set the user's check state for the radio/checkbox element identified by the key string.

Note that it only sets initial state, but does not overwrite actual check state on DOM. 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.

Note that the Procedure can only get/set check state in Views on the same Layer that the Procedure is executed.

Handle View events on Procedure

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

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

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

{ key : String
, type_ : String
, decoder :
Decoder
{ stopPropagation : Bool
, preventDefault : Bool
, value : a
}
}
-> Promise m a

Advanced version of awaitViewEvent.

The decoder field is the Decoder for the target event object. If the returned stopPropagation value is True, it calls the Event.stopPropagation() method. If the returned preventDefault value is True, it calls the Event.preventDefault() method.

For more precise control, you can use Stream versions.

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

Tepa.Stream

{ key : String
, type_ : String
, decoder :
Decoder
{ stopPropagation : Bool
, preventDefault : Bool
, value : a
}
}
-> Promise m (Stream a)

Note that the Procedure can only capture events in Views on the same Layer that the Procedure is executed.

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.

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
                    ]

Assertion has no effect while the TEPA code is running as an application, but if the same code is used for scenario testing, the test will fail when an Assertion Error occurs.

Scenario

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

Headless

{ init : 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 =
Msg