A sensible AJAX approach

45 views
Skip to first unread message

Chris Lewis

unread,
Nov 19, 2009, 4:52:48 PM11/19/09
to lif...@googlegroups.com
Classic use case: a user chooses to view/edit and object by clicking on
a link. This causes the app to fetch an edit view (form) and render it
asynchronously, probably rendering it as a modal dialog. To specify the
case a bit more, consider a table of like objects that allows you to
edit them (orders or accounts). As far as the user experience, clicking
edit for one would yield the same edit form as any other - only the
contents (the target of the edit) would change.

Normal ajax forms in lift are simple - just wrap the bind in an ajax
form (http://is.gd/4Z61Z) and you get an async submit with essentially
the same template code.

But what about ajax forms delivered by ajax? What's the best way to
implement this in lift? It seems like there are two routes: client and
server-based.


1) In the client-based approach, I can declare an invisible form in the
template, as well as write some static javascript to do the heavy
lifting (no pun intended). This JS would be responsible for receiving
the data representing the object to edit from the server as json,
unpacking it into the form for editing, rendering the form, handling the
submit as ajax, and finally hiding the form.

This means writing a good bit more JS by hand, but it keeps the
(compiled) snippet code smaller. Ordinarily I'd see that as good, but
more and more snippets seem like they are intended for such heavy view
meddling.


2) The server-based approach would require very little of the main
template: basically just a containing element (w/ dom id) to host the
delivered form. The snippet itself would yield a form on an ajax call
via SetHtml. It would also have to set up the handlers and populate the
form contents with the target object.

This is the part that I'm not clear on. I know I can just inline the XML
with bind points as if it were in a form, but that just feels strange.
Would it make more sense to use an external template, similar to a rails
partial? Is there a template loading facility for this?

Thanks for any and all input.

chris

Jeppe Nejsum Madsen

unread,
Nov 19, 2009, 5:30:40 PM11/19/09
to lif...@googlegroups.com
Chris Lewis <burning...@gmail.com> writes:

> Classic use case: a user chooses to view/edit and object by clicking on
> a link. This causes the app to fetch an edit view (form) and render it
> asynchronously, probably rendering it as a modal dialog. To specify the
> case a bit more, consider a table of like objects that allows you to
> edit them (orders or accounts). As far as the user experience, clicking
> edit for one would yield the same edit form as any other - only the
> contents (the target of the edit) would change.

Seems you're a few steps ahead of me. I'll be looking at basically the same
use case soon :-)

> Normal ajax forms in lift are simple - just wrap the bind in an ajax
> form (http://is.gd/4Z61Z) and you get an async submit with essentially
> the same template code.
>
> But what about ajax forms delivered by ajax? What's the best way to
> implement this in lift? It seems like there are two routes: client and
> server-based.

Agreed

> 1) In the client-based approach, I can declare an invisible form in the
> template, as well as write some static javascript to do the heavy
> lifting (no pun intended). This JS would be responsible for receiving
> the data representing the object to edit from the server as json,
> unpacking it into the form for editing, rendering the form, handling the
> submit as ajax, and finally hiding the form.
>
> This means writing a good bit more JS by hand, but it keeps the
> (compiled) snippet code smaller. Ordinarily I'd see that as good, but
> more and more snippets seem like they are intended for such heavy view
> meddling.

Probably depends on the app. If it's mostly a single-url, js based app
this would work. Personally (and since I'm not a JS ninja :-) I try to
limit the amount of client side code to the where it makes sense. I
think the development experience (for me) is better with a statically
typed languagne (tdd etc).

> 2) The server-based approach would require very little of the main
> template: basically just a containing element (w/ dom id) to host the
> delivered form. The snippet itself would yield a form on an ajax call
> via SetHtml. It would also have to set up the handlers and populate the
> form contents with the target object.
>
> This is the part that I'm not clear on. I know I can just inline the XML
> with bind points as if it were in a form, but that just feels strange.
> Would it make more sense to use an external template, similar to a rails
> partial? Is there a template loading facility for this?

I've been doing this (external templates) and it works great. Very easy
for designers to modify layout etc.

Here's a simple example the changes several page elements when a select
is changed:

bind("select", in,
"type" -> ajaxSelectObj(reportTypes, currentType.is,
(f:ResultF) => {
currentResult(Empty)
currentType(Full(f))
val nodeseq = TemplateFinder.findAnyTemplate(List("tender", "results")).open_!
SetHtml("comparison", resultComparison(tender, chooseTemplate("lift", "tender.result_comparison", nodeseq))) &
SetHtml("summary", segmentSummary(tender, chooseTemplate("lift", "tender.segment_summary", nodeseq)))
})
)

