diff --git a/deform/templates/autocomplete_input.pt b/deform/templates/autocomplete_input.pt
index 88c60b45..a9a8774a 100644
--- a/deform/templates/autocomplete_input.pt
+++ b/deform/templates/autocomplete_input.pt
@@ -11,11 +11,11 @@
style style;
attributes|field.widget.attributes|{};"
id="${oid}"/>
-
diff --git a/deform/tests/test_widget.py b/deform/tests/test_widget.py
index f41c4a74..9981ea18 100644
--- a/deform/tests/test_widget.py
+++ b/deform/tests/test_widget.py
@@ -1,6 +1,7 @@
"""Widget tests."""
# Standard Library
import unittest
+import warnings
# Pyramid
import colander
@@ -243,6 +244,25 @@ def test_deserialize_bad_type(self):
self.assertRaises(colander.Invalid, widget.deserialize, field, {})
+class Test_serialize_js_config(unittest.TestCase):
+ def _makeLiteral(self, js):
+ from deform.widget import literal_js
+ return literal_js(js)
+
+ def _callIt(self, obj):
+ from deform.widget import serialize_js_config
+ return serialize_js_config(obj)
+
+ def test_serialize_literal(self):
+ literal = self._makeLiteral('func()')
+ serialized = self._callIt(literal)
+ self.assertEqual(serialized, 'func()')
+
+ def test_serialize_literal_in_object(self):
+ literal = self._makeLiteral('func()')
+ serialized = self._callIt({'x': literal})
+ self.assertEqual(serialized, '{"x": func()}')
+
class TestAutocompleteInputWidget(unittest.TestCase):
def _makeOne(self, **kw):
from deform.widget import AutocompleteInputWidget
@@ -316,6 +336,49 @@ def test_serialize_iterable(self):
{"local": [1, 2, 3, 4], "minLength": 1, "limit": 8},
)
+ def test_serialize_dataset(self):
+ import json
+ widget = self._makeOne()
+ dataset = {'local': [5,6,7,8],
+ 'minLength': 2,
+ }
+ widget.datasets = dataset
+ renderer = DummyRenderer()
+ schema = DummySchema()
+ field = DummyField(schema, renderer=renderer)
+ cstruct = 'abc'
+ widget.serialize(field, cstruct)
+ self.assertEqual(renderer.template, widget.template)
+ self.assertEqual(renderer.kw['field'], field)
+ self.assertEqual(renderer.kw['cstruct'], cstruct)
+ self.assertEqual(json.loads(renderer.kw['options']),
+ [{"local": [5,6,7,8],
+ "minLength": 2,
+ "limit": 8}])
+
+ def test_serialize_warns_if_datasets_and_values(self):
+ widget = self._makeOne()
+ widget.datasets = [{'local': [42]}]
+ widget.values = [9]
+ renderer = DummyRenderer()
+ schema = DummySchema()
+ field = DummyField(schema, renderer=renderer)
+ cstruct = 'abc'
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter('always')
+ widget.serialize(field, cstruct)
+ self.assertEqual(len(w), 1)
+ self.assertTrue(' ignores ' in str(w[0].message))
+
+ def test_serialize_datasets_type_error(self):
+ widget = self._makeOne()
+ widget.datasets = 2
+ renderer = DummyRenderer()
+ schema = DummySchema()
+ field = DummyField(schema, renderer=renderer)
+ cstruct = 'abc'
+ self.assertRaises(TypeError, widget.serialize, field, cstruct)
+
def test_serialize_not_null_readonly(self):
widget = self._makeOne()
renderer = DummyRenderer()
diff --git a/deform/widget.py b/deform/widget.py
index 182d8dbf..4b06a2c8 100644
--- a/deform/widget.py
+++ b/deform/widget.py
@@ -4,6 +4,7 @@
import csv
import json
import random
+from warnings import warn
# Pyramid
from colander import Invalid
@@ -400,11 +401,40 @@ def deserialize(self, field, pstruct):
return pstruct
+class literal_js(text_type):
+ """ A marker class which can be used to include literal javascript within
+ the configurations for javascript functions.
+
+ """
+ def __json__(self):
+ return self
+
+
+class JSConfigSerializer(json.JSONEncoder):
+ """ A hack-up of the stock python JSON encoder to support the inclusion
+ of literal javascript in the JSON output.
+
+ """
+ def _iterencode(self, o, markers):
+ if hasattr(o, '__json__') and callable(o.__json__):
+ return (chunk for chunk in (o.__json__(),))
+ else:
+ return super(JSConfigSerializer, self)._iterencode(o, markers)
+
+ def encode(self, o):
+ # bypass "extremely simple cases and benchmarks" optimization
+ chunks = list(self.iterencode(o))
+ return ''.join(chunks)
+
+
+serialize_js_config = JSConfigSerializer().encode
+
+
class AutocompleteInputWidget(Widget):
"""
Renders an ```` widget which provides
- autocompletion via a list of values using bootstrap's typeahead plugin
- https://github.com/twitter/typeahead.js/
+ autocompletion via a list of values using twitters's typeahead plugin
+ https://github.com/twitter/typeahead.js
**Attributes/Arguments**
@@ -444,6 +474,30 @@ class AutocompleteInputWidget(Widget):
The max number of items to display in the dropdown. Defaults to
``8``.
+ datasets
+ This is an alternative way to configure the typeahead
+ javascript plugin. Use of this parameter provides access to
+ the complete set of functionality provided by ``typeahead.js``.
+
+ If set to other than ``None`` (the default), a JSONified
+ version of this value is used to configure the typeahead
+ plugin. (In this case any value specified for the ``values``
+ parameter is ignored. The ``min_length`` and ``items``
+ parameters will be used to fill in ``minLength`` and ``limit``
+ in ``datasets`` if they are not already there.)
+
+ Literal javascript can be included in the configuration using
+ the :class:`literal_js` marker. For example, assuming your
+ data provider yields datums that have an ``html`` attribute
+ containing HTML markup you want displayed for the choice::
+
+ datasets = {
+ 'remote': 'https://example.com/search.json?q=%QUERY',
+ 'template':
+ literal_js('function (datum) { return datum.html; }'),
+ }
+
+ might do the trick.
"""
min_length = 1
@@ -452,6 +506,7 @@ class AutocompleteInputWidget(Widget):
items = 8
template = "autocomplete_input"
values = None
+ datasets = None
requirements = (("typeahead", None), ("deform", None))
def serialize(self, field, cstruct, **kw):
@@ -466,14 +521,32 @@ def serialize(self, field, cstruct, **kw):
readonly = kw.get("readonly", self.readonly)
options = {}
- if isinstance(self.values, string_types):
- options["remote"] = "%s?term=%%QUERY" % self.values
- else:
- options["local"] = self.values
-
options["minLength"] = kw.pop("min_length", self.min_length)
options["limit"] = kw.pop("items", self.items)
- kw["options"] = json.dumps(options)
+
+ datasets = kw.pop('datasets', self.datasets)
+ if datasets is None:
+ if isinstance(self.values, string_types):
+ options['remote'] = '%s?term=%%QUERY' % self.values
+ elif self.values:
+ options['local'] = self.values
+ datasets = options
+ else:
+ if self.values:
+ warn('AutocompleteWidget ignores the *values* parameter '
+ 'when *datasets* is also specified.')
+
+ if isinstance(datasets, dict):
+ datasets = [datasets]
+ elif not isinstance(datasets, (list, tuple)):
+ raise TypeError(
+ 'Expected list, dict, or tuple, not %r' % datasets)
+
+ def set_defaults(dataset):
+ return dict(options, **dataset)
+ datasets = list(map(set_defaults, datasets))
+
+ kw['options'] = serialize_js_config(datasets) if datasets else None
tmpl_values = self.get_template_values(field, cstruct, kw)
template = readonly and self.readonly_template or self.template
return field.renderer(template, **tmpl_values)