Evaluation of checkBoxesFieldList in Yesod - forms

I'm struggling with the evaluation of checkBoxesFieldList of monadic forms in my Yesod appliction.
I'm using these to create n-questions where n is the number of checkBoxesFieldList, with 4 answers (only True/False) each. But in comparison to the applicative forms, these already "evaluate" the result and tell me if I answered correctly.
The problem occurs if there are boxes unchecked, or you check a wrong one out of four, so I can't know what wrong answer you checked.
Example:
If I checked only the first box, the result is [False].
If I checked only the second box wrong, the result is still [False].
If I checked only the third box (but it was the right answer), the result is [True].
What I want to achieve: The result from runFormPost should give me a [Bool] based on: checked -> return True, unchecked -> return False.
Example:
If I checked only the first box wrong, the result should be [True, False, False, False]
I already had to customize the checkboxesField from the library, but I just can't find a way remove the evaluation.
Here is the code:
data Question = Question {
, identity :: Text
, content :: Text
, answerList :: [Answer]
, maxScore :: Int
}
data Answer = Answer {
, identity :: Text
, content :: Text
, isCorrect :: Bool
, hint :: Text
}
zipAnswers :: [Answer] -> [(Text, Bool)]
zipAnswers [] = []
zipAnswers ((Answer _ text val _):xs) = (text, val):zipAnswers xs
listEditMForm :: [Question] -> Html -> MForm Handler (FormResult ([FormResult (Maybe [Bool])]), Widget)
listEditMForm xs token = do
check_fields <- forM xs (\(Question _ content alist _ ) -> mopt (checkboxesFieldList' $ zipAnswers alist) (fromString $ T.unpack content) Nothing)
let (check_results, check_views) = unzip check_fields
let numerated_views = zip ([1..]::[Int]) check_views
let widget = [whamlet|
^{token}
<ul class="tabs">
$forall (c,view) <- numerated_views
<li>
<input type="radio" name="tabs" id="tab#{fvId view}">
<label for="tab#{fvId view}">Q #{show c}
<div id="tab-content#{fvId view}" class="tab-content animated fadeIn">
<p class=boldWhite> #{fvLabel view}: </p>
^{fvInput view}
<br>
<input class=button type=submit value="Testing">
|]
return ((FormSuccess check_results), widget)
checkboxesFieldList' :: (Eq a, RenderMessage site FormMessage, RenderMessage site msg) => [(msg, a)]
-> Field (HandlerT site IO) [a]
checkboxesFieldList' = checkboxesField' . optionsPairs
checkboxesField' :: (Eq a, RenderMessage site FormMessage)
=> HandlerT site IO (OptionList a)
-> Field (HandlerT site IO) [a]
checkboxesField' ioptlist = (multiSelectField ioptlist)
{ fieldView =
\theId name attrs val _ -> do
opts <- fmap olOptions $ handlerToWidget ioptlist
let optselected (Left _) _ = False
optselected (Right vals) opt = (optionInternalValue opt) `elem` vals
[whamlet|
<span ##{theId}>
$forall opt <- opts
<label>
<input type=checkbox name=#{name} value=#{optionExternalValue opt} *{attrs} :optselected val opt:checked>
<span class=simpleWhite> #{optionDisplay opt}
|]
}

Related

How do I conditionally render more than one item in a repeat

I would like to produce a repeating set of nodes using a conditional showIf for one of the nodes something like the following:
div<id = "parent">
div<id = "child1">Child 1</div>
div<id = "child2">Child 2</div>
div<>Optional text for child 2</div>
</div>
To produce this I might use the repeat function something like the following:
div(id := "parent",
repeat(seqProp)(child =>
div(id := child.get.id),
showIf(child.transform(_.otionalText.nonEmpty))(div(child.optionalText.get))
)
)
But no matter what way I seem to try to write this I cannot get the above code to compile. Can someone recommend me a good way do do this?
NOTE. If I have a Seq[Frag] then I can call render on that sequence. But showIf produces a Binding which seems to have an implicit conversion to a Modifier but not to a Frag.
This is a tricky one due to the requirement for bindings to render DOM nodes, not Modifiers (so they can be replaced accordingly on any change).
First of all, repeat tracks only structure changes, so you need to combine 2 bindings. To avoid leaks, you can use repeatWithNested in that case.
Secondly, scalatags.generic.LowPriUtil#OptionFrag allows you to render Option nodes, so you don't need to worry about showIf here.
Taking that into account, assuming you have some model class and sequence:
case class C(id: String, optionalText: Option[String])
val seqProp = SeqProperty(C("a", Some("")))
You could write:
div(
id := "parent",
repeatWithNested(seqProp)((childProp, nested) => div(
nested(produce(childProp)(child => Seq(
div(id := child.id)(child.id).render,
child.optionalText.render,
)))
).render)
)
This unfortunately produces an additional nested div, but reacts to both structure and value patches correctly.
You can see this code in action here: https://scalafiddle.io/sf/BHG388f/0
If you really wanted to avoid that, you'd have to sacrifice some of these properties e.g. by using produce on the seqProp and creating the parent as its root node inside the builder.
I will elaborate a bit on my scenario to explain better the context. I have the following classes:
trait MenuItem {
val id: String
val label: String
val subMenu: Option[() => Future[Seq[MenuItem]]]
}
case class MenuNode(item: MenuItem, level: Int, subNodes: Seq[MenuNode])
Menu nodes are organised in a tree, with level starting at zero for the root node and incrementing as we go down the tree. I want to be able to dynamically expand/collapse a node by clicking on it. But the DOM will not match up to this hierarchy - it will be flat. So say for example I want to create a 3 level menu of recipes, the DOM would be something like the following:
<div class="list-group">
<button class="list-group-item menu-item menu-level-1">Vegetables</button>
<button class="list-group-item menu-item menu-level-2">Carrot</button>
<button class="list-group-item menu-item action-item">Soup</button>
<button class="list-group-item menu-item action-item">Coleslaw</button>
<button class="list-group-item menu-item menu-level-2">Potatoes</button>
<button class="list-group-item menu-item menu-level-1">Fruits</button>
<button class="list-group-item menu-item menu-level-2">Apple</button>
<button class="list-group-item menu-item action-item">Tart</button>
<button class="list-group-item menu-item action-item">Cider</button>
<button class="list-group-item menu-item menu-level-2">Orange</button>
</div>
I originally approached this trying to write a recursive function to go through the tree producing the DOM as I recurse. But I've taken a step back and realised a better approach would be to flatten the tree (recursively) to produce all relevant MenuNodes in a sequence. I could then use a SeqProperty to manage how my tree is displayed. Then when a node is expanded/collapsed I only have to update the relevant parts of the SeqProperty accordingly. So I added the following definitions to MenuNode:
def flatten(): Seq[MenuNode] = flatten(subNodes.toList, Seq())
private def flatten(nodes: List[MenuNode], slots: Seq[MenuNode]): Seq[MenuNode] = nodes match {
case h :: t =>
// Add this node and any sub-slots after it
flatten(t, (slots :+ h) ++ h.flatten())
case _ =>
slots
}
def isSlot(node: MenuNode) = level == node.level && item.id == node.item.id
And here is my finalised MenuView:
class MenuView(model: ModelProperty[MenuModel]) extends View with Futures {
val seqProp = SeqProperty(model.get.rootNode.flatten())
def getTemplate: Modifier = {
div(cls := "list-group",
repeat(seqProp) { slot =>
button(cls := "list-group-item " + itemStyle(slot.get),
onclick := { () => handleClick(slot) },
slot.get.item.label
).render
}
)
}
model.subProp(_.rootNode).listen { node =>
// Find the first difference between previous and current
val prevSlots = seqProp.get
val curSlots = node.flatten()
prevSlots.indexWhere(curSlots)(! _.isSlot(_)) match {
case i if i > 0 =>
// Replace the slot that was toggled
seqProp.replace(i - 1, 1, curSlots(i - 1))
(curSlots.size - prevSlots.size) match {
case diff if diff > 0 =>
// Expand. Insert the new ones
seqProp.insert(i, curSlots.drop(i).take(diff): _*)
case diff =>
// Collapse. Remove the difference
seqProp.remove(i, -diff)
}
case _ =>
seqProp.set(curSlots)
}
}
def itemStyle(node: MenuNode) = "menu-item " +
(if (node.hasSubMenu) s"menu-level-${node.level}"
else "action-item") + (if (node.isActive) " item-active" else "")
def handleClick(node: Property[MenuNode]): Unit =
if (node.get.hasSubMenu) {
if (! node.get.isExpanded) node.get.expand().success { expanded =>
model.subProp(_.rootNode).set(model.get.rootNode.replace(expanded))
}
else {
model.subProp(_.rootNode).set(model.get.rootNode.replace(node.get.collapse()))
}
}
else {
val vector = node.get.vector
model.set(model.get.copy(
rootNode = model.get.rootNode.activate(vector),
activated = vector
))
}
}

bs-webapi - How to loop over Dom.nodeList?

The following won't work since sides is a Dom.nodeList and DomTokenList.forEach expects a Dom.domTokenList.
open Bs_webapi.Dom;
external length : Dom.nodeList => int = "" [##bs.get];
let sides = Document.querySelectorAll "#carousel > figure" document;
DomTokenList.forEach (fun item _ => print_endline item) (sides);
Paraphrased from the Reason Discord, courtesy of #anmonteiro:
Js.Array.forEach Js.log (NodeList.toArray sides);
Here is an example of how to setAttribute for each element in a NodeList. Note, Element.ofNode can be used to convert a Dom.node to option Dom.element.
open Bs_webapi.Dom;
external length : Dom.nodeList => int = "" [##bs.get];
let sides = Document.querySelectorAll "#carousel > figure" document;
Js.Array.forEachi
(fun side index =>
switch (Element.ofNode side) {
| Some element =>
Element.setAttribute "style" "some style here" element
| None => ()
}
)
(NodeList.toArray sides)
https://bucklescript.github.io/bucklescript/api/Js_array.html#VALforEach

Lift (Scala) form validation

I have a question for which I couldn't find an answer anywhere in the web. I have a lift-based web application written on Scala. There is a form in my application which is driven by a snippet. I use Mapper for my models, and I use override def validations ... to define model's validation parameters. The template code looks like this:
<form class="lift:GroupManager.addGroup?form=POST&action=add">
<div class="row collapse" style="width: 30em">
<div class="small-8 columns">
<span class="group-name prefix radius"></span>
</div>
<div class="small-4 columns">
<span class="submit button small postfix radius">Add New Group</span>
</div>
</div>
</span>
</form>
The snippet code:
def addGroup = {
object groupName extends RequestVar[String]("")
def doAddGroup() = {
val group = Group.create.createdAt(new java.util.Date).groupName(groupName.is)
group.validate match {
case Nil => {
group.save
S.notice("Group '" + group.groupName.is + "' has been added successfully")
}
case errors:List[FieldError] => {
S.error(errors)
}
}
}
".group-name" #> SHtml.text(groupName.is, groupName(_), "id" -> "group-name") &
".submit" #> SHtml.submit("Add group", doAddGroup)
}
The model's validations override:
object groupName extends MappedString(this, 700) {
override def dbColumnName = "group_name"
override def dbNotNull_? = true
override def validations = valMinLen(1, S.?("Group name should not be empty!")) _ ::
valMaxLen(700, S.?("Group name should not be more than 700 characters long!")) _ ::
valUnique(S.?("Group name should be unique!")) _ ::
super.validations
}
Now everything is perfect except the fact that when invalid data is provided, S.error is used to inform user about problems. What I want to do is to highlight the form element (by applying class="error") in which the data is invalid and add a <span> element with the error message in front of the form element. Is there a simple way to do that with Lift framework? Or I should just traverse through errors:List[FieldError] and fetch field information from that array, which does not seem too elegant to me?

Dynamically add textareas client-side to a form in lift

I've got a form based on the sample in http://simply.liftweb.net/index-4.2.html#toc-Section-4.2 and I was wondering if there's a way of having a button on the page that would add a textarea each time it's clicked, and then in the lift code get that as an array of strings.
What I'm picturing is something like this:
<form class="lift:OnSubmit?form=post">
Name: <input name="name"><br>
Age: <input name="age" value="0"><br>
<span id="somecomments"></span>
<input type="button" onclick="$('#somecomments').append($('<textarea cols=80 rows=10 name=comments>'))" value="Add Comment"/>
<input type="submit" value="Submit">
</form>
//in scala:
object OnSubmit {
def render = {
var name = ""
var age = 0
var comments = List("")​
def process() {
S.notice("Name: "+name)
S.notice("Age: "+age)
S.notice(comments)
S.redirectTo("/")
}
}​
"name=name" #> SHtml.onSubmit(name = _) &
"name=age" #> SHtml.onSubmit(s => asInt(s).foreach(age = _)) &
"name=comments" #> SHtml.onSubmit(comments = _) &
"type=submit" #> SHtml.onSubmitUnit(process)
}
}
But I get the compile error that the comments field is a String so I can't assign it to a List with "name=comments" #> SHtml.onSubmit(comments = _)
What's the best way to make this code work?
for prepending:
"name=comments" #> SHtml.onSubmit(comments ::= _)
for appending:
"name=comments" #> SHtml.onSubmit(comments :+= _)
Here I describe how you can add any number of fields (I have a textarea and a "related" numeric field.
You add them using jQuery and then Lift gets all the data as a json object.
Dynamically adding fields to a Lift application

Lift Scala - Redirect not working for Two Phased Form

I have a two stage form which im trying to implement in Lift Scala. I have the first stage of the form working but the second stage is not working as expected.
The first form asks the user for some input, on submission this will take the user to the second form with the details from the first form shown. The user can enter some text in the textfield and then click submit to view the form results. The second form should be made available to any other users so that they can enter a description for the title given by the initial user.
The way it should work is as follows - user 1 adds a title (form phase one), another user can enter a description (form phase two) and then submit the form to view the results. However, when a user enters the description on form phase two and clicks submit - nothing happens. No redirect and no error.
Any help on this is much appreciated.
My code is below, is this the correct approach?
class TwoPhaseForm extends DispatchSnippet with Logger {
def dispatch : DispatchIt = {
case "addformphaseone" => addformphaseone _
case "viewformphaseone" => viewformphaseone _
case "addformphasetwo" => addformphasetwo _
case "viewresults" => viewresults _
}
object currentAccountVar extends RequestVar[Entry]({
Entry.create.author(User.currentUser.open_!)
})
def currentAccount = currentAccountVar.is
//The first part of the form
def addformphaseone (xhtml : NodeSeq) : NodeSeq = {
def doSave () = {
currentAccount.validate match {
case Nil =>
currentAccount.save
S.redirectTo("/viewformphaseone?id=" + currentAccount.id)
case x => S.error(x)
}
}
val acct = currentAccount
bind("entry", xhtml,
"id" -> SHtml.hidden(() => currentAccountVar(acct)),
"title" -> SHtml.text(currentAccount.title.is, currentAccount.title(_)),
"submit" -> SHtml.submit("Submit", doSave))
}
//view the details from form phase one
def viewformphaseone(xhtml : NodeSeq) : NodeSeq = {
val t = Entry.find(S.param("id"))
t.map(t =>
bind("entry", xhtml,
"title" -> t.title.toString,
)) openOr <span>Not found!</span> <b>Not found!</b>
}
//Second phase of the form
def addformphasetwo (xhtml : NodeSeq) : NodeSeq = {
def doSave () = {
currentAccount.validate match {
case Nil =>
currentAccount.save
S.redirectTo("/results")
case x => S.error(x)
}
}
val t = Entry.find(S.param("id"))
t.map(t =>
bind("entry", xhtml,
"desc" -> SHtml.text(currentAccount.desc.is, currentAccount.desc(_)),
"submit" -> SHtml.submit("Submit", doSave)
)) openOr <span>Not found!</span> <b>Not found!</b>
}
//view the results from both forms
def viewresults(xhtml : NodeSeq) : NodeSeq = {
val t = Entry.find(S.param("id"))
t.map(t =>
bind("entry", xhtml,
"title" -> t.title.toString,
"desc" -> t.desc.toString,
)) openOr <span>Not found!</span> <b>Not found!</b>
}
}
a typical school boy error. from the "Exploring Lift book":
If the form attribute is included with a value of either “POST” or “GET”, then an appropriate form tag will be emitted into the XHTML using the specified submission method. If you omit this tag from a snippet that generates a form, the form elements will display but the form won’t submit.
I had missed the form="POST" tag in my html file. Hence the form was not being submited