Here the two elements are read from the same template file but it could
easily be split...

/Jeppe

Chris Lewis

unread,
Nov 19, 2009, 8:43:43 PM11/19/09
to lif...@googlegroups.com
Thanks for the feedback Jeppe. I can't completely infer the context of
your example, but I get the idea (and I hadn't known about
TemplateFinder). Here's what I've hacked together so far.

In my normal snippet, I have a render loop that binds orders to template
names. One of the bind points looks like this:

..
"label" -> SHtml.a(viewOrder _, Text("View")),
..

Here, viewOrder is a function called by ajax, so a () => JsCmd. I've
defined it as follows (assume i've imported TemplateFinder.findAnyTemplate):

def viewOrder(): JsCmd = {
SetHtml("order", SHtml.ajaxForm(
bind("order", findAnyTemplate(List("orders/_edit_order")).open_!,
"orderNo" -> Text(order.orderNo),
"submit" -> SHtml.submit("submit", () => println(" -- submit!"))
) ++ SHtml.hidden(() => println(" -- submit!"))
))
}

Using the usual ajaxForm technique, I wrap the result of a bind which
results in the contents passed to SetHtml. The funny part is that I
directly load a template file as a NodeSeq and use that as the arg to
bind. That template file doesn't call a snippet, but it uses the bind
points bound in the call - and it works! Here's the full _edit_order.html:

<div>
<order:orderNo/>
<order:submit/>
</div>

It's just raw xml. No snippet calls, yet bind still processes it
correctly - even the ajax wiring! I think this is all in all a pretty
good way to handle the common-edit-dialog problem, and certainly the
cleanest I've seen yet. I wonder though, is it intentional that bind
points work when not explicitly wrapped in a snippet call tag?

thanks!

chris
> --
>
> 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=.
>
>
>

Kris Nuttycombe

unread,
Nov 19, 2009, 11:15:02 PM11/19/09
to lif...@googlegroups.com
I use the second style, to great effect, in Lift - except I use
net.liftweb.http.TemplateFinder.findAnyTemplate to get the NodeSeq
representing the template instead of inlining the XML, and then use a
normal call to bind. It works a treat.

There are a couple of utility traits that I use to make this even
cleaner; I don't have time to detail how they're used at the moment
but maybe you can get a little bit of an idea by looking at the types.
One of these days I'll clean these up into something truly reusable
(these rely on the bindings stuff I detailed on my blog at
http://logji.blogspot.com/2009/09/composable-bindings-in-lift.html
with a couple of extra things, tbind and the Templated trait.

def tbind(x: Binding with Templated): NodeSeq = x.bind(x.template)

trait Templated {
def template: List[String]
}

trait Form[T] {
def onSubmit: Unit = process
def process: T
}

trait FormSnippets {
this: StatefulSnippet =>

var actionBinding: Binding with Templated = _
var editFormBinding: Binding with Templated = _

lazy val dispatch: DispatchIt = {
case "actionSelect" => actionBinding
case "editForm" => editFormBinding
}

def ajaxify(b: Binding with Templated with Form[_],
clientOnSubmit: JsCmd, serverRedirect: Option[String]) = new Binding
with Templated {
override val template = b.template
override def apply(xhtml: NodeSeq): NodeSeq = {
def serverOnSubmit: JsCmd = {
b.onSubmit

serverRedirect.map(where => RedirectTo(where)).
getOrElse(SetHtml("action_selector",
tbind(actionBinding)) & SetHtml("edit_form", tbind(editFormBinding)))
}

ajaxForm(b.apply(xhtml) ++ hidden(serverOnSubmit _), clientOnSubmit)
}
}
}

Kris

Jeppe Nejsum Madsen

unread,
Nov 20, 2009, 2:35:25 AM11/20/09
to lif...@googlegroups.com
Chris Lewis <burning...@gmail.com> writes:

> Thanks for the feedback Jeppe. I can't completely infer the context of
> your example,

Seems to happen a lot to me lately. :-) This was just a quick paste from
our codebase....

> but I get the idea (and I hadn't known about TemplateFinder). Here's
> what I've hacked together so far.


/Jeppe

glenn

unread,
Nov 20, 2009, 12:45:47 PM11/20/09
to Lift
I've been following this thread as lately I'm having difficulty with
an ajax-submitted
request in my app. Here's the scenario (There's no simple way to
describe this, so bear with me):

I create a JQuery tree of my data (Mapper object with just an item
name and a parent Mapper object as it's fields) as follows:

def buildTree(in:NodeSeq):NodeSeq = {

object MyJqLoad {
def apply(content: JsExp) = new JsExp with JQueryRight with
JQueryLeft {
def toJsCmd = JqId("json_result").toJsCmd + ".load(" +
content.toJsCmd + JsRaw("""+this.id""") + ")"
}
}

val host = "http://" + S.hostName
val link = host + ":8080" + S.contextPath + "/api/json/" +
dbTableName.toLowerCase + "/"

val func = AnonFunc( MyJqLoad(link))

TreeView("tree", JsObj(("persist", "location"), ("toggle", func)),
loadTree, loadNode)
}

The functions, loadTree and loadNode, are used to asynchronously
build the tree of item names from the Mapper instances.
The link val creates a URL, for example, http://localhost:8080/api/json/<dbTableName>/,
where dbTableName is just the Mapper object name.

On the client, the user clicks on a tree item, which, as you can see
from the above code, submits the Mapper id selected to the server,
which then
pulls the Mapper.toForm html from the Mapper instance and returns it
to the client at <div id="json_result"/>, which is in my template.
This all works great.

Where this all breaks down, is in submitting the returned
Mapper.toForm back to the server when I click on the "Save" button,
because no Mapper instance
exists at that point in the request or session for the save button to
act on.

Somewhere, I'm thinking I have to modify the Mapper.toForm html so the
submit button runs another JavaScript function on the client to return
the Mapper id
to the server for processing. But I haven't yet been able to do this.
One of the problems is that I don't even have an id on the client
(none is returned with Mapper.toForm
html) to work with. And, it seems to me that to make this scenario
work, I'm forced to become a JavaScript guru and work outside of Lift.
In other words, if there isn't
a mechanism in Lift to handle this situation directly, there should
be.

One of the things I've tried is to add a hidden form field to the
Mapper.toForm html,

SHtml.hidden(() => itemIdVar(id.toString)),

where itemIdVar extends RequestVar[String].

But I couldn't make that work either. The hidden form field is suppose
to run the ()=Any function, and it may do that, but not in the correct
sequence to do any good
when the form is submitted.

Glenn

On Nov 19, 11:35 pm, Jeppe Nejsum Madsen <je...@ingolfs.dk> wrote:

David Pollak

unread,
Nov 20, 2009, 1:05:42 PM11/20/09
to lif...@googlegroups.com
Glenn,

All functions are bound to the current state in the scope that the function was created.  There's no need to do weird RequestVar things with Hidden fields or anything else.  If you've got a function passed to SHtml.text, submit, etc. that function closes over its local scope: it captures all the variables it refers to.

So:

def mySnippet = {
  val myInstance = MyTable.find(whatever)

  SHtml.ajaxCheckbox(..., bool => {myInstance.boolField(bool).save; Noop})
}

In the above code, you've captured the instance in the val myInstance... that's kept around for the lifespan of the function that you passed to ajaxCheckbox.

Thanks,

David

--

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=.





--
Lift, the simply functional web framework http://liftweb.net
Beginning Scala http://www.apress.com/book/view/1430219890
Follow me: http://twitter.com/dpp
Surf the harmonics

glenn

unread,
Nov 20, 2009, 1:14:16 PM11/20/09
to Lift
David,

That's what I thought. All I needed to do was create the Mapper.toForm
like so:

item.toForm(Full("Save"), { _.save })

and the item would be saved on submit. But it doesn't work in my case.

Glenn


On Nov 20, 10:05 am, David Pollak <feeder.of.the.be...@gmail.com>
wrote:
> >  The link val creates a URL, for example,http://localhost:8080/api/json/
> > liftweb+u...@googlegroups.com<liftweb%2Bunsu...@googlegroups.com>
> > .
> > For more options, visit this group at
> >http://groups.google.com/group/liftweb?hl=.
>
> --
> Lift, the simply functional web frameworkhttp://liftweb.net
> Beginning Scalahttp://www.apress.com/book/view/1430219890

David Pollak

unread,
Nov 20, 2009, 1:28:36 PM11/20/09
to lif...@googlegroups.com
On Fri, Nov 20, 2009 at 10:14 AM, glenn <gl...@exmbly.com> wrote:
David,

That's what I thought. All I needed to do was create the Mapper.toForm
like so:

item.toForm(Full("Save"), { _.save })

and the item would be saved on submit. But it doesn't work in my case.

Please put together a complete runnable example of it not working for you and we'll debug it.
 
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=.





--
Lift, the simply functional web framework http://liftweb.net
Beginning Scala http://www.apress.com/book/view/1430219890

glenn

unread,
Nov 20, 2009, 8:40:03 PM11/20/09
to Lift
David,

I was able to put together a simplified application to demo the
problem.
It's just a basic lift archetype with a Role.scala added to the mapper
package that
contains all the relevant code.

Here is the link to the application:

http://github.com/glennSilverman/treeview

If you run it as-is, you can add new roles to the db but you can't
edit them.

Glenn

On Nov 20, 10:28 am, David Pollak <feeder.of.the.be...@gmail.com>
> > <liftweb%2Bunsu...@googlegroups.com<liftweb%252Buns...@googlegroups.com>

glenn

unread,
Nov 24, 2009, 11:02:52 AM11/24/09
to Lift
David,

The statement that all functions are bound to the current state in the
scope that the function
was created may be true for functions submitted via SHtml, but doesn't
seem to hold true
for those from LiftResponse, otherwise, the edit function in my
example would work for editing,
just as it does for creating.

In any case, I'm still at a loss on how to fix this.

Glenn

David Pollak

unread,
Nov 24, 2009, 8:53:18 PM11/24/09
to lif...@googlegroups.com
Glenn,

The issue is that you were creating call-back functions during an API call (rather than as part of Lift's HTML/Ajax/Comet pipeline).

In general, I think this is very bad design.  I'd strongly recommend using the Ajax/Json call facilities built into Lift.  You get a pile of things for free including securing calls (you don't need to keep a lot of state client-side).

So, your code looks like:


  def buildTree(in:NodeSeq):NodeSeq = {   
    val func = AnonFunc( SHtml.ajaxCall(JsRaw("this.id"), (id: String) =>
        SetHtml("role_edit", Role.find(id).map(edit) openOr NodeSeq.Empty))._2 )

      
    TreeView("tree", JsObj(("persist", "location"), ("toggle",  func)), loadTree, loadNode)
  }

Also, you might want to look at S.hostAndPort for calculating the current server.  Also, think about using the map() call rather than pattern matching on Full/_.

Also, there's no need to register API calls in SiteMap

I'll update your app and push code to http://github.com/dpp/treeview

Thanks,

David


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.





--
Lift, the simply functional web framework http://liftweb.net
Beginning Scala http://www.apress.com/book/view/1430219890

glenn

unread,
Nov 30, 2009, 3:15:27 PM11/30/09
to Lift
David,

Thanks a bunch. Your solution is one I was trying to implement but
wasn't quite
sure how until you showed me that I can create an AnonFunc using
Shtml.ajaxCall.
It certainly does simplify things.

One of the issues that stymied me - and still does, frankly - is how
the ajaxCall wraps the edit function
with the correct parameter for the callback to work.

I updated the code in http://github.com/glennSilverman/treeview and
refactored it into
a MetaMegaTreeItem trait to easily add these features to any Mapper
object. Maybe
some Lift committer might find the ideas involved here usefull enough
to include something
similar in a future release of Lift.

Glenn
.


On Nov 24, 5:53 pm, David Pollak <feeder.of.the.be...@gmail.com>
wrote:
> Glenn,
>
> The issue is that you were creating call-back functions during an API call
> (rather than as part of Lift's HTML/Ajax/Comet pipeline).
>
> In general, I think this is very bad design.  I'd strongly recommend using
> the Ajax/Json call facilities built into Lift.  You get a pile of things for
> free including securing calls (you don't need to keep a lot of state
> client-side).
>
> So, your code looks like:
>
>   def buildTree(in:NodeSeq):NodeSeq = {
>     val func = AnonFunc( SHtml.ajaxCall(JsRaw("this.id"), (id: String) =>
>         SetHtml("role_edit", Role.find(id).map(edit) openOr
> NodeSeq.Empty))._2 )
>
>     TreeView("tree", JsObj(("persist", "location"), ("toggle",  func)),
> loadTree, loadNode)
>   }
>
> Also, you might want to look at S.hostAndPort for calculating the current
> server.  Also, think about using the map() call rather than pattern matching
> on Full/_.
>
> Also, there's no need to register API calls in SiteMap
>
> I'll update your app and push code tohttp://github.com/dpp/treeview
> > > > > <liftweb%2Bunsu...@googlegroups.com<liftweb%252Buns...@googlegroups.com>
> > <liftweb%252Buns...@googlegroups.com<liftweb%25252Bun...@googlegroups.com>
> >http://groups.google.com/group/liftweb?hl=en.
>
> --
> Lift, the simply functional web frameworkhttp://liftweb.net
> Beginning Scala ...
>
> read more »
Reply all
Reply to author
Forward
0 new messages