Skip to content
This repository was archived by the owner on Nov 20, 2025. It is now read-only.

Commit c387ee1

Browse files
Working TextInput component, needs massaging
1 parent 21c57f9 commit c387ee1

2 files changed

Lines changed: 117 additions & 5 deletions

File tree

sandbox-ui/src/main/scala/example/Model.scala

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import cats.effect.IO
44
import tyrian.*
55
import tyrian.next.*
66
import tyrian.ui.*
7+
import tyrian.ui.input.TextInput
78

89
final case class Model(
9-
topNav: TopNav
10+
topNav: TopNav,
11+
nameInput: TextInput.State
1012
):
1113

1214
def update: GlobalMsg => Outcome[Model] =
@@ -20,24 +22,35 @@ final case class Model(
2022
case e =>
2123
for {
2224
tn <- topNav.update(e)
25+
ni <- nameInput.update(e)
2326
} yield this.copy(
24-
topNav = tn
27+
topNav = tn,
28+
nameInput = ni
2529
)
2630

2731
def view(using Theme): HtmlFragment =
2832
topNav.view |+|
29-
Model.tmpView
33+
Model.tmpView(this)
3034

3135
object Model:
3236
val init: Model =
33-
Model(TopNav.initial)
37+
Model(TopNav.initial, TextInput.State("name-input"))
3438

35-
def tmpView(using Theme): HtmlFragment =
39+
def tmpView(m: Model)(using Theme): HtmlFragment =
3640
HtmlFragment(
3741
Row(
3842
Column(
3943
TextBlock("Welcome to Tyrian UI!").toHeading1
4044
.withColor(RGBA.fromHex("#2563eb")),
45+
Row(
46+
Column(
47+
TextBlock("Your name:"),
48+
TextInput(
49+
m.nameInput.withPlaceholder("Type here...")
50+
),
51+
TextBlock("Reversed: " + m.nameInput.value.reverse)
52+
)
53+
).withSpacing(Spacing.Small),
4154
Row(
4255
TextBlock("Hello, Tyrian!").withColor(RGBA.Blue),
4356
TextBlock("More text").withColor(RGBA.Red.mix(RGBA.Blue))
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package tyrian.ui.input
2+
3+
import tyrian.Elem
4+
import tyrian.Empty
5+
import tyrian.EmptyAttribute
6+
import tyrian.Html.*
7+
import tyrian.next.GlobalMsg
8+
import tyrian.next.Outcome
9+
import tyrian.ui.Theme
10+
import tyrian.ui.UIElement
11+
12+
final case class TextInput(
13+
state: TextInput.State,
14+
classNames: Set[String],
15+
_modifyTheme: Option[Theme => Theme]
16+
) extends UIElement[TextInput] {
17+
18+
def withState(next: TextInput.State): TextInput =
19+
this.copy(state = next)
20+
21+
def withClassNames(classes: Set[String]): TextInput =
22+
this.copy(classNames = classes)
23+
24+
def modifyTheme(f: Theme => Theme): TextInput =
25+
this.copy(_modifyTheme = Some(f))
26+
27+
def toHtml: Theme ?=> Elem[GlobalMsg] = {
28+
val disabledAttr = if state.disabled then attribute("disabled", "true") else EmptyAttribute
29+
val readonlyAttr = if state.readonly then attribute("readonly", "true") else EmptyAttribute
30+
31+
val inputAttrs = List(
32+
placeholder := state.placeholder,
33+
value := state.value,
34+
typ := state.kind,
35+
onInput((s: String) => TextInput.Msg.Changed(state.uid, s)),
36+
disabledAttr,
37+
readonlyAttr
38+
)
39+
40+
val clearBtn =
41+
if state.showClearIcon && state.value.nonEmpty then button(onClick(TextInput.Msg.Cleared(state.uid)))(text("×"))
42+
else Empty
43+
44+
div()(input(inputAttrs*), clearBtn)
45+
}
46+
}
47+
48+
object TextInput {
49+
50+
enum Msg extends GlobalMsg {
51+
case Changed(uid: String, value: String)
52+
case Cleared(uid: String)
53+
}
54+
55+
final case class State(
56+
placeholder: String,
57+
showClearIcon: Boolean,
58+
disabled: Boolean,
59+
readonly: Boolean,
60+
value: String,
61+
kind: String,
62+
uid: String
63+
) derives CanEqual {
64+
65+
def withPlaceholder(p: String): State = this.copy(placeholder = p)
66+
def showClearIconOn: State = this.copy(showClearIcon = true)
67+
def showClearIconOff: State = this.copy(showClearIcon = false)
68+
def withDisabled(d: Boolean): State = this.copy(disabled = d)
69+
def withReadonly(r: Boolean): State = this.copy(readonly = r)
70+
def withValue(v: String): State = this.copy(value = v)
71+
def withKind(k: String): State = this.copy(kind = k)
72+
def withUid(id: String): State = this.copy(uid = id)
73+
74+
def update: GlobalMsg => Outcome[State] = {
75+
case Msg.Changed(id, v) if id == uid => Outcome(this.copy(value = v))
76+
case Msg.Cleared(id) if id == uid => Outcome(this.copy(value = ""))
77+
case _ => Outcome(this)
78+
}
79+
}
80+
81+
object State {
82+
val default: State =
83+
State(
84+
placeholder = "",
85+
showClearIcon = true,
86+
disabled = false,
87+
readonly = false,
88+
value = "",
89+
kind = "text",
90+
uid = ""
91+
)
92+
93+
def apply(uid: String): State =
94+
default.copy(uid = uid)
95+
}
96+
97+
def apply(state: State): TextInput =
98+
TextInput(state, Set.empty, None)
99+
}

0 commit comments

Comments
 (0)