I'm in the process of trying to create my own custom form elements
that combine other, existing types of elements. Here's the element
that's currently giving me fits:
def optionalDateField(value: DateTime, func: Option[DateTime] =>
Any, attrs: (String, String)*): ChoiceHolder[NodeSeq] = {
var useDate = false
var curDate: Option[DateTime] = None
fmapFunc(SFuncHolder(s => {Log.debug("Got " + s + " for
optional field"); if (s == "Some") func(curDate)})) {
name => {
val noneItem = ChoiceItem[NodeSeq](Text("None"),
<input type="radio" name={name} value="None" checked="true"/>)
val someItem = ChoiceItem[NodeSeq](dateField(value, d
=> {Log.debug("Got " + d + " as submitted date."); curDate =
Some(d)}), <input type="radio" name={name} value="Some"/>)
ChoiceHolder(noneItem :: someItem :: Nil)
}
}
}
dateField is implemented as:
def dateField(value: DateTime, func: DateTime => Any, attrs:
(String, String)*): Elem = {
var curDate = value.withMillisOfDay(0)
val daySelectId = randomString(8)
def dayOptions = (1 to
curDate.property(dayOfMonth).getMaximumValue).map(i => (i.toString,
i.toString)).toList
def daySelect = untrustedSelect(
dayOptions,
Full(curDate.getDayOfMonth.toString),
d => curDate = curDate.withDayOfMonth(d.toInt),
"id" -> daySelectId
)
def monthSelect = ajaxSelect(
(1 to 12) map (i => (i.toString, i.toString)),
Full(curDate.getMonthOfYear.toString),
{m =>
curDate = curDate.withMonthOfYear(m.toInt)
ReplaceOptions(daySelectId, dayOptions,
Full(curDate.getDayOfMonth.toString))
}
)
def yearSelect = ajaxSelect(
(curDate.getYear to curDate.getYear + 10) map (y =>
(y.toString, y.toString)),
Full(curDate.getYear.toString),
{y =>
curDate = curDate.withYear(y.toInt)
ReplaceOptions(daySelectId, dayOptions,
Full(curDate.getDayOfMonth.toString))
}
)
def hourSelect = select(
(0 to 23) map (h => (h.toString, h.toString)),
Full(curDate.getHourOfDay.toString),
h => curDate = curDate.withHourOfDay(h.toInt)
)
def minuteSelect = select(
(0 to 59) map (h => (h.toString, h.toString)),
Full(curDate.getMinuteOfHour.toString),
m => curDate = curDate.withMinuteOfHour(m.toInt)
)
def hiddenField = hidden(() => func(curDate))
attrs.foldLeft(
<span>
{yearSelect} / {monthSelect} / {daySelect}
{hourSelect}:{minuteSelect}
{hiddenField}
</span>
)(_ % _)
}
The problem that I'm running into is that because of having to collect
the state of various form elements in vars, I have no means to control
the order of operations. I'm attempting to use my optionalDateField in
the following snippet:
def addHold(xhtml: NodeSeq): NodeSeq = {
var start: Option[DateTime] = None
var end: Option[DateTime] = None
def onSubmit = for (d <- start; sub <- subscription) {
Log.debug("New hold submitted; end is " + end)
sub.addHold {
end match {
case Some(e) => SubscriptionHold.finite(sub, d, e,
Initiator.ADMINISTRATIVE)
case None => SubscriptionHold.infinite(sub, d,
Initiator.ADMINISTRATIVE)
}
}
sub.merge
EM.tx.commit
}
if (subscription.exists(_.isCurrentAsOf(new DateTime()))) {
bind("add_hold", xhtml,
"start_date" -> dateField(new DateTime, d => start = Some(d)),
"end_date" -> <span>{optionalDateField(new DateTime,
d => {Log.debug("Capturing end as " + d); end = d}).flatMap(i =>
i.xhtml ++ i.key)}</span>,
"submit" -> submit("Add Hold", onSubmit _)
)
} else {
NodeSeq.Empty
}
}
Here's a log trace showing the problem:
2010-01-25 17:26:58,502 DEBUG lift - Got Some for optional field
2010-01-25 17:26:58,505 DEBUG lift - Capturing end as None
2010-01-25 17:26:58,511 DEBUG lift - Got 2010-01-30T00:00:00.000Z as
submitted date.
How can I force the order of evaluation of the form elements such that
the submitted date variable will be set *before* the function passed
to fmapFunc is evaluated? This is the problem that spawned my "A
better approach to AJAX forms" thread. I've been beating my head
against this for days now, and I can't help but place the blame
squarely on the "pass a function that modifies a local var" pattern
that I complained about in the other thread. Does a better solution
exist? It just seems way too easy to shoot oneself in the foot with
this cruddy pattern.
Thanks,
Kris
def optionalDateField(value: DateTime, func: Option[DateTime] =>
Any, attrs: (String, String)*): ChoiceHolder[NodeSeq] = {
var useDate = false
var curDate: Option[DateTime] = None
val df = dateField(value, d => {Log.debug("Got " + d + " as
submitted date."); curDate = Some(d)})
fmapFunc(SFuncHolder(s => {Log.debug("Got " + s + " for
optional field"); if (s == "Some") func(curDate)})) {
name => {
val noneItem = ChoiceItem[NodeSeq](Text("None"),
<input type="radio" name={name} value="None" checked="true"/>)
val someItem = ChoiceItem[NodeSeq](df, <input
type="radio" name={name} value="Some"/>)
ChoiceHolder(noneItem :: someItem :: Nil)
}
}
}
This causes the closures to be evaluated in the right order. So much
for idempotence.
Frustrated,
Kris
I'm a bit tired to follow code from this page so could you please put
together a minimalistic application that I could just try?
Br's,
Marius
Thanks, Marius, but I'm too time-crunched at the moment to boil this
down. In any case, I found a solution. My frustration is primarily
that ordering of function creation matters in the first place; making
the ordering of function creation less relevant is the point of my
proposal in the other thread. If the order that you do things in
matters, it completely hoses you for the purposes of composition (as I
painfully found out with this example.) Here, the composition is just
two calls deep and explicit, and even with only that small amount of
composition it was a pain to track down.
Does this make my proposal in the other thread make any more sense to you?
Kris
On Jan 25, 8:19 pm, Kris Nuttycombe <kris.nuttyco...@gmail.com> wrote:
> On Mon, Jan 25, 2010 at 11:07 AM, Marius <marius.dan...@gmail.com> wrote:
> > S.formFuncName .. should guarantee proper ordering of functions
> > application respecting the ordering of functions creation (and this is
> > used by fmapFunc).
>
> > I'm a bit tired to follow code from this page so could you please put
> > together a minimalistic application that I could just try?
>
> > Br's,
> > Marius
>
> Thanks, Marius, but I'm too time-crunched at the moment to boil this
> down. In any case, I found a solution. My frustration is primarily
> that ordering of function creation matters in the first place; making
> the ordering of function creation less relevant is the point of my
> proposal in the other thread. If the order that you do things in
> matters, it completely hoses you for the purposes of composition (as I
> painfully found out with this example.) Here, the composition is just
> two calls deep and explicit, and even with only that small amount of
> composition it was a pain to track down.
As I didn't look at your code in details I fail to see where ordering
hurts composition.
functions ordering matters because when submitting a form you'd want
functions to be called in the order of the fields "creation" ... not
randomly ... as HTTP parameters order is not guaranteed.
Perhaps we should add a generic mechanism to specify custom field
functions application order so that:
Assume having a a bind like:
bind("f", xhtml,
"field1" -> formOrder(0, SHtml.text("", f1)),
"field2 -> formOrder(2, "SHtml.text("", f2)),
"field3" -> formOrder(1, SHtml.text("", f3))
)
the functions evaluation on server side would be: f1, f3, f2.
Thoughts?
Kris
--
You received this message because you are subscribed to the Google Groups "Lift" group.
To post to this group, send email to lif...@googlegroups.com.
To unsubscribe from this group, send email to liftweb+u...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/liftweb?hl=en.
The salient difference between what works and what doesn't from the
code I posted earlier is here (edited for readability):
http://paste.pocoo.org/show/169908/
My thought was that instead of explicitly specifying form element
closure evaluation order, you could instead have a FormElement[T]
class that would return a T on a method call.
Thus, it would be the user's code that would trigger the execution of
the closures, not the framework. The framework could do it in the
background to maintain backwards compatibilty, with lazy
initialization semantics for the stored value so that if the user's
code executed first, the closure would be executed and the result
stored (so that the framework wouldn't re-execute it.
Kris
>
>>
>> Does this make my proposal in the other thread make any more sense to you?
>>
>> Kris
>
They have to be executed in some order; I just wish that the execution
could actually be performed by user code! That's the whole point of my
suggestion from the other thread.
> More broadly, I think you might want to look at what I did with Screen and
> Wizard. These are declarative mechanisms for defining forms, validations,
> and behaviors. Where are these not working for you?
I simply haven't had the time to port thousands of lines of form code
I've already written over to stuff on SNAPSHOT. I was supposed to have
this stuff released last Friday; my intention was to release on M8
since I haven't had time to test SNAPSHOT adequately because I've been
hunting down order of evaluation bugs.
Kris
More to the point, I think that with a slight modification in design,
I wouldn't *have* to think about order of evaluation. The fact that
it's exposed (and has been, for me, an infuriating source of bugs) is
a design smell.
Kris
Kris
--
You received this message because you are subscribed to the Google Groups "Lift" group.
To post to this group, send email to lif...@googlegroups.com.
To unsubscribe from this group, send email to liftweb+u...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/liftweb?hl=en.
I think what I'm trying to accomplish is at a somewhat lower level
than what Screen/Wizard is targeted at. The paste at
http://paste.pocoo.org/show/169908/ is an example of what I'm trying
to do, where order of operations has fouled me up. In the "before"
example, the function passed in gets evaluated with the correct value
from the radio button, but since the dateField was inline, it has not
yet been evaluated at the time of processing of the form that
contained it.
It just seems really bad to me that inlining something has different
behavior than does assigning it to a val and then using the val. This
is totally nonobvious to me, at least.
The essence of my suggestion from the other thread is this; get rid of
the var in the following snippet:
def mySnippet(xhtml: NodeSeq) {
var a: Option[String] = None
bind("a", xhtml,
"value" -> text("". s => a = Some(s)),
"submit" -> submit("Submit", () => a.foreach(doSomething _)
)
}
With my proposal, this would become:
def mySnippet(xhtml: NodeSeq) {
val aField = text("s", s => s)
bind("a", xhtml,
"value" -> aField,
"submit" -> submit("Submit", () => doSomething(aField!))
)
}
If you eliminate the var, you eliminate any questions with respect to
order of operations. I think that this is valuable with or without
your Screen/Wizard changes, unless the intent is to get rid of the
SHtml.* methods for form fields entirely.
Kris
That looks good; I think it'd go a long way toward making the order of
operations a little more foolproof. The idea of 'Elem with
AnswerHolder' had never occurred to me; I guess I just always assumed
that Elem or Node was sealed, but it doesn't appear to be from the
scaladocs. That would certainly help on the backwards compatibility
front and would alleviate the need for the implicit conversion I'd
imagined from FormField[T] to NodeSeq.
What do you think about the notion of a "!" method that does the
unsafe answer.open_! ? I would think that in the vast majority of
cases, the Box being Empty would represent either a coding error or a
framework error. In the case of a coding error (the answer attempting
to be extracted during the initial rendering of the form elements)
this would fail fast, and in the case where the response is actually
being processed it should probably never be empty anyway.
Kris
On Mon, Feb 15, 2010 at 4:31 PM, David Pollak
<feeder.of...@gmail.com> wrote:
> If all the SHtml stuff returned a NodeSeq (or Elem) with AnswerHolder andThat looks good; I think it'd go a long way toward making the order of
> AnswerHolder[T] looked like:
>
> trait AnswerHolder[T] {
> def hasAnswer: Boolean
> def answer: Box[T]
> def map[S](f: T => S): Box[S]
> ...
> }
>
> Then we could get what you want (no explicit mutability) and keep APIs
> working the way they work now. What do you think?
>
> We could even introduce alternative SHtml stuff like:
>
> def text(original: String): Elem with AnswerHolder[String]
>
> What do you think?
operations a little more foolproof. The idea of 'Elem with
AnswerHolder' had never occurred to me; I guess I just always assumed
that Elem or Node was sealed, but it doesn't appear to be from the
scaladocs. That would certainly help on the backwards compatibility
front and would alleviate the need for the implicit conversion I'd
imagined from FormField[T] to NodeSeq.
What do you think about the notion of a "!" method that does the
unsafe answer.open_! ?
I would think that in the vast majority of
cases, the Box being Empty would represent either a coding error or a
framework error. In the case of a coding error (the answer attempting
to be extracted during the initial rendering of the form elements)
this would fail fast, and in the case where the response is actually
being processed it should probably never be empty anyway.