diff --git a/deform/templates/autocomplete_input.pt b/deform/templates/autocomplete_input.pt
index 9ddf6bd4..40bb730c 100644
--- a/deform/templates/autocomplete_input.pt
+++ b/deform/templates/autocomplete_input.pt
@@ -12,11 +12,11 @@
class string: form-control ${css_class};
style style"
id="${oid}"/>
-
diff --git a/deform/tests/test_widget.py b/deform/tests/test_widget.py
index b905bb93..b1f228ef 100644
--- a/deform/tests/test_widget.py
+++ b/deform/tests/test_widget.py
@@ -1,4 +1,5 @@
import unittest
+import warnings
from deform.compat import (
text_type,
@@ -222,6 +223,25 @@ def test_deserialize_with_nondefault_thousands_separator(self):
result = widget.deserialize(field, pstruct)
self.assertEqual(result, '1000000.00')
+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
@@ -288,6 +308,49 @@ def test_serialize_iterable(self):
"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 a69f121b..8b7fde62 100644
--- a/deform/widget.py
+++ b/deform/widget.py
@@ -3,6 +3,7 @@
import csv
import json
import random
+from warnings import warn
from colander import (
Invalid,
@@ -17,6 +18,7 @@
string_types,
StringIO,
string,
+ text_type,
url_quote,
uppercase,
)
@@ -369,11 +371,37 @@ def deserialize(self, field, pstruct):
return null
return pstruct
-class AutocompleteInputWidget(Widget):
+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
- http://twitter.github.com/bootstrap/javascript.html#typeahead.
+ autocompletion via a list of values using twitters's typeahead plugin
+ https://github.com/twitter/typeahead.js
**Attributes/Arguments**
@@ -423,6 +451,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
readonly_template = 'readonly/textinput'
@@ -432,6 +484,7 @@ class AutocompleteInputWidget(Widget):
style = None
template = 'autocomplete_input'
values = None
+ datasets = None
requirements = (('typeahead', None), ('deform', None))
def __init__(self, **kw):
@@ -445,18 +498,37 @@ def serialize(self, field, cstruct, **kw):
)
if cstruct in (null, None):
cstruct = ''
- self.values = self.values or []
readonly = kw.get('readonly', self.readonly)
- options = {}
- if isinstance(self.values, string_types):
- options['remote'] = '%s?term=%%QUERY' % self.values
+ options = {
+ 'minLength': kw.pop('min_length', self.min_length),
+ 'limit': kw.pop('items', self.items),
+ }
+
+ datasets = kw.pop('datasets', self.datasets)
+ if datasets is None:
+ if isinstance(self.values, string_types):
+ options['remote'] = '%s?term=%%QUERY' % self.values
+ datasets = options
+ elif self.values:
+ options['local'] = self.values
+ datasets = options
else:
- options['local'] = self.values
+ if self.values:
+ warn('AutocompleteWidget ignores the *values* parameter '
+ 'when *datasets* is also specified.')
- options['minLength'] = kw.pop('min_length', self.min_length)
- options['limit'] = kw.pop('items', self.items)
- kw['options'] = json.dumps(options)
+ 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 = 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)