Widget 是Django 對(duì)HTML 輸入元素的表示。Widget 負(fù)責(zé)渲染HTML和提取GET/POST 字典中的數(shù)據(jù)。
小貼士
不要將Widget 與表單字段搞混淆。表單字段負(fù)責(zé)驗(yàn)證輸入并直接在模板中使用。Widget 負(fù)責(zé)渲染網(wǎng)頁(yè)上HTML 表單的輸入元素和提取提交的原始數(shù)據(jù)。但是,Widget 需要賦值給表單的字段。
每當(dāng)你指定表單的一個(gè)字段的時(shí)候,Django 將使用適合其數(shù)據(jù)類(lèi)型的默認(rèn)Widget。若要查找每個(gè)字段使用的Widget,參見(jiàn)內(nèi)建的字段文檔。
然而,如果你想要使用一個(gè)不同的Widget,你可以在定義字段時(shí)使用widget 參數(shù)。例如:
from django import forms
class CommentForm(forms.Form):
name = forms.CharField()
url = forms.URLField()
comment = forms.CharField(widget=forms.Textarea)
這將使用一個(gè)Textarea Widget來(lái)設(shè)置表單的評(píng)論 ,而不是默認(rèn)的TextInput Widget。
很多Widget 都有可選的參數(shù);它們可以在定義字段的Widget 時(shí)設(shè)置。在下面的示例中,設(shè)置了SelectDateWidget 的years 屬性:
from django import forms
from django.forms.extras.widgets import SelectDateWidget
BIRTH_YEAR_CHOICES = ('1980', '1981', '1982')
FAVORITE_COLORS_CHOICES = (('blue', 'Blue'),
('green', 'Green'),
('black', 'Black'))
class SimpleForm(forms.Form):
birth_year = forms.DateField(widget=SelectDateWidget(years=BIRTH_YEAR_CHOICES))
favorite_colors = forms.MultipleChoiceField(required=False,
widget=forms.CheckboxSelectMultiple, choices=FAVORITE_COLORS_CHOICES)
可用的Widget 以及它們接收的參數(shù),參見(jiàn)內(nèi)建的Widget。
繼承自Select 的Widget 負(fù)責(zé)處理HTML 選項(xiàng)。它們呈現(xiàn)給用戶一個(gè)可以選擇的選項(xiàng)列表。不同的Widget 以不同的方式呈現(xiàn)選項(xiàng);Select 使用HTML 的列表形式<select>,而RadioSelect 使用單選按鈕。
ChoiceField 字段默認(rèn)使用Select。Widget 上顯示的選項(xiàng)來(lái)自ChoiceField,對(duì)ChoiceField.choices 的改變將更新Select.choices。例如:
>>> from django import forms
>>> CHOICES = (('1', 'First',), ('2', 'Second',))
>>> choice_field = forms.ChoiceField(widget=forms.RadioSelect, choices=CHOICES)
>>> choice_field.choices
[('1', 'First'), ('2', 'Second')]
>>> choice_field.widget.choices
[('1', 'First'), ('2', 'Second')]
>>> choice_field.widget.choices = ()
>>> choice_field.choices = (('1', 'First and only',),)
>>> choice_field.widget.choices
[('1', 'First and only')]
提供choices 屬性的Widget 也可以用于不是基于選項(xiàng)的字段 , 例如CharField —— 當(dāng)選項(xiàng)與模型有關(guān)而不只是Widget 時(shí),建議使用基于ChoiceField 的字段。
當(dāng)Django 渲染W(wǎng)idget 成HTML 時(shí),它只渲染最少的標(biāo)記 —— Django 不會(huì)添加class 的名稱(chēng)和特定于Widget 的其它屬性。這表示,網(wǎng)頁(yè)上所有TextInput 的外觀是一樣的。
有兩種自定義Widget 的方式:基于每個(gè)Widget 實(shí)例和基于每個(gè)Widget 類(lèi)。
如果你想讓某個(gè)Widget 實(shí)例與其它Widget 看上去不一樣,你需要在Widget 對(duì)象實(shí)例化并賦值給一個(gè)表單字段時(shí)指定額外的屬性(以及可能需要在你的CSS 文件中添加一些規(guī)則)。
例如下面這個(gè)簡(jiǎn)單的表單:
from django import forms
class CommentForm(forms.Form):
name = forms.CharField()
url = forms.URLField()
comment = forms.CharField()
這個(gè)表單包含三個(gè)默認(rèn)的TextInput Widget,以默認(rèn)的方式渲染 —— 沒(méi)有CSS 類(lèi)、沒(méi)有額外的屬性。這表示每個(gè)Widget 的輸入框?qū)秩镜靡荒R粯樱?/p>
>>> f = CommentForm(auto_id=False)
>>> f.as_table()
<tr><th>Name:</th><td><input type="text" name="name" /></td></tr>
<tr><th>Url:</th><td><input type="url" name="url"/></td></tr>
<tr><th>Comment:</th><td><input type="text" name="comment" /></td></tr>
在真正得網(wǎng)頁(yè)中,你可能不想讓每個(gè)Widget 看上去都一樣。你可能想要給comment 一個(gè)更大的輸入元素,你可能想讓‘name’ Widget 具有一些特殊的CSS 類(lèi)??梢灾付ā畉ype’ 屬性來(lái)利用新式的HTML5 輸入類(lèi)型。在創(chuàng)建Widget 時(shí)使用Widget.attrs 參數(shù)可以實(shí)現(xiàn):
class CommentForm(forms.Form):
name = forms.CharField(widget=forms.TextInput(attrs={'class': 'special'}))
url = forms.URLField()
comment = forms.CharField(widget=forms.TextInput(attrs={'size': '40'}))
Django 將在渲染的輸出中包含額外的屬性:
>>> f = CommentForm(auto_id=False)
>>> f.as_table()
<tr><th>Name:</th><td><input type="text" name="name" class="special"/></td></tr>
<tr><th>Url:</th><td><input type="url" name="url"/></td></tr>
<tr><th>Comment:</th><td><input type="text" name="comment" size="40"/></td></tr>
你還可以使用attrs 設(shè)置HTML id。參見(jiàn)BoundField.id_for_label 示例。
可以添加(css 和javascript)給Widget,以及深度定制它們的外觀和行為。
概況來(lái)講,你需要子類(lèi)化Widget 并定義一個(gè)“Media” 內(nèi)聯(lián)類(lèi) 或 創(chuàng)建一個(gè)“media” 屬性。
這些方法涉及到Python 高級(jí)編程,詳細(xì)細(xì)節(jié)在表單Assets 主題中講述。
Widget 和MultiWidget 是所有內(nèi)建Widget 的基類(lèi),并可用于自定義Widget 的基類(lèi)。
class Widget(attrs=None)
這是個(gè)抽象類(lèi),它不可以渲染,但是提供基本的屬性attrs。你可以在自定義的Widget 中實(shí)現(xiàn)或覆蓋render() 方法。
attrs
包含渲染后的Widget 將要設(shè)置的HTML 屬性。
>>> from django import forms
>>> name = forms.TextInput(attrs={'size': 10, 'title': 'Your name',})
>>> name.render('name', 'A name')
'<input title="Your name" type="text" name="name" value="A name" size="10" />'
Changed in Django 1.8:
如果你給一個(gè)屬性賦值True 或False,它將渲染成一個(gè)HTML5 風(fēng)格的布爾屬性:
>>> name = forms.TextInput(attrs={'required': True})
>>> name.render('name', 'A name')
'<input name="name" type="text" value="A name" required />'
>>>
>>> name = forms.TextInput(attrs={'required': False})
>>> name.render('name', 'A name')
'<input name="name" type="text" value="A name" />'
render(name, value, attrs=None)
返回Widget 的HTML,為一個(gè)Unicode 字符串。子類(lèi)必須實(shí)現(xiàn)這個(gè)方法,否則將引發(fā)NotImplementedError。
它不會(huì)確保給出的‘value’ 是一個(gè)合法的輸入,因此子類(lèi)的實(shí)現(xiàn)應(yīng)該防衛(wèi)式地編程。
value_from_datadict(data, files, name)
根據(jù)一個(gè)字典和該Widget 的名稱(chēng),返回該Widget 的值。files may contain data coming from request.FILES. 如果沒(méi)有提供value,則返回None。 在處理表單數(shù)據(jù)的過(guò)程中,value_from_datadict 可能調(diào)用多次,所以如果你自定義并添加額外的耗時(shí)處理時(shí),你應(yīng)該自己實(shí)現(xiàn)一些緩存機(jī)制。
class MultiWidget(widgets, attrs=None)
由多個(gè)Widget 組合而成的Widget。MultiWidget 始終與MultiValueField 聯(lián)合使用。
MultiWidget 具有一個(gè)必選參數(shù):
widgets
一個(gè)包含需要的Widget 的可迭代對(duì)象。
以及一個(gè)必需的方法:
decompress(value)
這個(gè)方法接受來(lái)自字段的一個(gè)“壓縮”的值,并返回“解壓”的值的一個(gè)列表??梢约僭O(shè)輸入的值是合法的,但不一定是非空的。
子類(lèi)必須實(shí)現(xiàn) 這個(gè)方法,而且因?yàn)橹悼赡転榭?,?shí)現(xiàn)必須要防衛(wèi)這點(diǎn)。
“解壓”的基本原理是需要“分離”組合的表單字段的值為每個(gè)Widget 的值。
有個(gè)例子是,SplitDateTimeWidget 將datetime 值分離成兩個(gè)獨(dú)立的值分別表示日期和時(shí)間:
from django.forms import MultiWidget
class SplitDateTimeWidget(MultiWidget):
# ...
def decompress(self, value):
if value:
return [value.date(), value.time().replace(microsecond=0)]
return [None, None]
小貼士
注意,MultiValueField 有一個(gè)compress() 方法用于相反的工作 —— 將所有字段的值組合成一個(gè)值。
其它可能需要覆蓋的方法:
render(name, value, attrs=None)
這個(gè)方法中的 value參數(shù)的處理方式與Widget子類(lèi)不同,因?yàn)樾枰宄绾螢榱嗽诓煌瑆idget中展示分割單一值。
渲染中使用的value參數(shù)可以是二者之一:
列表。列表的“壓縮”表現(xiàn)形式。如果value是個(gè)列表,render()的輸出會(huì)是一系列渲染后的子widget。如果value不是一個(gè)列表,首先會(huì)通過(guò)decompress()方法來(lái)預(yù)處理,創(chuàng)建列表,之后再渲染。
render()方法執(zhí)行HTML渲染時(shí),列表中的每個(gè)值都使用相應(yīng)的widget來(lái)渲染 -- 第一個(gè)值在第一個(gè)widget中渲染,第二個(gè)值在第二個(gè)widget中渲染,以此類(lèi)推。
不像單一值的widget,render() 方法并不需要在子類(lèi)中實(shí)現(xiàn)。
format_output(_renderedwidgets)
接受選然后的widget(以字符串形式)的一個(gè)列表,返回表示全部HTML的Unicode字符串。
這個(gè)鉤子允許你以任何你想要的方式,格式化widget的HTML設(shè)計(jì)。
下面示例中的Widget 繼承MultiWidget 以在不同的選擇框中顯示年、月、日。這個(gè)Widget 主要想用于DateField 而不是MultiValueField,所以我們實(shí)現(xiàn)了value_from_datadict():
from datetime import date
from django.forms import widgets
class DateSelectorWidget(widgets.MultiWidget):
def __init__(self, attrs=None):
# create choices for days, months, years
# example below, the rest snipped for brevity.
years = [(year, year) for year in (2011, 2012, 2013)]
_widgets = (
widgets.Select(attrs=attrs, choices=days),
widgets.Select(attrs=attrs, choices=months),
widgets.Select(attrs=attrs, choices=years),
)
super(DateSelectorWidget, self).__init__(_widgets, attrs)
def decompress(self, value):
if value:
return [value.day, value.month, value.year]
return [None, None, None]
def format_output(self, rendered_widgets):
return ''.join(rendered_widgets)
def value_from_datadict(self, data, files, name):
datelist = [
widget.value_from_datadict(data, files, name + '_%s' % i)
for i, widget in enumerate(self.widgets)]
try:
D = date(day=int(datelist[0]), month=int(datelist[1]),
year=int(datelist[2]))
except ValueError:
return ''
else:
return str(D)
構(gòu)造器在一個(gè)元組中創(chuàng)建了多個(gè)Select widget。超類(lèi)使用這個(gè)元組來(lái)啟動(dòng)widget。
format_output()方法相當(dāng)于在這里沒(méi)有干什么新的事情(實(shí)際上,它和MultiWidget中默認(rèn)實(shí)現(xiàn)的東西相同),但是這個(gè)想法是,你可以以自己的方式在widget之間添加自定義的HTML。
必需的decompress()方法將datetime.date 值拆成年、月和日的值,對(duì)應(yīng)每個(gè)widget。注意這個(gè)方法如何處理value為None的情況。
value_from_datadict()的默認(rèn)實(shí)現(xiàn)會(huì)返回一個(gè)列表,對(duì)應(yīng)每一個(gè)Widget。當(dāng)和MultiValueField一起使用MultiWidget的時(shí)候,這樣會(huì)非常合理,但是由于我們想要和擁有單一值得DateField一起使用這個(gè)widget,我們必須覆寫(xiě)這一方法,將所有子widget的數(shù)據(jù)組裝成datetime.date。這個(gè)方法從POST 字典中獲取數(shù)據(jù),并且構(gòu)造和驗(yàn)證日期。如果日期有效,會(huì)返回它的字符串,否則會(huì)返回一個(gè)空字符串,它會(huì)使form.is_valid返回False。
Django 提供所有基本的HTML Widget,并在django.forms.widgets 模塊中提供一些常見(jiàn)的Widget 組,包括文本的輸入、各種選擇框、文件上傳和多值輸入。
這些Widget 使用HTML 元素input 和 textarea。
class TextInput
文本輸入:<input type="text" ...>
class NumberInput
文本輸入:<input type="number" ...>
注意,不是所有瀏覽器的number輸入類(lèi)型都支持輸入本地化的數(shù)字。Django 將字段的localize 屬性設(shè)置為True 以避免字段使用它們。
class EmailInput
文本輸入:<input type="email" ...>
class URLInput
文本輸入:<input type="url" ...>
class PasswordInput
密碼輸入:<input type='password' ...>
接收一個(gè)可選的參數(shù):
render_value
決定在驗(yàn)證錯(cuò)誤后重新顯示表單時(shí),Widget 是否填充(默認(rèn)為False)。
class HiddenInput
隱藏的輸入:<input type='hidden' ...>
注意,還有一個(gè)MultipleHiddenInput Widget,它封裝一組隱藏的輸入元素。
class DateInput
日期以普通的文本框輸入:<input type='text' ...>
接收的參數(shù)與TextInput 相同,但是帶有一些可選的參數(shù):
format
字段的初始值應(yīng)該顯示的格式。
如果沒(méi)有提供format 參數(shù),默認(rèn)的格式為參考本地化格式在DATE_INPUT_FORMATS 中找到的第一個(gè)格式。
class DateTimeInput
日期/時(shí)間以普通的文本框輸入:<input type='text' ...>
接收的參數(shù)與TextInput 相同,但是帶有一些可選的參數(shù):
format
字段的初始值應(yīng)該顯示的格式。
如果沒(méi)有提供format 參數(shù),默認(rèn)的格式為參考本地化格式在DATETIME_INPUT_FORMATS 中找到的第一個(gè)格式。
class TimeInput
時(shí)間以普通的文本框輸入:<input type='text' ...>
接收的參數(shù)與TextInput 相同,但是帶有一些可選的參數(shù):
format
字段的初始值應(yīng)該顯示的格式。
如果沒(méi)有提供format 參數(shù),默認(rèn)的格式為參考本地化格式在TIME_INPUT_FORMATS 中找到的第一個(gè)格式。
class Textarea
文本區(qū)域:<textarea>...</textarea>
class CheckboxInput
復(fù)選框:<input type='checkbox' ...>
接受一個(gè)可選的參數(shù):
check_test
一個(gè)可調(diào)用的對(duì)象,接收CheckboxInput 的值并如果復(fù)選框應(yīng)該勾上返回True。
class Select
Select widget:<select><option ...>...</select>
choices
當(dāng)表單字段沒(méi)有choices 屬性時(shí),該屬性是隨意的。如果字段有choice 屬性,當(dāng)字段的該屬性更新時(shí),它將覆蓋你在這里的任何設(shè)置。
class NullBooleanSelect
Select Widget,選項(xiàng)為‘Unknown’、‘Yes’ 和‘No’。
class SelectMultiple
類(lèi)似Select,但是允許多個(gè)選擇:<select multiple='multiple'>...</select>
class RadioSelect
類(lèi)似Select,但是渲染成<li> 標(biāo)簽中的一個(gè)單選按鈕列表:
<ul>
<li><input type='radio' name='...'></li>
...
</ul>
你可以迭代模板中的單選按鈕來(lái)更細(xì)致地控制生成的HTML。假設(shè)表單myform 具有一個(gè)字段beatles,它使用RadioSelect 作為Widget:
{% for radio in myform.beatles %}
<div class="myradio">
{{ radio }}
</div>
{% endfor %}
它將生成以下HTML:
<div class="myradio">
<label for="id_beatles_0"><input id="id_beatles_0" name="beatles" type="radio" value="john" /> John</label>
</div>
<div class="myradio">
<label for="id_beatles_1"><input id="id_beatles_1" name="beatles" type="radio" value="paul" /> Paul</label>
</div>
<div class="myradio">
<label for="id_beatles_2"><input id="id_beatles_2" name="beatles" type="radio" value="george" /> George</label>
</div>
<div class="myradio">
<label for="id_beatles_3"><input id="id_beatles_3" name="beatles" type="radio" value="ringo" /> Ringo</label>
</div>
這包括<label> 標(biāo)簽。你可以使用單選按鈕的tag、choice_label 和 id_for_label 屬性進(jìn)行更細(xì)的控制。例如,這個(gè)模板:
{% for radio in myform.beatles %}
<label for="{{ radio.id_for_label }}">
{{ radio.choice_label }}
<span class="radio">{{ radio.tag }}</span>
</label>
{% endfor %}
... 將生成下面的HTML:
<label for="id_beatles_0">
John
<span class="radio"><input id="id_beatles_0" name="beatles" type="radio" value="john" /></span>
</label>
<label for="id_beatles_1">
Paul
<span class="radio"><input id="id_beatles_1" name="beatles" type="radio" value="paul" /></span>
</label>
<label for="id_beatles_2">
George
<span class="radio"><input id="id_beatles_2" name="beatles" type="radio" value="george" /></span>
</label>
<label for="id_beatles_3">
Ringo
<span class="radio"><input id="id_beatles_3" name="beatles" type="radio" value="ringo" /></span>
</label>
如果你不迭代單選按鈕 —— 例如,你的模板只是簡(jiǎn)單地包含{{ myform.beatles }} —— 它們將以<ul> 中的<li> 標(biāo)簽輸出,就像上面一樣。
外層的<ul> 將帶有定義在Widget 上的id 屬性。
Changed in Django 1.7:
當(dāng)?shù)鷨芜x按鈕時(shí),label 和input 標(biāo)簽分別包含for 和id 屬性。每個(gè)單項(xiàng)按鈕具有一個(gè)id_for_label 屬性來(lái)輸出元素的ID。
class CheckboxSelectMultiple
類(lèi)似SelectMultiple,但是渲染成一個(gè)復(fù)選框列表:
<ul>
<li><input type='checkbox' name='...' ></li>
...
</ul>
外層的<ul> 具有定義在Widget 上的id 屬性。
類(lèi)似RadioSelect,你可以迭代列表的每個(gè)復(fù)選框。更多細(xì)節(jié)參見(jiàn)RadioSelect 的文檔。
Changed in Django 1.7:
當(dāng)?shù)鷨芜x按鈕時(shí),label 和input 標(biāo)簽分別包含for 和id 屬性。 每個(gè)單項(xiàng)按鈕具有一個(gè)id_for_label 屬性來(lái)輸出元素的ID。
class FileInput
文件上傳輸入:<input type='file' ...>
class ClearableFileInput
文件上傳輸入:<input type='file' ...>,帶有一個(gè)額外的復(fù)選框,如果該字段不是必選的且有初始的數(shù)據(jù),可以清除字段的值。
class MultipleHiddenInput
多個(gè)<input type='hidden' ...> Widget。
一個(gè)處理多個(gè)隱藏的Widget 的Widget,用于值為一個(gè)列表的字段。
choices
當(dāng)表單字段沒(méi)有choices 屬性時(shí),這個(gè)屬性是可選的。如果字段有choice 屬性,當(dāng)字段的該屬性更新時(shí),它將覆蓋你在這里的任何設(shè)置。
class SplitDateTimeWidget
封裝(使用MultiWidget)兩個(gè)Widget:DateInput 用于日期,TimeInput 用于時(shí)間。
SplitDateTimeWidget 有兩個(gè)可選的屬性:
date_format
類(lèi)似DateInput.format
time_format
類(lèi)似TimeInput.format
class SplitHiddenDateTimeWidget
類(lèi)似SplitDateTimeWidget,但是日期和時(shí)間都使用HiddenInput。
class SelectDateWidget[source]
封裝三個(gè)Select Widget:分別用于年、月、日。注意,這個(gè)Widget 與標(biāo)準(zhǔn)的Widget 位于不同的文件中。
接收一個(gè)可選的參數(shù):
years
一個(gè)可選的列表/元組,用于”年“選擇框。默認(rèn)為包含當(dāng)前年份和未來(lái)9年的一個(gè)列表。
months
New in Django 1.7.
一個(gè)可選的字典,用于”月“選擇框。
字典的鍵對(duì)應(yīng)于月份的數(shù)字(從1開(kāi)始),值為顯示出來(lái)的月份:
MONTHS = {
1:_('jan'), 2:_('feb'), 3:_('mar'), 4:_('apr'),
5:_('may'), 6:_('jun'), 7:_('jul'), 8:_('aug'),
9:_('sep'), 10:_('oct'), 11:_('nov'), 12:_('dec')
}
empty_label
New in Django 1.8.
如果DateField 不是必選的,SelectDateWidget 將有一個(gè)空的選項(xiàng)位于選項(xiàng)的頂部(默認(rèn)為---)。你可以通過(guò)empty_label 屬性修改這個(gè)文本。empty_label 可以是一個(gè)字符串、列表 或元組。當(dāng)使用字符串時(shí),所有的選擇框都帶有這個(gè)空選項(xiàng)。如果empty_label 為具有3個(gè)字符串元素的列表 或元組,每個(gè)選擇框?qū)⒕哂兴鼈冏远x的空選項(xiàng)??者x項(xiàng)應(yīng)該按這個(gè)順序('year_label', 'month_label', 'day_label')。
# A custom empty label with string
field1 = forms.DateField(widget=SelectDateWidget(empty_label="Nothing"))
# A custom empty label with tuple
field1 = forms.DateField(widget=SelectDateWidget(
empty_label=("Choose Year", "Choose Month", "Choose Day"))
譯者:Django 文檔協(xié)作翻譯小組,原文:Built-in widgets。
本文以 CC BY-NC-SA 3.0 協(xié)議發(fā)布,轉(zhuǎn)載請(qǐng)保留作者署名和文章出處。
Django 文檔協(xié)作翻譯小組人手緊缺,有興趣的朋友可以加入我們,完全公益性質(zhì)。交流群:467338606。