Question

What would be a clean way to implement a simple CRUD interface in Lift and make it

  1. Designer Friendly
  2. Ajax

Lets suppose we have a view

<table data-lift="CrudList">
    <tr>
         <td role="data">Item goes here</td>
         <td><button role="remove" type="button">remove</button></td>
    </tr>
    <tr class="clearable">
         <td>Item two here</td>
         <td><button type="button">remove</button></td>
    </tr>
    <tr class="clearable">
         <td>Item three!</td>
         <td><button type="button">remove</button></td>
    </tr>
</table>
<form data-lift="form.ajax">
    <div data-lift="CrudList.create">
        <input type="text" name="text"></input>
        <button type="submit"></button>
    </div>
</form>

And a snippet

object CrudList {
  def render = {
    def remove(item: String) = () => {
      ListDAO.remove(item)
      JE.JsRaw("""Some JavaScript to remove <tr> from the UI""")
    }

    ClearClearable &
    "tr *" #> ListDAO.all.map(item => {
      "role=data" #> item &
      "role=remove" #> ajaxInvoke(remove(item))
    })
  }

  def create = {
    var text = ""

    def process(): JsCmd = {
      val item = ListDAO.create(text)
      JsCmds.Noop // TODO: replace this with some JsCmd 
                  // that will create and populate new table row in the UI
                  // without polluting the snippet with markup
    }

    "@text" #> SHtml.text(text, s => text = s) &
    "button *+" #> SHtml.hidden(process)
  }
}

Example might have bugs, purely for demonstration.

The render snippet is straightforward - we modify to existing markup and render our list as table rows.

I'm a bit hesitant to complete the create snippet. The code that persists the list item is straightforward, but I don't know how to approach the part that updates the <table> with new <tr>. I'd like to avoid polluting the snippet with markup leaving room for the designer to do with the table what they want. How would you complete this snippet?

Was it helpful?

Solution

The easiest way to do this would be to just swap out the entire HTML table. To do that, you can use a built in function in SHtml that will memoize the initial transformation.

To do that, we'd give table an ID like:

<table data-lift="CrudList" id="mytable">

Then in your snippet, you could do:

object CrudList {
  object tableMemo extends RequestVar[Box[IdMemoizeTransform]](Empty)

  def render = {
    def remove(item: String) = () => {
      ListDAO.remove(item)
      tableMemo.get.foreach{ _.setHtml }
    }

    "#mytable" #> SHtml.idMemoize{ memo =>
      tableMemo(memo)
      ClearClearable &
      "tr *" #> ListDAO.all.map(item => {
        "role=data" #> item &
        "role=remove" #> ajaxInvoke(remove(item))
      })
    }
  }

  def create = {
    var text = ""

    def process(): JsCmd = {
      val item = ListDAO.create(text)
      tableMemo.get.foreach{ _.setHtml }
    }

    "@text" #> SHtml.text(text, s => text = s) &
    "button *+" #> SHtml.hidden(process)
  }
}

Any call to tableMemo.get.foreach{ _.setHtml } will reRender the table provided the first render took place and set the RequestVar.

If you are looking to only reRender the affected rows, that gets a bit more challenging.

I would probably try something like this:

First, create a template with the HTML for a given row. In this example, we'll put it in templates-hidden/rowtemplate.html. With the content:

<tr>
     <td role="data">Item goes here</td>
     <td><button role="remove" type="button">remove</button></td>
</tr>

Then, we'll modify the render to give each tr and retrieve the row from the template

  val rowTemplate = Templates("templates-hidden" :: "rowtemplate" :: Nil) openOr <tr></tr>

  def render = {
    def remove(item: String) = () => {
      ListDAO.remove(item)
      JsCmds.Run("$('#' + item.id).remove()")
    }

    ClearClearable &
    "tr" #> {
      "tr" #> ListDAO.all.map(item => {
        "* [id]" #> item.id &
        "role=data" #> item &
        "role=remove" #> ajaxInvoke(remove(item))
      })
    }.apply(rowTemplate)
  }

Note: The first <tr> above will bind to the TR in your html, the second will bind to the TR specified in the template.

def create = { var text = ""

 def process(): JsCmd = {
   val item = ListDAO.create(text)
   val rowNS = {
        "* [id]" #> item.id &
        "role=data" #> item &
        "role=remove" #> ajaxInvoke(remove(item))
    }.apply(rowTemplate)
   JsCmds.Run("tr:last").append(rowNS.toString)
 }

 "@text" #> SHtml.text(text, s => text = s) &
 "button *+" #> SHtml.hidden(process)
}

I haven't tested that to make sure it all works, but hopefully will point you in the right direction.

OTHER TIPS

How about an approach like this?:

in the class, add a field private var html: NodeSeq = NodeSeq.Empty

change the render method to def render(in: NodeSeq) = {html = in; ...; cssTransform.apply(in)}

in create, reuse the html again. Something like JsAppend("id", transformation.apply(html)).

Alternatives I know about:

  • SHtml.memoize (might be very useful, try it)
  • create separate html-s for <tr> columns. In the code you may use def html: NodeSeq = Templates("myHtmlFile" :: Nil).openOr(NodeSeq.Empty). Seems dirty to me because the <tr> code would be separated from the <table> then.
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top