Autocomplete for TextLine that allows custom values

I'd like to give users autocomplete support for entering a city name while still allowing them to add cities not known to our system.

The training documentation has an example for a List that allows to select item from a vocabulary but this results in the property being a List.

list_field_voc_unconstrained = schema.List(
    title=u'List field with values from vocabulary but not constrained to them.',
    value_type=schema.TextLine(),
    required=False,
    missing_value=[],
    )
directives.widget(
    'list_field_voc_unconstrained',
    AjaxSelectFieldWidget,
    vocabulary='plone.app.vocabularies.Users'
)

Using the same approach on a TextLine with SelectFieldWidget results in a lookup error when rendering the form (SelectFieldWidget seems not to play well with TextLine):

    widget(
        "city",
        SelectFieldWidget,
        vocabulary="kkv.site.cities",
        pattern_options={"placeholder": "select a city"},
    )
    city = schema.TextLine(
        title="City",
        required=False,
    )

zope.interface.interfaces.ComponentLookupError: ((<Container at /Plone/termine>, <WSGIRequest, URL=http://cms.localhost:8080/Plone/organisa/termine/++add++Event>, <plone.dexterity.browser.add.DefaultAddForm object at 0x7fbe98802e50>, <zope.schema._bootstrapfields.TextLine object at 0x7fbe990ee160 kkv.site.behaviors.city.ICity.city>, <SelectWidget 'form.widgets.ICity.city'>), <InterfaceClass z3c.form.interfaces.ITerms>, '')

-> raise ComponentLookupError(objects, interface, name)

Using schema.Choice works but limits the user to the vocabulary values (as choice requires a vocabulary and does not have an option such as enforceVocabulary (from Products.AutocompleteWidget) that could be set to false)

    widget(
        "city",
        SelectFieldWidget,
        pattern_options={"placeholder": "select a city"},
    )
    city = schema.Choice(
        title=_("City"),
        required=False,
        vocabulary="kkv.site.cities",
    )

Is there a recommended way for a single select Textfield that allows custom input?

side question: plone.training does not mention GitHub - plone/plone.formwidget.autocomplete: z3c.form widget for Plone using jQuery Autocomplete. is this meant to be superseded by plone.app.z3cform [Ajax]SelectWidget or is this still relevant and worth a try?

I think the subject field does exactly what you want to do: plone.app.dexterity/metadata.py at master · plone/plone.app.dexterity · GitHub

thanks for your reply @tmassman. the pattern of the subject field is more or less simliar to list_field_voc_unconstrained from the example above.

drawback: it uses a schema.Tuple / schema.List field so the resulting attribute will be an iterable instead of str :frowning:

i also tried something like this in the meantime

  • form renders
  • widget allows vocab values and custom input
  • BUT widget is multiselect and i can't figure out how to stop it to allow mutiple values
  • i use a predefined vocab in the example below because i previously had troubles to get ajaxselectfield working with custom searchable vocabs / sources (errors similar to the ones reported in this thread)
widget(
    "city",
    AjaxSelectFieldWidget,
    vocabulary="plone.app.vocabularies.Users",  
    pattern_options={
        "maximumSelectionLength": 1,  # does not affect multiselect
        "multiple": False, # neither
    },
)
city = schema.TextLine(
    title="City",
    required=False,
)

I was on the run yesterday, so sorry for my short reply.

I did this last year for a project and it works pretty well:

    directives.widget(
        'ingredient',
        AjaxSelectFieldWidget,
        pattern_options={
            'maximumSelectionSize': 1,
            'vocabularyUrl': get_ingredients_vocabulary_url,
        },
        vocabulary='xxx.recipe_ingredients'
    )
    ingredient = schema.TextLine(
        required=True,
        title=u'Zutat',
    )

This looks exactly like your example above :thinking:

We use this in a DataGridField and the schema also includes a quantity and unit field, so people can add a list of ingredients, but one line can only have 1 selected ingredient. And if it does not exist before, it will be added to the vocabulary.



This runs on Plone 5.2.2 right now

Just for completeness, this is the vocabulary used:

  <!-- Dynamic incredients vocabulary. Used by the IRecipe content type. -->
  <utility
      component=".recipes.IngredientsVocabularyFactory"
      name="xxx.recipe_ingredients"
      />
from plone.app.vocabularies.catalog import KeywordsVocabulary
from zope.interface import implementer
from zope.schema.interfaces import IVocabularyFactory

@implementer(IVocabularyFactory)
class IngredientsVocabulary(KeywordsVocabulary):
    """Return a list of available ingredients."""

    keyword_index = 'recipeIngredients'

IngredientsVocabularyFactory = IngredientsVocabulary()

And the catalog definition:

  <!-- Index for IRecipe.ingredients. -->
  <index name="recipeIngredients" meta_type="KeywordIndex">
    <indexed_attr value="recipeIngredients" />
  </index>

And the custom indexer:

@indexer(IRecipe)
def recipe_ingredients(obj):
    """Custom indexer for 'recipeIngredients' KeywordIndex."""
    result = set()
    ingredients = []
    ingredients.extend(obj.ingredients_details or [])

    # Check for sub recipes and index those ingredients as well
    sub_recipes = relapi.relations(obj, 'sub_recipe_links')
    for sub_recipe in sub_recipes:
        ingredients.extend(sub_recipe.ingredients_details or [])

    for ingredient in ingredients:
        name = ingredient.get('ingredient')
        if not name:
            continue
        if isinstance(name, tuple):
            for name_part in name:
                result.add(safe_encode(name_part))
        else:
            result.add(safe_encode(name))
    return tuple(result)
  <!-- Indexer: recipeIngredients for IRecipe. -->
  <adapter
      factory=".recipe.recipe_ingredients"
      name="recipeIngredients"
      />

Just a side note: recipes can have linked sub recipes, so you can use the recipe for e.g. dumplings for multiple main recipes. That’s what the sub_recipes above is for.

Edit:

The code above uses a dynamic URL for the vocabulary. I think that was necessary because of the DataGridField. So just for completeness:

def get_ingredients_vocabulary_url(context):
    request = getRequest()
    ctx = request.PUBLISHED.context
    if not IRecipe.providedBy(ctx):
        ctx = plone.api.portal.get()
    return u'/'.join([
        u'/'.join(ctx.getPhysicalPath()),
        '@@getIngredientsVocabulary?name=xxx.recipe_ingredients',
    ])
1 Like

Plone Foundation Code of Conduct