xiaohongshu
xiaohongshu
elm-development
Comprehensive Elm functional programming for web development including The Elm Architecture, type system, JavaScript interop, and production-ready patterns
Full skill instructions
A comprehensive skill for building reliable web applications using Elm, a functional programming language that compiles to JavaScript. Elm offers no runtime errors in practice, friendly compiler messages, and a delightful development experience through strong typing and pure functional programming.
Use this skill when:
The Elm Architecture is a pattern for building interactive applications that emerged naturally from Elm development. It inspired Redux and similar state management patterns.
Core Components:
Data Flow:
Model → View → HTML
↑ ↓
└── Update ←──┘
Messages
Basic Pattern:
type alias Model =
{ count : Int }
type Msg
= Increment
| Decrement
init : Model
init =
{ count = 0 }
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
Decrement ->
{ model | count = model.count - 1 }
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model.count) ]
, button [ onClick Increment ] [ text "+" ]
]
For real applications, you need side effects (HTTP, random, etc.):
Enhanced Components:
(Model, Cmd Msg) - initial state plus commands to run(Model, Cmd Msg) - new state plus commands to runProgram Types:
Browser.sandbox: Simple apps without side effectsBrowser.element: Apps with side effects (HTTP, etc.)Browser.document: Control over <title> and <body>Browser.application: Full SPA with URL routingElm's type system prevents runtime errors through static analysis and type inference.
Type Annotations:
-- Function type annotations
add : Int -> Int -> Int
add x y = x + y
-- Record type alias
type alias User =
{ id : Int
, name : String
, email : String
}
-- Custom types (sum types / tagged unions)
type UserStatus
= Active
| Suspended
| Deleted
-- Parametric types
type Result error value
= Ok value
| Err error
Type Inference: Elm infers types automatically, but annotations are recommended for top-level functions:
-- Compiler infers: String -> String
greet name =
"Hello, " ++ name
-- Better: Add explicit annotation
greet : String -> String
greet name =
"Hello, " ++ name
Benefits:
Elm eliminates null/undefined errors using Maybe and Result.
Maybe Type:
type Maybe a
= Just a
| Nothing
-- Example: Finding a user
findUser : Int -> Maybe User
findUser id =
-- Returns Just user if found, Nothing otherwise
...
-- Pattern matching on Maybe
case findUser 123 of
Just user ->
"Found: " ++ user.name
Nothing ->
"User not found"
-- Helper functions
Maybe.withDefault : a -> Maybe a -> a
Maybe.map : (a -> b) -> Maybe a -> Maybe b
Maybe.andThen : (a -> Maybe b) -> Maybe a -> Maybe b
Result Type:
type Result error value
= Ok value
| Err error
-- Example: Validating user input
validateAge : String -> Result String Int
validateAge input =
case String.toInt input of
Nothing ->
Err "Not a valid number"
Just age ->
if age < 0 then
Err "Age cannot be negative"
else if age > 150 then
Err "Age seems unrealistic"
else
Ok age
-- Using Result
case validateAge "25" of
Ok age ->
"Valid age: " ++ String.fromInt age
Err error ->
"Error: " ++ error
Pattern matching is fundamental to Elm programming:
-- Matching on custom types
type Traffic
= Red
| Yellow
| Green
describe : Traffic -> String
describe light =
case light of
Red ->
"Stop"
Yellow ->
"Prepare to stop"
Green ->
"Go"
-- Matching on lists
describeList : List a -> String
describeList list =
case list of
[] ->
"Empty list"
[single] ->
"One item"
[first, second] ->
"Two items"
_ ->
"Many items"
-- Matching with extraction
type User
= Anonymous
| LoggedIn String Int
getUserName : User -> String
getUserName user =
case user of
Anonymous ->
"Guest"
LoggedIn name _ ->
name
Function Definition:
-- Simple function
double : Int -> Int
double x =
x * 2
-- Multiple parameters
add : Int -> Int -> Int
add x y =
x + y
-- Partial application
add5 : Int -> Int
add5 =
add 5
-- Anonymous functions (lambdas)
doubleList : List Int -> List Int
doubleList numbers =
List.map (\n -> n * 2) numbers
-- Pipe operator
result : Int
result =
[1, 2, 3, 4, 5]
|> List.map double
|> List.filter (\n -> n > 5)
|> List.sum
-- Function composition
addThenDouble : Int -> Int
addThenDouble =
add 3 >> double
Let Expressions:
calculateArea : Float -> Float -> Float
calculateArea width height =
let
perimeter =
2 * (width + height)
area =
width * height
in
area
Record Syntax:
-- Record type alias
type alias Point =
{ x : Float
, y : Float
}
-- Creating records
origin : Point
origin =
{ x = 0, y = 0 }
-- Accessing fields
getX : Point -> Float
getX point =
point.x
-- Field access function
.x origin -- Returns 0
-- Updating records (immutable)
moveRight : Point -> Point
moveRight point =
{ point | x = point.x + 1 }
-- Updating multiple fields
moveDiagonal : Point -> Point
moveDiagonal point =
{ point
| x = point.x + 1
, y = point.y + 1
}
-- Pattern matching on records
distance : Point -> Float
distance { x, y } =
sqrt (x * x + y * y)
Lists (Linked Lists):
-- List literals
numbers : List Int
numbers =
[1, 2, 3, 4, 5]
-- Cons operator
moreNumbers : List Int
moreNumbers =
0 :: numbers -- [0, 1, 2, 3, 4, 5]
-- List functions
List.map : (a -> b) -> List a -> List b
List.filter : (a -> Bool) -> List a -> List a
List.foldl : (a -> b -> b) -> b -> List a -> b
List.length : List a -> Int
List.reverse : List a -> List a
List.sort : List comparable -> List comparable
List.concat : List (List a) -> List a
-- Example usage
processNumbers : List Int -> Int
processNumbers nums =
nums
|> List.filter (\n -> n > 0)
|> List.map (\n -> n * 2)
|> List.sum
Arrays (Random Access):
import Array exposing (Array)
-- Arrays for fast random access
numbers : Array Int
numbers =
Array.fromList [1, 2, 3, 4, 5]
-- Array operations
Array.get : Int -> Array a -> Maybe a
Array.set : Int -> a -> Array a -> Array a
Array.push : a -> Array a -> Array a
Array.length : Array a -> Int
Module Definition:
module Utils exposing (capitalize, truncate)
-- Private function (not exposed)
isBlank : String -> Bool
isBlank str =
String.trim str == ""
-- Public function
capitalize : String -> String
capitalize str =
if isBlank str then
str
else
String.toUpper (String.left 1 str) ++ String.dropLeft 1 str
-- Public function
truncate : Int -> String -> String
truncate maxLength str =
if String.length str > maxLength then
String.left (maxLength - 3) str ++ "..."
else
str
Import Syntax:
-- Import entire module
import List
import Dict
-- Import with alias
import Json.Decode as Decode
import Json.Encode as Encode
-- Import specific functions
import List exposing (map, filter, foldl)
-- Import all exposed functions (use sparingly)
import Html exposing (..)
Commands represent side effects to perform:
type Msg
= FetchUser
| GotUser (Result Http.Error User)
init : (Model, Cmd Msg)
init =
( { user = Nothing }
, Cmd.none -- No command initially
)
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
FetchUser ->
( model
, Http.get
{ url = "/api/user"
, expect = Http.expectJson GotUser userDecoder
}
)
GotUser result ->
case result of
Ok user ->
( { model | user = Just user }
, Cmd.none
)
Err _ ->
( model
, Cmd.none
)
-- Combining commands
Cmd.batch : List (Cmd msg) -> Cmd msg
Cmd.batch
[ fetchUser
, fetchPosts
, logEvent
]
Subscriptions listen to external events:
import Browser.Events
import Time
type Msg
= Tick Time.Posix
| KeyPressed String
| MouseMoved Int Int
subscriptions : Model -> Sub Msg
subscriptions model =
if model.isActive then
Sub.batch
[ Time.every 1000 Tick
, Browser.Events.onKeyPress keyDecoder
, Browser.Events.onMouseMove mouseDecoder
]
else
Sub.none
-- Subscription functions
Time.every : Float -> (Time.Posix -> msg) -> Sub msg
Browser.Events.onKeyPress : Decode.Decoder msg -> Sub msg
Browser.Events.onAnimationFrame : (Time.Posix -> msg) -> Sub msg
HTML Generation:
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
view : Model -> Html Msg
view model =
div [ class "container" ]
[ header [ class "header" ]
[ h1 [] [ text "My App" ]
]
, main_ [ class "content" ]
[ viewUserForm model.form
, viewUserList model.users
]
, footer [ class "footer" ]
[ text "© 2025" ]
]
-- Conditional rendering
viewIf : Bool -> Html msg -> Html msg
viewIf condition content =
if condition then
content
else
text ""
-- List rendering
viewUserList : List User -> Html Msg
viewUserList users =
div [ class "user-list" ]
[ h2 [] [ text "Users" ]
, ul [] (List.map viewUser users)
]
viewUser : User -> Html Msg
viewUser user =
li [ class "user-item" ]
[ text user.name
, button [ onClick (DeleteUser user.id) ]
[ text "Delete" ]
]
Nested Updates:
type alias Model =
{ loginForm : LoginForm
, settings : Settings
}
type Msg
= LoginFormMsg LoginFormMsg
| SettingsMsg SettingsMsg
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
LoginFormMsg formMsg ->
let
(newForm, formCmd) =
LoginForm.update formMsg model.loginForm
in
( { model | loginForm = newForm }
, Cmd.map LoginFormMsg formCmd
)
SettingsMsg settingsMsg ->
let
(newSettings, settingsCmd) =
Settings.update settingsMsg model.settings
in
( { model | settings = newSettings }
, Cmd.map SettingsMsg settingsCmd
)
GET Requests:
import Http
import Json.Decode as Decode
type Msg
= FetchData
| GotData (Result Http.Error String)
fetchData : Cmd Msg
fetchData =
Http.get
{ url = "https://api.example.com/data"
, expect = Http.expectString GotData
}
-- With JSON decoder
type alias User =
{ id : Int
, name : String
}
fetchUser : Int -> Cmd Msg
fetchUser userId =
Http.get
{ url = "https://api.example.com/users/" ++ String.fromInt userId
, expect = Http.expectJson GotUser userDecoder
}
userDecoder : Decode.Decoder User
userDecoder =
Decode.map2 User
(Decode.field "id" Decode.int)
(Decode.field "name" Decode.string)
POST Requests:
import Json.Encode as Encode
type Msg
= CreateUser
| UserCreated (Result Http.Error User)
createUser : String -> String -> Cmd Msg
createUser name email =
Http.post
{ url = "https://api.example.com/users"
, body = Http.jsonBody (encodeUser name email)
, expect = Http.expectJson UserCreated userDecoder
}
encodeUser : String -> String -> Encode.Value
encodeUser name email =
Encode.object
[ ("name", Encode.string name)
, ("email", Encode.string email)
]
Custom Requests:
updateUser : Int -> User -> Cmd Msg
updateUser userId user =
Http.request
{ method = "PUT"
, headers = [ Http.header "Authorization" "Bearer token123" ]
, url = "https://api.example.com/users/" ++ String.fromInt userId
, body = Http.jsonBody (encodeUserUpdate user)
, expect = Http.expectJson UserUpdated userDecoder
, timeout = Just 10000
, tracker = Nothing
}
Basic Decoders:
import Json.Decode as Decode exposing (Decoder)
-- Primitive decoders
stringDecoder : Decoder String
stringDecoder = Decode.string
intDecoder : Decoder Int
intDecoder = Decode.int
floatDecoder : Decoder Float
floatDecoder = Decode.float
boolDecoder : Decoder Bool
boolDecoder = Decode.bool
-- Field extraction
nameDecoder : Decoder String
nameDecoder =
Decode.field "name" Decode.string
-- Nested fields
streetDecoder : Decoder String
streetDecoder =
Decode.at ["address", "street"] Decode.string
Complex Decoders:
type alias User =
{ id : Int
, name : String
, email : String
, age : Maybe Int
, isActive : Bool
}
userDecoder : Decoder User
userDecoder =
Decode.map5 User
(Decode.field "id" Decode.int)
(Decode.field "name" Decode.string)
(Decode.field "email" Decode.string)
(Decode.maybe (Decode.field "age" Decode.int))
(Decode.field "is_active" Decode.bool)
-- Alternative with pipeline style (requires elm-json-decode-pipeline)
userDecoderPipeline : Decoder User
userDecoderPipeline =
Decode.succeed User
|> required "id" Decode.int
|> required "name" Decode.string
|> required "email" Decode.string
|> optional "age" (Decode.maybe Decode.int) Nothing
|> required "is_active" Decode.bool
List and Array Decoders:
-- Decode list of users
usersDecoder : Decoder (List User)
usersDecoder =
Decode.list userDecoder
-- Decode field containing list
postsDecoder : Decoder (List Post)
postsDecoder =
Decode.field "posts" (Decode.list postDecoder)
-- Decode dictionary
userByIdDecoder : Decoder (Dict String User)
userByIdDecoder =
Decode.dict userDecoder
Handling Decode Errors:
type Msg
= GotData (Result Http.Error Data)
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
GotData result ->
case result of
Ok data ->
( { model | data = Just data }
, Cmd.none
)
Err httpError ->
case httpError of
Http.BadUrl url ->
( { model | error = Just ("Bad URL: " ++ url) }
, Cmd.none
)
Http.Timeout ->
( { model | error = Just "Request timeout" }
, Cmd.none
)
Http.NetworkError ->
( { model | error = Just "Network error" }
, Cmd.none
)
Http.BadStatus code ->
( { model | error = Just ("Bad status: " ++ String.fromInt code) }
, Cmd.none
)
Http.BadBody message ->
( { model | error = Just ("Decode error: " ++ message) }
, Cmd.none
)
Pass data from JavaScript to Elm on initialization:
Elm Side:
type alias Flags =
{ apiKey : String
, userId : Int
, theme : String
}
init : Flags -> (Model, Cmd Msg)
init flags =
( { apiKey = flags.apiKey
, userId = flags.userId
, theme = flags.theme
, data = Nothing
}
, fetchUserData flags.userId
)
main : Program Flags Model Msg
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
JavaScript Side:
var app = Elm.Main.init({
node: document.getElementById('app'),
flags: {
apiKey: 'your-api-key',
userId: 12345,
theme: 'dark'
}
});
Defining Ports:
port module Main exposing (..)
-- Port for sending data to JavaScript
port saveToLocalStorage : String -> Cmd msg
-- Port for receiving data from JavaScript
port onStorageChange : (String -> msg) -> Sub msg
-- Using ports in update
type Msg
= SaveData String
| StorageChanged String
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
SaveData data ->
( model
, saveToLocalStorage data
)
StorageChanged data ->
( { model | data = data }
, Cmd.none
)
-- Using ports in subscriptions
subscriptions : Model -> Sub Msg
subscriptions model =
onStorageChange StorageChanged
JavaScript Side:
var app = Elm.Main.init({
node: document.getElementById('app')
});
// Receiving from Elm
app.ports.saveToLocalStorage.subscribe(function(data) {
localStorage.setItem('app-data', data);
});
// Sending to Elm
window.addEventListener('storage', function(e) {
if (e.key === 'app-data') {
app.ports.onStorageChange.send(e.newValue);
}
});
Common Port Patterns:
-- Local storage
port setStorage : String -> Cmd msg
port getStorage : (String -> msg) -> Sub msg
-- WebSocket
port sendSocketMessage : String -> Cmd msg
port receiveSocketMessage : (String -> msg) -> Sub msg
-- Analytics
port trackEvent : { category : String, action : String } -> Cmd msg
-- Third-party libraries
port initMap : { lat : Float, lng : Float } -> Cmd msg
port updateMarker : { lat : Float, lng : Float } -> Cmd msg
Embed Elm in existing applications using web components:
JavaScript Side:
customElements.define('elm-widget', class extends HTMLElement {
connectedCallback() {
this.app = Elm.Widget.init({
node: this,
flags: {
initialValue: this.getAttribute('value')
}
});
// Listen to Elm events
this.app.ports.valueChanged.subscribe(value => {
this.dispatchEvent(new CustomEvent('change', { detail: value }));
});
}
});
HTML Usage:
<elm-widget value="initial"></elm-widget>
Package vs Application:
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3"
},
"indirect": {}
},
"test-dependencies": {
"direct": {
"elm-explorations/test": "1.2.2"
},
"indirect": {}
}
}
Building:
# Compile to JavaScript
elm make src/Main.elm --output=main.js
# Compile with optimization
elm make src/Main.elm --output=main.js --optimize
# Compile multiple files
elm make src/Main.elm src/Admin.elm --output=dist/
# Debug build
elm make src/Main.elm --output=main.js --debug
Development:
# Start development server
elm reactor
# Install packages
elm install elm/http
elm install elm/json
elm install elm/random
# Format code
elm-format src/ --yes
# Run tests
elm-test
Optimization:
# Build optimized
elm make src/Main.elm --output=main.js --optimize
# Minify with uglify-js
uglifyjs main.js --compress 'pure_funcs=[F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9],pure_getters,keep_fargs=false,unsafe_comps,unsafe' | uglifyjs --mangle --output main.min.js
Recommended Structure:
my-elm-app/
├── elm.json # Package configuration
├── src/
│ ├── Main.elm # Entry point
│ ├── Models/
│ │ ├── User.elm
│ │ └── Post.elm
│ ├── Views/
│ │ ├── Home.elm
│ │ ├── Profile.elm
│ │ └── Common/
│ │ ├── Header.elm
│ │ └── Footer.elm
│ ├── Updates/
│ │ ├── User.elm
│ │ └── Post.elm
│ ├── Api/
│ │ ├── User.elm
│ │ └── Post.elm
│ ├── Utils/
│ │ ├── Validators.elm
│ │ └── Formatters.elm
│ └── Ports.elm
├── tests/
│ └── Tests.elm
├── public/
│ ├── index.html
│ ├── styles.css
│ └── assets/
└── README.md
Complete Form Pattern:
type alias Form =
{ email : String
, password : String
, errors : List String
, isSubmitting : Bool
}
type FormMsg
= UpdateEmail String
| UpdatePassword String
| SubmitForm
| FormSubmitted (Result Http.Error User)
initForm : Form
initForm =
{ email = ""
, password = ""
, errors = []
, isSubmitting = False
}
updateForm : FormMsg -> Form -> (Form, Cmd FormMsg)
updateForm msg form =
case msg of
UpdateEmail email ->
( { form | email = email }
, Cmd.none
)
UpdatePassword password ->
( { form | password = password }
, Cmd.none
)
SubmitForm ->
case validateForm form of
Ok validForm ->
( { form | isSubmitting = True, errors = [] }
, submitForm validForm
)
Err errors ->
( { form | errors = errors }
, Cmd.none
)
FormSubmitted result ->
case result of
Ok user ->
( initForm
, Cmd.none
)
Err _ ->
( { form
| isSubmitting = False
, errors = ["Submission failed"]
}
, Cmd.none
)
validateForm : Form -> Result (List String) Form
validateForm form =
let
errors =
[ validateEmail form.email
, validatePassword form.password
]
|> List.filterMap identity
in
if List.isEmpty errors then
Ok form
else
Err errors
viewForm : Form -> Html FormMsg
viewForm form =
Html.form [ onSubmit SubmitForm ]
[ input
[ type_ "email"
, value form.email
, onInput UpdateEmail
, disabled form.isSubmitting
]
[]
, input
[ type_ "password"
, value form.password
, onInput UpdatePassword
, disabled form.isSubmitting
]
[]
, div [ class "errors" ]
(List.map viewError form.errors)
, button
[ type_ "submit"
, disabled form.isSubmitting
]
[ text (if form.isSubmitting then "Submitting..." else "Submit") ]
]
URL Routing with Browser.application:
import Browser
import Browser.Navigation as Nav
import Url
import Url.Parser as Parser exposing (Parser, oneOf, s, int, (</>))
type Route
= Home
| Profile Int
| Settings
| NotFound
routeParser : Parser (Route -> a) a
routeParser =
oneOf
[ Parser.map Home Parser.top
, Parser.map Profile (s "profile" </> int)
, Parser.map Settings (s "settings")
]
fromUrl : Url.Url -> Route
fromUrl url =
Parser.parse routeParser url
|> Maybe.withDefault NotFound
type alias Model =
{ key : Nav.Key
, route : Route
}
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
init : () -> Url.Url -> Nav.Key -> (Model, Cmd Msg)
init _ url key =
( { key = key
, route = fromUrl url
}
, Cmd.none
)
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
LinkClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
( model
, Nav.pushUrl model.key (Url.toString url)
)
Browser.External href ->
( model
, Nav.load href
)
UrlChanged url ->
( { model | route = fromUrl url }
, Cmd.none
)
view : Model -> Browser.Document Msg
view model =
{ title = "My App"
, body =
[ viewNavigation
, viewRoute model.route
]
}
viewRoute : Route -> Html Msg
viewRoute route =
case route of
Home ->
viewHome
Profile userId ->
viewProfile userId
Settings ->
viewSettings
NotFound ->
div [] [ text "404 Not Found" ]
main : Program () Model Msg
main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = \_ -> Sub.none
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
Search with Debounce:
type alias Model =
{ searchQuery : String
, searchDebounce : Int
, results : List Result
}
type Msg
= UpdateSearch String
| DebounceTick Time.Posix
| PerformSearch
| GotResults (Result Http.Error (List Result))
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
UpdateSearch query ->
( { model
| searchQuery = query
, searchDebounce = 3 -- Wait 3 ticks
}
, Cmd.none
)
DebounceTick _ ->
if model.searchDebounce > 0 then
( { model | searchDebounce = model.searchDebounce - 1 }
, Cmd.none
)
else if model.searchQuery /= "" then
update PerformSearch model
else
( model, Cmd.none )
PerformSearch ->
( model
, searchApi model.searchQuery
)
GotResults result ->
case result of
Ok results ->
( { model | results = results }
, Cmd.none
)
Err _ ->
( model, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions model =
Time.every 300 DebounceTick -- Check every 300ms
Remote Data Pattern:
type RemoteData error value
= NotAsked
| Loading
| Success value
| Failure error
type alias Model =
{ userData : RemoteData Http.Error User
}
type Msg
= FetchUser
| GotUser (Result Http.Error User)
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
FetchUser ->
( { model | userData = Loading }
, fetchUser
)
GotUser result ->
case result of
Ok user ->
( { model | userData = Success user }
, Cmd.none
)
Err error ->
( { model | userData = Failure error }
, Cmd.none
)
viewUser : RemoteData Http.Error User -> Html Msg
viewUser userData =
case userData of
NotAsked ->
button [ onClick FetchUser ] [ text "Load User" ]
Loading ->
div [ class "spinner" ] [ text "Loading..." ]
Success user ->
div [ class "user" ]
[ h2 [] [ text user.name ]
, p [] [ text user.email ]
]
Failure error ->
div [ class "error" ]
[ text "Failed to load user"
, button [ onClick FetchUser ] [ text "Retry" ]
]
import Test exposing (..)
import Expect
suite : Test
suite =
describe "User Validation"
[ test "validates correct email" <|
\_ ->
validateEmail "[email protected]"
|> Expect.equal (Ok "[email protected]")
, test "rejects invalid email" <|
\_ ->
validateEmail "invalid"
|> Expect.err
, describe "Password validation"
[ test "requires minimum length" <|
\_ ->
validatePassword "short"
|> Expect.err
, test "accepts valid password" <|
\_ ->
validatePassword "securePassword123"
|> Expect.ok
]
]
-- Use semantic HTML
viewButton : String -> msg -> Html msg
viewButton label msg =
button
[ onClick msg
, ariaLabel label
]
[ text label ]
-- Provide keyboard navigation
viewModal : Html msg
viewModal =
div
[ role "dialog"
, ariaModal True
, tabindex -1
]
[ -- modal content
]
-- Use ARIA attributes
viewStatus : String -> Html msg
viewStatus message =
div
[ ariaLive "polite"
, ariaAtomic True
]
[ text message ]
Official Resources:
Key Packages:
Learning Resources:
Skill Version: 1.0.0 Last Updated: November 2025 Skill Category: Functional Programming, Web Development, Type-Safe Programming Compatible With: JavaScript, Web Components, SPA Frameworks
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience