我正在寻找的内容:一个小工具,为用户提供一个下拉列表,但下方还有一个文本输入框,供用户输入新值。
后端模型将有一组默认选项(但不会在模型上使用choices关键字)。我知道我可以(并且我已经)通过让表单同时具有ChoicesField和CharField来实现这一点,并且如果ChoicesField保留默认值,则代码使用CharField,但这感觉“un-django”就像。
是否有办法(使用Django-builtins或Django插件)为表单定义类似ChoiceEntryField(在IIRC为此执行此操作的GtkComboboxEntry之后建模)?
如果有人发现这一点,请注意在https://ux.stackexchange.com/questions/85980/is-there-a-ux-pattern-for-drop-down-preferred-but-free-text-allowed
从UX角度出发,如何最好地完成我想要的工作,这是一个类似的问题。答案 0 :(得分:22)
我建议使用自定义Widget方法,HTML5允许您使用下拉列表进行自由文本输入,该列表可用作选择一个或写入其他类型的字段,这就是我的方法:< / p>
fields.py
from django import forms
class ListTextWidget(forms.TextInput):
def __init__(self, data_list, name, *args, **kwargs):
super(ListTextWidget, self).__init__(*args, **kwargs)
self._name = name
self._list = data_list
self.attrs.update({'list':'list__%s' % self._name})
def render(self, name, value, attrs=None, renderer=None):
text_html = super(ListTextWidget, self).render(name, value, attrs=attrs)
data_list = '<datalist id="list__%s">' % self._name
for item in self._list:
data_list += '<option value="%s">' % item
data_list += '</datalist>'
return (text_html + data_list)
forms.py
from django import forms
from myapp.fields import ListTextWidget
class FormForm(forms.Form):
char_field_with_list = forms.CharField(required=True)
def __init__(self, *args, **kwargs):
_country_list = kwargs.pop('data_list', None)
super(FormForm, self).__init__(*args, **kwargs)
# the "name" parameter will allow you to use the same widget more than once in the same
# form, not setting this parameter differently will cuse all inputs display the
# same list.
self.fields['char_field_with_list'].widget = ListTextWidget(data_list=_country_list, name='country-list')
views.py
from myapp.forms import FormForm
def country_form(request):
# instead of hardcoding a list you could make a query of a model, as long as
# it has a __str__() method you should be able to display it.
country_list = ('Mexico', 'USA', 'China', 'France')
form = FormForm(data_list=country_list)
return render(request, 'my_app/country-form.html', {
'form': form
})
答案 1 :(得分:6)
我知道我参加聚会有点晚了,但我最近还有另一个解决方案。
我使用Input
widget的django-floppyforms datalist
参数。这会生成一个HTML5 <datalist>
元素,您的浏览器会自动为其创建一个建议列表(另请参阅this SO answer)。
以下是模型表格的简单外观:
class MyProjectForm(ModelForm):
class Meta:
model = MyProject
fields = "__all__"
widgets = {
'name': floppyforms.widgets.Input(datalist=_get_all_proj_names())
}
答案 2 :(得分:3)
编辑:更新以使其也适用于UpdateView
所以我所寻找的似乎是
<强> utils.py:强>
from django.core.exceptions import ValidationError
from django import forms
class OptionalChoiceWidget(forms.MultiWidget):
def decompress(self,value):
#this might need to be tweaked if the name of a choice != value of a choice
if value: #indicates we have a updating object versus new one
if value in [x[0] for x in self.widgets[0].choices]:
return [value,""] # make it set the pulldown to choice
else:
return ["",value] # keep pulldown to blank, set freetext
return ["",""] # default for new object
class OptionalChoiceField(forms.MultiValueField):
def __init__(self, choices, max_length=80, *args, **kwargs):
""" sets the two fields as not required but will enforce that (at least) one is set in compress """
fields = (forms.ChoiceField(choices=choices,required=False),
forms.CharField(required=False))
self.widget = OptionalChoiceWidget(widgets=[f.widget for f in fields])
super(OptionalChoiceField,self).__init__(required=False,fields=fields,*args,**kwargs)
def compress(self,data_list):
""" return the choicefield value if selected or charfield value (if both empty, will throw exception """
if not data_list:
raise ValidationError('Need to select choice or enter text for this field')
return data_list[0] or data_list[1]
(的 forms.py 强>)
from .utils import OptionalChoiceField
from django import forms
from .models import Dummy
class DemoForm(forms.ModelForm):
name = OptionalChoiceField(choices=(("","-----"),("1","1"),("2","2")))
value = forms.CharField(max_length=100)
class Meta:
model = Dummy
(示例虚拟模型.py :)
from django.db import models
from django.core.urlresolvers import reverse
class Dummy(models.Model):
name = models.CharField(max_length=80)
value = models.CharField(max_length=100)
def get_absolute_url(self):
return reverse('dummy-detail', kwargs={'pk': self.pk})
(示例虚拟views.py:)
from .forms import DemoForm
from .models import Dummy
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, UpdateView
class DemoCreateView(CreateView):
form_class = DemoForm
model = Dummy
class DemoUpdateView(UpdateView):
form_class = DemoForm
model = Dummy
class DemoDetailView(DetailView):
model = Dummy
答案 3 :(得分:3)
我的要求与OP类似,但基本字段为 DecimalField 。因此,用户可以输入有效的浮点数或从可选选项列表中进行选择。
我喜欢奥斯汀·福克斯(Austin Fox)的回答,因为它遵循django框架要比Viktor eXe的回答更好。从 ChoiceField 对象继承后,该字段可以管理选项小部件数组。因此尝试尝试很诱人;
class CustomField(Decimal, ChoiceField): # MRO Decimal->Integer->ChoiceField->Field
...
class CustomWidget(NumberInput, Select):
但是,前提是该字段必须包含出现在选择列表中的内容。有一个方便的 valid_value 方法,您可以重写该方法以允许任何值,但是存在一个更大的问题-绑定到十进制模型字段。
从根本上讲,所有ChoiceField对象都管理值列表,然后具有一个或多个表示该选择的选择索引。因此绑定的数据将在小部件中显示为;
[some_data] or [''] empty value
因此,奥斯丁·福克斯(Austin Fox)重写了format_value方法,以返回到基本的Input类方法版本。适用于charfield,但不适用于Decimal或Float字段,因为我们丢失了数字小部件中的所有特殊格式。
所以我的解决方案是直接从Decimal字段继承,但仅添加choice属性(从django CoiceField提升)。...
首先是自定义小部件;
class ComboBoxWidget(Input):
"""
Abstract class
"""
input_type = None # must assigned by subclass
template_name = "datalist.html"
option_template_name = "datalist_option.html"
def __init__(self, attrs=None, choices=()):
super(ComboBoxWidget, self).__init__(attrs)
# choices can be any iterable, but we may need to render this widget
# multiple times. Thus, collapse it into a list so it can be consumed
# more than once.
self.choices = list(choices)
def __deepcopy__(self, memo):
obj = copy.copy(self)
obj.attrs = self.attrs.copy()
obj.choices = copy.copy(self.choices)
memo[id(self)] = obj
return obj
def optgroups(self, name):
"""Return a list of optgroups for this widget."""
groups = []
for index, (option_value, option_label) in enumerate(self.choices):
if option_value is None:
option_value = ''
subgroup = []
if isinstance(option_label, (list, tuple)):
group_name = option_value
subindex = 0
choices = option_label
else:
group_name = None
subindex = None
choices = [(option_value, option_label)]
groups.append((group_name, subgroup, index))
for subvalue, sublabel in choices:
subgroup.append(self.create_option(
name, subvalue
))
if subindex is not None:
subindex += 1
return groups
def create_option(self, name, value):
return {
'name': name,
'value': value,
'template_name': self.option_template_name,
}
def get_context(self, name, value, attrs):
context = super(ComboBoxWidget, self).get_context(name, value, attrs)
context['widget']['optgroups'] = self.optgroups(name)
context['wrap_label'] = True
return context
class NumberComboBoxWidget(ComboBoxWidget):
input_type = 'number'
class TextComboBoxWidget(ComboBoxWidget):
input_type = 'text'
自定义字段类
class OptionsField(forms.Field):
def __init__(self, choices=(), **kwargs):
super(OptionsField, self).__init__(**kwargs)
self.choices = list(choices)
def _get_choices(self):
return self._choices
def _set_choices(self, value):
"""
Assign choices to widget
"""
value = list(value)
self._choices = self.widget.choices = value
choices = property(_get_choices, _set_choices)
class DecimalOptionsField(forms.DecimalField, OptionsField):
widget = NumberComboBoxWidget
def __init__(self, choices=(), max_value=None, min_value=None, max_digits=None, decimal_places=None, **kwargs):
super(DecimalOptionsField, self).__init__(choices=choices, max_value=max_value, min_value=min_value,
max_digits=max_digits, decimal_places=decimal_places, **kwargs)
class CharOptionsField(forms.CharField, OptionsField):
widget = TextComboBoxWidget
def __init__(self, choices=(), max_length=None, min_length=None, strip=True, empty_value='', **kwargs):
super(CharOptionsField, self).__init__(choices=choices, max_length=max_length, min_length=min_length,
strip=strip, empty_value=empty_value, **kwargs)
HTML模板
datalist.html
<input list="{{ widget.name }}_list" type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %} />
<datalist id="{{ widget.name }}_list">{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
<optgroup label="{{ group_name }}">{% endif %}{% for option in group_choices %}
{% include option.template_name with widget=option %}{% endfor %}{% if group_name %}
</optgroup>{% endif %}{% endfor %}
</datalist>
datalist_option.html
<option value="{{ widget.value|stringformat:'s' }}"{% include "django/forms/widgets/attrs.html" %}>
使用示例。请注意,HTML数据列表选项标签不需要选择元组的第二个元素,因此我将其保留为None。同样,第一个元组值可以是文本或本地十进制数-您可以看到小部件如何处理它们。
class FrequencyDataForm(ModelForm):
frequency_measurement = DecimalOptionsField(
choices=(
('Low Freq', (
('11.11', None),
('22.22', None),
(33.33, None),
),
),
('High Freq', (
('66.0E+06', None),
(1.2E+09, None),
('2.4e+09', None)
),
)
),
required=False,
max_digits=15,
decimal_places=3,
)
class Meta:
model = FrequencyData
fields = '__all__'
答案 4 :(得分:2)
输入类型在选择字段和文本字段中是否相同?如果是这样,我会在类中创建一个CharField(或Textfield)并使用一些前端javascript / jquery通过应用&#34来处理将传递的数据;如果下拉列表中没有信息,则使用textfield中的数据&# 34;子句。
我制作了一个jsFiddle来演示如何在前端执行此操作。
HTML:
<div class="formarea">
<select id="dropdown1">
<option value="One">"One"</option>
<option value="Two">"Two"</option>
<option value="Three">or just write your own</option>
</select>
<form><input id="txtbox" type="text"></input></form>
<input id="inputbutton" type="submit" value="Submit"></input>
</div>
JS:
var txt = document.getElementById('txtbox');
var btn = document.getElementById('inputbutton');
txt.disabled=true;
$(document).ready(function() {
$('#dropdown1').change(function() {
if($(this).val() == "Three"){
document.getElementById('txtbox').disabled=false;
}
else{
document.getElementById('txtbox').disabled=true;
}
});
});
btn.onclick = function () {
if((txt).disabled){
alert('input is: ' + $('#dropdown1').val());
}
else{
alert('input is: ' + $(txt).val());
}
};
然后,您可以在提交时指定将哪个值传递给您的视图。
答案 5 :(得分:0)
以下是我如何解决这个问题。我从传递给模板form
对象中检索选项并手动填充datalist
:
{% for field in form %}
<div class="form-group">
{{ field.label_tag }}
<input list="options" name="test-field" required="" class="form-control" id="test-field-add">
<datalist id="options">
{% for option in field.subwidgets %}
<option value="{{ option.choice_label }}"/>
{% endfor %}
</datalist>
</div>
{% endfor %}
答案 6 :(得分:0)
我知道这很老了,但认为这可能对其他人有用。 以下实现了与Viktor eXe's答案类似的结果,但适用于使用django本机方法的模型,查询集和外键。
在forms.py子类forms.Select和forms.ModelChoiceField中:
from django import forms
class ListTextWidget(forms.Select):
template_name = 'listtxt.html'
def format_value(self, value):
# Copied from forms.Input - makes sure value is rendered properly
if value == '' or value is None:
return ''
if self.is_localized:
return formats.localize_input(value)
return str(value)
class ChoiceTxtField(forms.ModelChoiceField):
widget=ListTextWidget()
然后在模板中创建listtxt.html:
<input list="{{ widget.name }}"
{% if widget.value != None %} name="{{ widget.name }}" value="{{ widget.value|stringformat:'s' }}"{% endif %}
{% include "django/forms/widgets/attrs.html" %}>
<datalist id="{{ widget.name }}">
{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
<optgroup label="{{ group_name }}">{% endif %}{% for option in group_choices %}
{% include option.template_name with widget=option %}{% endfor %}{% if group_name %}
</optgroup>{% endif %}{% endfor %}
</datalist>
现在您可以在表格中使用窗口小部件或字段了。
from .fields import *
from django import forms
Class ListTxtForm(forms.Form):
field = ChoiceTxtField(queryset=YourModel.objects.all()) # Using new field
field2 = ModelChoiceField(queryset=YourModel.objects.all(),
widget=ListTextWidget()) # Using Widget
小部件和字段也可以在form.ModelForm Forms中使用,并且可以接受属性。
答案 7 :(得分:0)
参加聚会晚了5年,但是@Foon的自我解答OptionalChoiceWidget
正是我在寻找的东西,希望其他想问相同问题的人可以通过StackOverflow的回答算法来解决像我以前一样。
如果从选项下拉菜单中选择了答案,我希望文本输入框消失,这很容易实现。由于它可能对其他人有用:
{% block onready_js %}
{{block.super}}
/* hide the text input box if an answer for "name" is selected via the pull-down */
$('#id_name_0').click( function(){
if ($(this).find('option:selected').val() === "") {
$('#id_name_1').show(); }
else {
$('#id_name_1').hide(); $('#id_name_1').val(""); }
});
$('#id_name_0').trigger("click"); /* sets initial state right */
{% endblock %}
有人想知道onready_js
块,(在我的base.html
模板中,其他所有东西都继承了)
<script type="text/javascript"> $(document).ready( function() {
{% block onready_js %}{% endblock onready_js %}
});
</script>
打败我,为什么不是每个人都这样做一点JQuery!
答案 8 :(得分:0)
这里是可重用的模型字段级别DataListCharField的用法。我修改了@Viktor eXe和@Ryan Skene的ListTextWidget。
# 'models.py'
from my_custom_fields import DatalistCharField
class MyModel(models.Model):
class MyChoices(models.TextChoices):
ans1 = 'ans1', 'ans1'
ans2 = 'ans2', 'ans2'
my_model_field = DatalistCharField('label of this field', datalist=MyChoices.choices, max_length=30)
在“ my_custom_fields.py”中定义的DatalistCharField(custom)。 从下到上都很容易阅读。
(DatalistCharField> ListTextField> ListTextWidget)
(((模型。字段>表格。字段>表格。小部件)结构)
# 'my_custom_fields.py'
class ListTextWidget(forms.TextInput): # form widget
def __init__(self, *args, **kwargs):
self.datalist = None
super(ListTextWidget, self).__init__(*args, **kwargs)
def render(self, name, value, attrs=None, renderer=None):
default_attrs = {'list': 'list__%s' % name}
default_attrs.update(attrs)
text_html = super(ListTextWidget, self).render(name, value, attrs=default_attrs) # TextInput rendered
data_list = '<datalist id="list__%s">' % name # append <datalist> under the <input> elem.
if self.datalist:
for _, value in self.datalist:
data_list += '<option value="%s">' % value
else:
data_list += '<option value="%s">' % 'no' # default datalist option
data_list += '</datalist>'
return text_html + data_list
class ListTextField(forms.CharField): # form field
widget = ListTextWidget
def __init__(self, *, max_length=None, min_length=None, strip=True, empty_value='', datalist=None, **kwargs):
super().__init__(max_length=max_length, min_length=min_length, strip=strip, empty_value=empty_value, **kwargs)
self.widget.datalist = datalist
class DatalistCharField(models.CharField): # model field
def __init__(self, *args, **kwargs):
self.datalist = kwargs.pop('datalist', None) # custom parameters should be poped here to bypass super().__init__() or it will raise an error of wrong parameter
super().__init__(*args, **kwargs)
def formfield(self, **kwargs):
defaults = {'form_class': ListTextField, 'datalist': self.datalist} # bypassed custom parameters arrived here
defaults.update(**kwargs)
return super().formfield(**defaults)