Created
November 3, 2012 21:52
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ... | |
object author extends ObjectIdRefListField(this, MongoTerm) | |
with One2ManyCRUD[BsonMetadata, MongoTerm] { | |
override def searchMeta = MongoTerm | |
override def definingQuery = ("category" -> "person") | |
} | |
// ... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<input id="F792666916492JZFORT" name="F792666916504MKWKBL" class="s2-input" | |
data-initial="[{"id":"50618d7579099aacc56aaf77","text":"Andi Wand"}]" | |
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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