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. ⚠⚠⚠⚠⚠⚠
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.
{ 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.
Platform.Program Json.Encode.Value (Model flags memory) Msg
A Program
describes an TEPA program.
An alias for Platform.Program.
{ 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.
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
}
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.
In TEPA, you describe application behaviour as Procedure, the time sequence of proccessing.
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.
Internal.Core.Promise memory result
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.
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
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 ()
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.
To request DOM related operations, you can use Tepa.Dom
module.
To navigate to another page, you can use Tepa.Navigation
module.
To request random values, you can use Tepa.Random
module.
To request time related operations, you can use Tepa.Time
module.
To send HTTP request to the backend server, you can use Tepa.Http
module.
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.
Json.Encode.Value -> Platform.Cmd.Cmd 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.
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!
Some Promises are resolved with Stream
, which is an advanced technique for controlling Promises precisely.
See Tepa.Stream for details.
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.
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
.
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 ()
init
Procedure.UrlRequest
value that indecates requested URL.Tepa.Navigation
.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 ()
init
Procedure.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.
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
init
Procedure initializing the page
in Memory to PageHome param
(where param
is the initial value of PageHome.Memory
).onLoad
Procedure is called, and it execute the Procedure for the home page.onChangeUrl
Procedure is called and the page
in memory is replaced with PageUsers param
(param
is the initial value of PageUsers.Memory
).onChangeUrl
Procedure is called, and the page
in memory is again replaced with the PageHome param
(param
is the initial value of PageHome.Memory
).onChangeUrl
Procedure continues to execute a new Procedure for the home page.PageHome
, Process A cannot detect that the page has changed in the middle of the process.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.
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)
On each Layer you define Memory with LayerMemory
.
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.
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.
Html Msg
Mixin Msg
view
To define view
function, you use layerView
.
layerView : (ViewContext -> m -> view) -> Layer m -> view
{ setKey : String -> Mixin
, setKeyAndId : String -> Mixin
, values : Dict String String
, checks : Dict String Basics.Bool
, setKey_ : String -> List (Html.Attribute Msg)
}
setKey
: Set a key to the element.
setKeyAndId
: Set a key and HTML ID to the element. Especially usefull when used with pushKeyPrefix
.
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.
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.
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.
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
]
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
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.
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.
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
]
To create user scenarios and generate tests for them, see the Tepa.Scenario
module.
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.Core.Msg