arowM/tepa/1.3.0

NOTE: the latest version is 6.0.0

Tepa

This module provides core functionality for TEPA.

Overall 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 src="./index.js"></script>
  </body>
</html>


// index.js


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


Elm.Main.init({
  // Create a `div` node in body and pass it as the parent node in which to render your TEPA application.
  node: document.body.appendChild(document.createElement("div")),
  // 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)


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


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


{-| Your implementation for `init`.
-}
init : Memory
init =
    Debug.todo ""


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


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


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


{-| Your implementation for `onUrlChange`.
-}
onUrlChange : Value -> AppUrl -> NavKey -> Promise memory ()
onUrlChange flags url key =
    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.

For TEA users: If you have an existing TEA application, you can use the low level API to partially integrate TEPA into your TEA application.

application : ApplicationProps memory -> Program memory

Entry point for building your applications.

type alias ApplicationProps memory =
{ init : memory
, procedure : Value -> AppUrl -> NavKey -> Promise memory ()
, view : memory -> Document
, onUrlRequest : Value -> UrlRequest -> NavKey -> Promise memory ()
, onUrlChange : Value -> AppUrl -> NavKey -> Promise memory ()
}

Property values for your application.

type alias Program memory =
Program Value (Model 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. You specify the initial state of the Memory as the init property.

{-| 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
    }


{-| Initial state of `Memory`. Pass it as `init` property to the `application` function.
-}
init : Memory
init =
    { pageState = InitialPageState
    }

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({
  // Create a `div` node in body and pass it as the parent node in which to render your TEPA application.
  node: document.body.appendChild(document.createElement("div")),
  // 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.

neverResolved : Promise m a

Promise that never be resolved. You can use neverResolved to prevent the current Layer (See Layer) from expiring.

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.

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({
  node: document.body.appendChild(document.createElement("div")),
  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
, 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.

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!

{ request : PortRequest Msg
, response : PortResponse Msg
, cancel : PortRequest Msg
, portName : String
, requestBody : Value
}
-> Promise m (Stream Value)

Advanced version of portStream.

The PortRequest specified as cancel is called when the Layer expires or the Stream ends. You can use portStreamWithCancel to manage JavaScript resources, such as WebSocket connections.

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.

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 three type of procedures you specify as an application property.

Main Procedure

The main procedure, set as the value of the procedure property, is executed on every page load. It takes three arguments:

  • Flags: JSON value passed by JavaScript on initialization.

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

Procedure to handle page transition requests.

You specify the procedure for handling page transition requests as onUrlRequest property of application. It takes three arguments:

  • Flags: JSON value passed by JavaScript on initialization.
  • 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 : Value -> Tepa.UrlRequest -> NavKey -> Promise Memory ()
onUrlRequest _ urlRequest key =
    case urlRequest of
        Tepa.InternalPath url ->
            Nav.pushPath key url


        Tepa.ExternalPage href ->
            Nav.load href

Procedure executed after URL changed

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

It takes three arguments:

  • Flags: JSON value passed by JavaScript on initialization.
  • 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

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.

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)
{ get : m -> Maybe (Layer m1)
, set : Layer m1 -> m -> m
}
-> Promise m1 a
-> Promise m (LayerResult a)

Run Promise on the specified Layer.

It determines the Layer to execute a given Promise with the parameter get based on the current memory state.

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
                { get =
                    \m ->
                        case m.page of
                            PageLogin layer ->
                                Just layer


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

Here, modify is called just before onLayer, and the bind guarantees that the onLayer request is generated based on the result of modify, even if there is another Promise to be executed in concurrently.

If the target layer disappears during the execution of a given Promise, the rest of the process is aborted, and if the result of the get parameter is Nothing at the time onLayer is called, the process is also aborted there.

= LayerOk a
| LayerNotExists
| LayerExpired
isOnSameLayer : Layer m -> Layer m -> Bool

Check if the both are on the same layer. One of the use cases is to find the target element from the list of layers.

See Widget.Toast in the sample application for detail.

currentLayerId : Promise m String

Request unique string for current Layer. The value is equivalent to the layerId value of the ViewContext.

layerState : Layer m1 -> Promise m m1

Promise that requests the current Memory state of a Layer.

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 Page.Home as PageHome
import Page.Users as PageUsers
import Tepa exposing (Layer)


type alias Memory =
    { page : Page
    }


type Page
    = PageLoading
    | PageHome PageHome.Memory
    | PageUsers PageUsers.Memory

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

  1. A user opens the home page.
    • The main procedure is executed, replacing the page in memory with the PageHome param (where param is the initial value of PageHome.Memory).
    • The main procedure continues to execute the procedure for the home page.
    • The home procedure is executed to replace the image to be displayed every 20 seconds (process A).
  2. The user is redirected to the user list page.
    • The onChangeUrl procedure is executed 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 executed, 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
    = PageLoading
    | PageHome (Layer PageHome.Memory)
    | PageUsers (Layer PageUsers.Memory)

The new Page' definition above usesLayer' 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.

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 Views

To define View function, you use layerView.

layerView : (ViewContext m -> view) -> Layer m -> view
type alias ViewContext m =
{ state : m
, setKey : String -> Mixin
, values : Dict String String
, checks : Dict String Bool
, layerId : String
, setKey_ : String -> List (Attribute Msg)
}
  • state: Current Memory state.

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

  • layerId: Unique string that identifies current Layer. The string consists of characters in the code point range U+0020 to U+D7FF. Because it contains no ASCII whitespace, it is safe to use as a part of the ID attribute value in HTML5.

  • setKey_: (Only for TEA users) TEA version of setKey.

mapViewContext : (m -> m1) -> ViewContext m -> ViewContext m1
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, Msg)
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 TEA users: You can use setKey alternative for Html.Attribute that elm/html expose. It means you can use layout libraries such as 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.

Helpers to handle form state

getFormState : Promise m FormState

Helper function to run getValues and getChecks concurrently.

type alias FormState =
{ values : Dict String String
, checks : Dict String Bool
}

Helper type alias representing a form state.

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

Scenario

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

Headless

{ init : memory
, procedure : Value -> Promise memory ()
}
-> Program memory

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

Connect to TEA app

For TEA users: If you have an existing application built with TEA, you can partially replace it with TEPA.

update : Msg -> Model memory -> ( Model memory, Cmd Msg )

TEA update function to execute your Procedures.

subscriptions : Model memory -> Sub Msg

TEA subscriptions function to execute your Procedures.

memory
-> { procedure : Promise memory ()
, onUrlChange : AppUrl -> Promise memory ()
, onUrlRequest : UrlRequest -> Promise memory ()
}
-> ( Model memory, Cmd Msg )

Construct the initial TEA data from Procedures.

type alias Msg =
Msg
type alias Model m =
Model m
onUrlChange : Url -> Msg

TEA onUrlChange property value.

onUrlRequest : UrlRequest -> Msg

TEA onUrlRequest property value.

Unsafe functions

unsafePush : (m -> Cmd Msg) -> Promise m ()

Lower level function to push TEA Commands.

For detailed documentation and testing, do not use this. We recommend using special functions like Tepa.Http.Request.

If you think you need to use this function, please check the following points:

  1. Check to see if there is an alternative function for TEPA that does what you want to do

  2. If not, report it in an issue on GitHub.

  3. If you still want to use it, use it at your own risk and do not ask any questions or bother the TEPA developers.