Skip to content

Instantly share code, notes, and snippets.

@tgpfeiffer
Created November 3, 2012 21:52
Show Gist options
  • Save tgpfeiffer/4008984 to your computer and use it in GitHub Desktop.
Save tgpfeiffer/4008984 to your computer and use it in GitHub Desktop.
Lift: CRUD interface for "foreign keys" using an AJAX auto-complete and select2
// ...
object author extends ObjectIdRefListField(this, MongoTerm)
with One2ManyCRUD[BsonMetadata, MongoTerm] {
override def searchMeta = MongoTerm
override def definingQuery = ("category" -> "person")
}
// ...
<script type="text/javascript">
$(".s2-input").each(function(i, elem) {
var fnName = 'selectTerm_' + $(this).attr("name");
$(this).select2({
initSelection: function(element, callback) {
callback(element.data("initial"));
},
minimumInputLength : 1,
query : window[fnName],
multiple: true,
width : "element"
});
});
</script>
trait One2ManyCRUD[A <: BsonRecord[A], B <: MongoRecord[B]]
extends ObjectIdRefListField[A, B] {
def searchMeta: Searchable[B]
/**
* This can be used to limit the set of B records that can be selected
* to a subset of B.findAll(). For example,
* override def definingQuery = ("category" -> "person")
* means that only such B objects with
* category = person
* can be selected.
*/
def definingQuery: JObject
/**
* Find the list of B entities matching the given string. The meaning
* of "matching" is defined by the [[Searchable.matchingQuery]]
* function, i.e. B knows how to find its own objects by string,
* but this fields's definingQuery can further narrow down the matching
* database entries.
*/
def matchingEntries(q: String, lang: String, page: Int, pageSize: Int):
(List[B], Boolean) = {
require(page >= 0)
require(pageSize > 0)
val query = searchMeta.matchingQuery(q, lang) ~ definingQuery
val results = refMeta.findAll(query, ("slug" -> 1),
Limit(pageSize + 1), Skip((page - 1) * pageSize))
val hasMore = results.size > pageSize
(results.take(pageSize), hasMore)
}
/**
* This creates a jsonCall that basically wraps [[matchingEntries]] and
* returns it in a format suitable for processing with select2.
*/
protected def matchingEntriesAjaxCall =
SHtml.jsonCall(
JE.JsRaw("query"),
new JsonContext(Full("success"), Empty),
(json: JValue) => {
// extract the query information from the JSON data
val pageSize = 5
implicit val formats = net.liftweb.json.DefaultFormats
val page = (json \ "page").extractOpt[Int].getOrElse(1)
val (objs, hasMore) = json \ "term" match {
case JString(term) =>
// lookup the data
matchingEntries(term, S.locale.getLanguage, page, pageSize)
case _ =>
(List(), false)
}
val results = objs.map(term => {
("id" -> term.id.toString) ~ ("text" -> term.stringIdentifier)
})
val r: JValue = ("results" -> JArray(results)) ~
("more" -> hasMore)
r
}
)
/**
* Render this as a <input type="hidden"> element with an appended
* JavaScript function that can be used by the select2 library to
* turn it into an auto-complete widget.
*/
override def toForm = {
// create a JSON blob with the initial data
val initialData: JValue = JArray(
objs.map(o => (("id" -> o.id.toString) ~
("text" -> o.stringIdentifier))))
val initialValue = is.map(_.toString).mkString(",")
// create the <input> element that will later hold the select2 list
def setFromString(s: String) = {
// split data, check for valid object ids and then set
val oids: List[ObjectId] = s.split(",").flatMap(id => tryo {
val oid = new ObjectId(id)
// important: make sure that no other objects are linked
refMeta.find(("_id" -> oid) ~ definingQuery).map(x => oid)
}).toList.flatten
set(oids)
}
val html = SHtml.hidden(setFromString _, initialValue,
("class" -> "s2-input"),
("data-initial" -> compact(render(initialData))))
// compose a function including the name of this input widget
val fnName = "selectTerm_" + (html \ "@name" text)
val script = <script type="text/javascript">{
JsCmds.Function(fnName, List("query"), JE.JsRaw(
"""function success(data, textStatus, jqXHR) { query.callback(data); }; """ +
matchingEntriesAjaxCall.toJsCmd).cmd).toJsCmd
}</script>
Full(html ++ script)
}
}
<input id="F792666916492JZFORT" name="F792666916504MKWKBL" class="s2-input"
data-initial="[{&quot;id&quot;:&quot;50618d7579099aacc56aaf77&quot;,&quot;text&quot;:&quot;Andi Wand&quot;}]"
type="hidden" value="50618d7579099aacc56aaf77">
<script type="text/javascript">function selectTerm_F792666916504MKWKBL(query) {
function success(data, textStatus, jqXHR) { query.callback(data); };
liftAjax.lift_ajaxHandler('F792666916505OWUF0Y=' + encodeURIComponent(JSON.stringify(query)), success, null, "json");
}
</script>
trait Searchable[A <: MongoRecord[A]] {
self: MongoRecord[A] =>
/**
* Defines the query for matching against certain strings.
*/
def matchingQuery(q: String, lang: String): JObject = {
("$or" -> List(
("de.title" -> (("$regex" -> (".*" + q + ".*")) ~
("$options" -> "i"))),
("en.title" -> (("$regex" -> (".*" + q + ".*")) ~
("$options" -> "i")))
))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment