alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Lift Framework example source code file (TableEditor.scala)

This example Lift Framework source code file (TableEditor.scala) is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Java - Lift Framework tags/keywords

list, mappedfield, mappedfield, metamapper, nil, nil, nodeseq, seq, some, string, string, t, t, unit

The Lift Framework TableEditor.scala source code

/*
 * Copyright 2009-2011 WorldWide Conferencing, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.liftweb
package mapper
package view

import xml.{NodeSeq, Text}

import common.{Box, Full, Empty}
import util.BindPlus._
import util.{Helpers, BindHelpers}
import Helpers._
import http.{SHtml, S, DispatchSnippet, js}
import S.?

import js.JsCmds.{Script, Run}

import mapper.{Mapper, MetaMapper, LongKeyedMetaMapper, MappedField}

import Util._


/**
 * Keeps track of pending adds to and removes from a list of mappers.
 * Supports in-memory sorting by a field.
 * Usage: override metaMapper with a MetaMapper instance, call sortBy
 * to specify the field to sort by. If it is already sorted by that
 * field it will sort descending, otherwise ascending.
 * Call save to actualize changes.
 * @author nafg
 */
trait ItemsList[T <: Mapper[T]] {
  /**
   * The MetaMapper that provides create and findAll functionality etc.
   * Must itself be a T (the mapper type it represents)
   */
  def metaMapper: T with MetaMapper[T]

  /**
   * Whether the sorting algorithm should put null first or last
   */
  var sortNullFirst = true
  /**
   * The list of items that correspond to items in the database
   */
  var current: List[T] = Nil
  /**
   * The list of items pending to be added to the database
   */
  var added: List[T] = Nil
  /**
   * The list of items to be deleted from current
   */
  var removed: List[T] = Nil
  /**
   * The field to sort by, if any
   */
  var sortField: Option[MappedField[_, T]] = None
  /**
   * The sort direction
   */
  var ascending = true

  /**
   * Returns the items (current + added - removed), sorted.
   * Sorting sorts strings case-insensitive, as well as Ordered and java.lang.Comparable.
   * Anything else where both values are nonnull are sorted via their toString method (case sensitive)
   */
  def items: Seq[T] = {
    import scala.util.Sorting._
    val unsorted: List[T] = current.filterNot(removed.contains) ++ added
    sortField match {
      case None =>
        unsorted
      case Some(field) =>
        unsorted.sortWith {
          (a, b) => ((field.actualField(a).is: Any, field.actualField(b).is: Any) match {
            case (aval: String, bval: String) => aval.toLowerCase < bval.toLowerCase
            case (aval: Ordered[Any], bval: Ordered[Any]) => aval < bval
            case (aval: java.lang.Comparable[Any], bval: java.lang.Comparable[Any]) => (aval compareTo bval) < 0
            case (null, _) => sortNullFirst
            case (_, null) => !sortNullFirst
            case (aval, bval) => aval.toString < bval.toString
          }) match {
            case cmp =>
              if(ascending) cmp else !cmp
          }
        }
    }
  }
  /**
   * Adds a new, unsaved item
   */
  def add {
    added ::= metaMapper.create
  }
  /**
   * Marks an item pending for removal
   */
  def remove(i: T) {
    if(added.exists(i.eq))
      added = added.filter(i.ne)
    else if(current.contains(i))
      removed ::= i
  }
  /**
   * Reset the ItemsList from the database: calls refresh, and 'added' and 'removed' are cleared.
   */
  def reload {
    refresh
    added = Nil
    removed = Nil
  }
  /**
   * Reloads the contents of 'current' from the database
   */
  def refresh {
    current = metaMapper.findAll
  }
  /**
   * Sends to the database:
   *  added is saved
   *  removed is deleted
   *  (current - removed) is saved
   */
  def save {
    val (successAdd, failAdd) = added.partition(_.save)
    added = failAdd

    val (successRemove, failRemove) = removed.partition(_.delete_!)
    current = current.filterNot(successRemove.contains)
    removed = failRemove

    for(c <- current if c.validate.isEmpty) c.save

    current ++= successAdd
  }


  def sortBy(field: MappedField[_, T]) = (sortField, ascending) match {
    case (Some(f), true) if f eq field =>
      ascending = false
    case _ | null =>
      sortField = Some(field)
      ascending = true
  }
  def sortFn(field: MappedField[_, T]) = (sortField, ascending) match {
    case (Some(f), true) if f eq field =>
      () => ascending = false
    case _ | null =>
      () => {
        sortField = Some(field)
        ascending = true
      }
  }

  reload
}


/**
 * Holds a registry of TableEditor delegates
 * Call TableEditor.registerTable(name_to_use_in_view, meta_mapper_for_the_table, display_title)
 * in Boot after DB.defineConnectionManager.
 * Referencing TableEditor triggers registering its snippet package and enabling
 * the provided template, /tableeditor/default.
 * @author nafg
 */
object TableEditor {
  net.liftweb.http.LiftRules.addToPackages("net.liftweb.mapper.view")

