Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AutocompleteInputWidget - more powerful configuration for typeahead.js #413

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions deform/templates/autocomplete_input.pt
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
style style;
attributes|field.widget.attributes|{};"
id="${oid}"/>
<script tal:condition="field.widget.values" type="text/javascript">
<script tal:condition="options" type="text/javascript">
deform.addCallback(
'${field.oid}',
function (oid) {
$('#' + oid).typeahead(${options});
$('#' + oid).typeahead(${structure: options});
}
);
</script>
Expand Down
63 changes: 63 additions & 0 deletions deform/tests/test_widget.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Widget tests."""
# Standard Library
import unittest
import warnings

# Pyramid
import colander
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
89 changes: 81 additions & 8 deletions deform/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import csv
import json
import random
from warnings import warn

# Pyramid
from colander import Invalid
Expand Down Expand Up @@ -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 ``<input type="text"/>`` 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**

Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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)
Expand Down