Search code examples
ajaxscalatemplateslift

Lift - CRUD with Designer Friendly templates and Ajax


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?


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.