  private[view] val map = new scala.collection.mutable.HashMap[String, TableEditorImpl[_]]
  def registerTable[T<:Mapper[T]](name: String, meta: T with MetaMapper[T], title: String) =
    map(name) = new TableEditorImpl(title, meta)
}

package snippet {
  /**
   * This is the snippet that the view references.
   * It requires the following contents:
   * table:title - the title registered in Boot
   * header:fields - repeated for every field of the MetaMapper, for the header.
   *  field:name - the displayName of the field, capified. Links to sort by the field.
   * table:items - repeated for each record
   * item:fields - repeated for each field of the current record
   *  field:form - the result of toForm on the field
   * item:removeBtn - a button to remove the current item
   * table:insertBtn - a button to insert another item
   * For a default layout, use lift:embed what="/tableeditor/default", with
   * @author nafg
   */
  class TableEditor extends DispatchSnippet {
    private def getInstance: Box[TableEditorImpl[_]] = S.attr("table").map(TableEditor.map(_))
    def dispatch = {
      case "edit" =>
        val o = getInstance.open_!
        o.edit _
    }
  }
}

/**
 * This class does the actual view binding against a ItemsList.
 * The implementation is in the base trait ItemsListEditor
 * @author nafg
 */
protected class TableEditorImpl[T <: Mapper[T]](val title: String, meta: T with MetaMapper[T]) extends ItemsListEditor[T] {
  var items = new ItemsList[T] {
    def metaMapper = meta
  }
}

/**
 * General trait to edit an ItemsList.
 * @author nafg
 */
trait ItemsListEditor[T<:Mapper[T]] {
  def items: ItemsList[T]
  def title: String

  def onInsert: Unit = items.add
  def onRemove(item: T): Unit = items.remove(item)
  def onSubmit: Unit = try {
    items.save
  } catch {
    case e: java.sql.SQLException =>
      S.error("Not all items could be saved!")
  }
  def sortFn(f: MappedField[_, T]): ()=>Unit = items.sortFn(f)

  val fieldFilter: MappedField[_,T]=>Boolean = (f: MappedField[_,T])=>true

  def customBind(item: T): NodeSeq=>NodeSeq = (ns: NodeSeq) => ns

  def edit(xhtml: NodeSeq): NodeSeq = {
    def unsavedScript = (<head>{Script(Run("""
                           var safeToContinue = false
                           window.onbeforeunload = function(evt) {{  // thanks Tim!
                             if(!safeToContinue) {{
                               var reply = "You have unsaved changes!";
                               if(typeof evt == 'undefined') evt = window.event;
                               if(evt) evt.returnValue = reply;
                               return reply;
                             }}
                           }}
    """))}</head>)
    val noPrompt = "onclick" -> "safeToContinue=true"
    val optScript = if(
      (items.added.length + items.removed.length == 0) &&
        items.current.forall(!_.dirty_?)
    ) {
      NodeSeq.Empty
    } else {
      unsavedScript
    }
    optScript ++ xhtml.bind("header",
         "fields" -> eachField[T](
             items.metaMapper,
             (f: MappedField[_,T]) => Seq(
               "name" -> SHtml.link(S.uri, sortFn(f), Text(capify(f.displayName)))
             ),
             fieldFilter
         )
    ).bind("table",
         "title" -> title,
         "insertBtn" -> SHtml.submit(?("Insert"), onInsert _, noPrompt),
         "items" -> {ns:NodeSeq =>
           items.items.flatMap {i =>
             bind("item",
                  customBind(i)(ns),
                  "fields" -> eachField(
                    i,
                    (f: MappedField[_,T]) => Seq("form" -> f.toForm),
                    fieldFilter
                  ),
                  "removeBtn" -> SHtml.submit(?("Remove"), ()=>onRemove(i), noPrompt),
                  "msg" -> ((i.validate match {
                    case Nil =>
                      if(!i.saved_?) Text(?("New")) else if(i.dirty_?) Text(?("Unsaved")) else NodeSeq.Empty
                    case errors => (<ul>{errors.flatMap(e => 
  • {e.msg}
  • )}) })) ) } ++ items.removed.flatMap { i => bind("item", customBind(i)(ns), "fields" -> eachField( i, {f: MappedField[_,T] => Seq("form" -> <strike>{f.asHtml})}, fieldFilter ), "removeBtn" -> NodeSeq.Empty, "msg" -> Text(?("Deleted")) ) }: NodeSeq }, "saveBtn" -> SHtml.submit(?("Save"), onSubmit _, noPrompt) ) } }

    Other Lift Framework examples (source code examples)

    Here is a short list of links related to this Lift Framework TableEditor.scala source code file:

    ... this post is sponsored by my books ...

    #1 New Release!

    FP Best Seller

     

    new blog posts

     

    Copyright 1998-2021 Alvin Alexander, alvinalexander.com
    All Rights Reserved.

    A percentage of advertising revenue from
    pages under the /java/jwarehouse URI on this website is
    paid back to open source projects.