Skip to content

Commit 6c99669

Browse files
authored
Fix various issues with datastar rendering of names and updates (#3854)
1 parent f675131 commit 6c99669

File tree

5 files changed

+670
-49
lines changed

5 files changed

+670
-49
lines changed

zio-http-datastar-sdk/src/main/scala/zio/http/datastar/Attributes.scala

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -265,16 +265,17 @@ object Attributes {
265265
def apply[A: Schema](signal: String): SignalAttr[A] =
266266
SignalAttr(attrName, SignalName(caseModifier)(signal).toSignal, caseModifier)
267267
def apply[A: Schema](signal: SignalName): SignalAttr[A] =
268-
SignalAttr(attrName, signal.caseModifier(caseModifier).toSignal, caseModifier)
268+
SignalAttr(attrName, signal.toSignal, caseModifier)
269269
def apply[A](signal: Signal[A]): SignalAttr[A] =
270-
SignalAttr(attrName, signal.caseModifier(caseModifier), caseModifier)
270+
SignalAttr(attrName, signal, caseModifier)
271271
}
272272

273273
final case class SignalAttr[A](attrName: String, signal: Signal[A], caseModifier: CaseModifier = CaseModifier.Camel) {
274-
private val full = s"$attrName:${signal.name.name}${caseModifier.suffix(CaseModifier.Camel)}"
274+
private val full = s"$attrName:${signal.name.name}${signal.name.caseModifier.suffix(CaseModifier.Camel)}"
275+
private val plain = s"$attrName${signal.name.caseModifier.suffix(CaseModifier.Camel)}"
275276

276-
def :=(expression: Js): Attribute = Dom.attr(full) := expression.value
277-
def :=(update: SignalUpdate[A]): Attribute = Dom.attr(full) := update.toExpression.value
277+
def :=(expression: Js): Attribute = Dom.attr(full) := expression.value
278+
def :=(update: SignalUpdate[A]): Attribute = Dom.attr(plain) := update.toExpression.value
278279
}
279280

280281
object SignalAttr {
@@ -290,9 +291,9 @@ object Attributes {
290291
def apply[A: Schema](signal: String): SignalsAttr[A] =
291292
SignalsAttr(prefix, SignalName(caseModifier)(signal).toSignal[A], caseModifier)
292293
def apply[A: Schema](signal: SignalName): SignalsAttr[A] =
293-
SignalsAttr(prefix, signal.caseModifier(caseModifier).toSignal[A], caseModifier)
294+
SignalsAttr(prefix, signal.toSignal[A], caseModifier)
294295
def apply[A: Schema](signal: Signal[A]): SignalsAttr[A] =
295-
SignalsAttr(prefix, signal.caseModifier(caseModifier), caseModifier)
296+
SignalsAttr(prefix, signal, caseModifier)
296297

297298
def :=(expression: Js): Attribute =
298299
Dom.attr(s"$prefix-signals${caseModifier.suffix(CaseModifier.Camel)}") := expression.value
@@ -306,32 +307,44 @@ object Attributes {
306307
) {
307308
private val full = {
308309
val ifMissing0: String = if (ifMissing) "__if-missing" else ""
309-
s"$prefix-signals:${signal.name.name}${caseModifier.suffix(CaseModifier.Camel)}$ifMissing0"
310+
s"$prefix-signals:${signal.name.name}${signal.name.caseModifier.suffix(CaseModifier.Camel)}$ifMissing0"
311+
}
312+
313+
private val plain = {
314+
val ifMissing0: String = if (ifMissing) "__if-missing" else ""
315+
s"$prefix-signals${signal.name.caseModifier.suffix(CaseModifier.Camel)}$ifMissing0"
310316
}
311317

312318
def :=(expression: Js): Attribute = Dom.attr(full) := expression.value
313319

314320
def :=(update: SignalUpdate[A]): Attribute =
315-
Dom.attr(s"$prefix-signals${caseModifier.suffix(CaseModifier.Camel)}") := update.toExpression.value
321+
Dom.attr(plain) := update.toExpression.value
316322

317323
def :=(in: A): Attribute =
318-
Dom.attr(s"$prefix-signals${caseModifier.suffix(CaseModifier.Camel)}") := signal.update(in).toExpression.value
324+
Dom.attr(plain) := signal
325+
.update(in)
326+
.toExpression
327+
.value
319328

320-
def camel: SignalsAttr[A] = copy(caseModifier = CaseModifier.Camel)
321-
def kebab: SignalsAttr[A] = copy(caseModifier = CaseModifier.Kebab)
322-
def snake: SignalsAttr[A] = copy(caseModifier = CaseModifier.Snake)
323-
def pascal: SignalsAttr[A] = copy(caseModifier = CaseModifier.Pascal)
329+
def camel: SignalsAttr[A] =
330+
copy(caseModifier = CaseModifier.Camel, signal = signal.caseModifier(CaseModifier.Camel))
331+
def kebab: SignalsAttr[A] =
332+
copy(caseModifier = CaseModifier.Kebab, signal = signal.caseModifier(CaseModifier.Kebab))
333+
def snake: SignalsAttr[A] =
334+
copy(caseModifier = CaseModifier.Snake, signal = signal.caseModifier(CaseModifier.Snake))
335+
def pascal: SignalsAttr[A] =
336+
copy(caseModifier = CaseModifier.Pascal, signal = signal.caseModifier(CaseModifier.Pascal))
324337
}
325338

326339
final case class PartialDataIndicator(prefix: String, caseModifier: CaseModifier = CaseModifier.Camel) {
327340
def apply(signal: String): DataIndicatorAttr =
328341
DataIndicatorAttr(prefix, SignalName(caseModifier)(signal).toSignal[Boolean], caseModifier)
329342

330343
def apply(signal: SignalName): DataIndicatorAttr =
331-
DataIndicatorAttr(prefix, signal.caseModifier(caseModifier).toSignal[Boolean], caseModifier)
344+
DataIndicatorAttr(prefix, signal.toSignal[Boolean], caseModifier)
332345

333346
def apply(signal: Signal[Boolean]): DataIndicatorAttr =
334-
DataIndicatorAttr(prefix, signal.caseModifier(caseModifier), caseModifier)
347+
DataIndicatorAttr(prefix, signal, caseModifier)
335348

336349
def camel: PartialDataIndicator = copy(caseModifier = CaseModifier.Camel)
337350
def kebab: PartialDataIndicator = copy(caseModifier = CaseModifier.Kebab)
@@ -344,12 +357,16 @@ object Attributes {
344357
signal: Signal[Boolean],
345358
caseModifier: CaseModifier = CaseModifier.Camel,
346359
) {
347-
private val full = s"$prefix-indicator:${signal.name.name}${caseModifier.suffix(CaseModifier.Camel)}"
360+
private val full = s"$prefix-indicator:${signal.name.name}${signal.name.caseModifier.suffix(CaseModifier.Camel)}"
348361

349-
def camel: DataIndicatorAttr = copy(caseModifier = CaseModifier.Camel)
350-
def kebab: DataIndicatorAttr = copy(caseModifier = CaseModifier.Kebab)
351-
def snake: DataIndicatorAttr = copy(caseModifier = CaseModifier.Snake)
352-
def pascal: DataIndicatorAttr = copy(caseModifier = CaseModifier.Pascal)
362+
def camel: DataIndicatorAttr =
363+
copy(caseModifier = CaseModifier.Camel, signal = signal.caseModifier(CaseModifier.Camel))
364+
def kebab: DataIndicatorAttr =
365+
copy(caseModifier = CaseModifier.Kebab, signal = signal.caseModifier(CaseModifier.Kebab))
366+
def snake: DataIndicatorAttr =
367+
copy(caseModifier = CaseModifier.Snake, signal = signal.caseModifier(CaseModifier.Snake))
368+
def pascal: DataIndicatorAttr =
369+
copy(caseModifier = CaseModifier.Pascal, signal = signal.caseModifier(CaseModifier.Pascal))
353370
}
354371

355372
object DataIndicatorAttr {
@@ -713,8 +730,8 @@ object Attributes {
713730

714731
final case class PartialDataBind(prefix: String, caseModifier: CaseModifier = CaseModifier.Camel) {
715732
def apply(signal: String): DataBind = DataBind(prefix, SignalName(caseModifier)(signal), caseModifier)
716-
def apply(signal: SignalName): DataBind = DataBind(prefix, signal.caseModifier(caseModifier), caseModifier)
717-
def apply(signal: Signal[_]): DataBind = DataBind(prefix, signal.name.caseModifier(caseModifier), caseModifier)
733+
def apply(signal: SignalName): DataBind = DataBind(prefix, signal, caseModifier)
734+
def apply(signal: Signal[_]): DataBind = DataBind(prefix, signal.name, caseModifier)
718735

719736
def camel: PartialDataBind = copy(caseModifier = CaseModifier.Camel)
720737
def kebab: PartialDataBind = copy(caseModifier = CaseModifier.Kebab)
@@ -723,12 +740,16 @@ object Attributes {
723740
}
724741

725742
final case class DataBind(prefix: String, signalName: SignalName, caseModifier: CaseModifier = CaseModifier.Camel) {
726-
private val full = s"$prefix-bind:${signalName.name}${caseModifier.suffix(CaseModifier.Camel)}"
743+
private val full = s"$prefix-bind:${signalName.name}${signalName.caseModifier.suffix(CaseModifier.Camel)}"
727744

728-
def camel: DataBind = copy(caseModifier = CaseModifier.Camel)
729-
def kebab: DataBind = copy(caseModifier = CaseModifier.Kebab)
730-
def snake: DataBind = copy(caseModifier = CaseModifier.Snake)
731-
def pascal: DataBind = copy(caseModifier = CaseModifier.Pascal)
745+
def camel: DataBind =
746+
copy(caseModifier = CaseModifier.Camel, signalName = signalName.caseModifier(CaseModifier.Camel))
747+
def kebab: DataBind =
748+
copy(caseModifier = CaseModifier.Kebab, signalName = signalName.caseModifier(CaseModifier.Kebab))
749+
def snake: DataBind =
750+
copy(caseModifier = CaseModifier.Snake, signalName = signalName.caseModifier(CaseModifier.Snake))
751+
def pascal: DataBind =
752+
copy(caseModifier = CaseModifier.Pascal, signalName = signalName.caseModifier(CaseModifier.Pascal))
732753
}
733754

734755
object DataBind {
@@ -737,7 +758,7 @@ object Attributes {
737758

738759
final case class PartialDataRef(prefix: String, caseModifier: CaseModifier = CaseModifier.Camel) {
739760
def apply(signal: String): DataRef = DataRef(prefix, SignalName(caseModifier)(signal), caseModifier)
740-
def apply(signal: SignalName): DataRef = DataRef(prefix, signal.caseModifier(caseModifier), caseModifier)
761+
def apply(signal: SignalName): DataRef = DataRef(prefix, signal, caseModifier)
741762

742763
def camel: PartialDataRef = copy(caseModifier = CaseModifier.Camel)
743764
def kebab: PartialDataRef = copy(caseModifier = CaseModifier.Kebab)
@@ -746,7 +767,7 @@ object Attributes {
746767
}
747768

748769
final case class DataRef(prefix: String, signalName: SignalName, caseModifier: CaseModifier = CaseModifier.Camel) {
749-
private val full = s"$prefix-ref:${signalName.name}${caseModifier.suffix(CaseModifier.Camel)}"
770+
private val full = s"$prefix-ref:${signalName.name}${signalName.caseModifier.suffix(CaseModifier.Camel)}"
750771

751772
def camel: DataRef = copy(caseModifier = CaseModifier.Camel)
752773
def kebab: DataRef = copy(caseModifier = CaseModifier.Kebab)
@@ -822,14 +843,21 @@ object Attributes {
822843
case object Window extends OptionLess
823844
}
824845

825-
sealed trait CaseModifier extends Product with Serializable {
826-
def modify(original: String): String = this match {
827-
case CaseModifier.Camel => zio.json.CamelCase(original)
828-
case CaseModifier.Kebab => zio.json.KebabCase(original)
829-
case CaseModifier.Snake => zio.json.SnakeCase(original)
830-
case CaseModifier.Pascal => zio.json.PascalCase(original)
846+
sealed trait CaseModifier extends Product with Serializable { self =>
847+
def modify(original: String): String = {
848+
val privateSignal = original.startsWith("_")
849+
val toModify = if (privateSignal) original.drop(1) else original
850+
val modified =
851+
self match {
852+
case CaseModifier.Camel => zio.json.CamelCase(toModify)
853+
case CaseModifier.Kebab => zio.json.KebabCase(toModify)
854+
case CaseModifier.Snake => zio.json.SnakeCase(toModify)
855+
case CaseModifier.Pascal => zio.json.PascalCase(toModify)
856+
}
857+
if (privateSignal) "_" + modified else modified
831858
}
832-
def suffix(default: CaseModifier): String = this match {
859+
860+
def suffix(default: CaseModifier): String = self match {
833861
case `default` => ""
834862
case CaseModifier.Camel => "__case.camel"
835863
case CaseModifier.Kebab => "__case.kebab"

zio-http-datastar-sdk/src/main/scala/zio/http/datastar/signal/Signal.scala

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,22 +97,51 @@ object SignalName {
9797
*/
9898
final case class SignalUpdate[A](signal: Signal[A], value: A) {
9999
private implicit val codec: JsonCodec[A] = zio.schema.codec.JsonCodec.jsonCodec(signal.schema)
100-
def toExpression: Js = {
101-
val update = signal.schema match {
102-
case _: Schema.Primitive[_] => value.toJson.replace("\"", "'")
103-
case _ =>
104-
val ast = value.toJsonAST.getOrElse(throw new RuntimeException("Failed to convert value to JSON AST"))
105-
val nested = signal.name.path.foldRight(ast) { (key, acc) =>
100+
101+
/**
102+
* Render as a JSON object expression with the signal name/path included (for
103+
* data-signals attributes)
104+
*/
105+
def toExpression: Js = {
106+
val ast = value.toJsonAST.getOrElse(throw new RuntimeException("Failed to convert value to JSON AST"))
107+
val nested = signal.name.path.foldRight(ast) { (key, acc) =>
108+
zio.json.ast.Json.Obj(key -> acc)
109+
}
110+
js"${SignalUpdate.astToExpression(nested)}"
111+
}
112+
113+
/**
114+
* Render as an assignment expression (for event handlers like dataOn.click)
115+
*/
116+
private[http] def toAssignmentExpression: Js = {
117+
signal.name.path match {
118+
case _ :: Nil =>
119+
// Root signal: $name = value
120+
val signalRef = signal.name.ref
121+
val valueStr = signal.schema match {
122+
case _: Schema.Primitive[_] => value.toJson.replace("\"", "'")
123+
case _ =>
124+
val ast = value.toJsonAST.getOrElse(throw new RuntimeException("Failed to convert value to JSON AST"))
125+
SignalUpdate.astToExpression(ast)
126+
}
127+
js"$signalRef = $valueStr"
128+
case outermost :: innerPath =>
129+
// Nested signal: $outermost = {inner: {path: value}}
130+
val ast = value.toJsonAST.getOrElse(throw new RuntimeException("Failed to convert value to JSON AST"))
131+
val nestedObj = innerPath.foldRight(ast) { (key, acc) =>
106132
zio.json.ast.Json.Obj(key -> acc)
107133
}
108-
SignalUpdate.astToExpression(nested)
134+
val outermostRef = s"$$${signal.name.caseModifier.modify(outermost)}"
135+
val nestedExprStr = SignalUpdate.astToExpression(nestedObj)
136+
js"$outermostRef = $nestedExprStr"
137+
case Nil =>
138+
throw new RuntimeException("Signal path cannot be empty")
109139
}
110-
js"$update"
111140
}
112141
}
113142

114143
object SignalUpdate {
115-
private def astToExpression(ast: zio.json.ast.Json): String = ast match {
144+
private[signal] def astToExpression(ast: zio.json.ast.Json): String = ast match {
116145
case zio.json.ast.Json.Obj(fields) =>
117146
val fieldStrs = fields.map { case (k, v) => s"$k: ${astToExpression(v)}" }
118147
s"{${fieldStrs.mkString(", ")}}"
@@ -124,5 +153,10 @@ object SignalUpdate {
124153
case zio.json.ast.Json.Bool(value) => value.toString
125154
case zio.json.ast.Json.Null => "null"
126155
}
127-
implicit def signalUpdateToJs[A](update: SignalUpdate[A]): Js = update.toExpression
156+
157+
/**
158+
* Implicit conversion uses assignment expression format for use in event
159+
* handlers
160+
*/
161+
implicit def signalUpdateToJs[A](update: SignalUpdate[A]): Js = update.toAssignmentExpression
128162
}

0 commit comments

Comments
 (0)