# Flask實作_ext_21_Flask-XEditable
###### tags: `flask` `flask_ext` `python` `xeditable`
:::danger
相關資源:
* [X-editable](https://vitalets.github.io/x-editable/)
* [X-editable_Demo](https://vitalets.github.io/x-editable/demo-bs3.html)
* [X-editable + Bootstrap 4](https://github.com/Talv/x-editable/tree/develop/dist/bootstrap4-editable)
:::
**這並不是Flask的擴展,屬前端的工具搭配應用**
有些時候,一些基本資料的編輯我們並不想要再產生一個新的表單來處理,希望可以利用`inplace-edit`的方式來直接編輯,但這必需透過前端的幫助才有辦法做的到,一起看看如何利用現在的簡便工具`X-editable`來完成這個需求。
## 說明
```
This library allows you to create editable elements on your page. It can be used with any engine (bootstrap, jquery-ui, jquery only) and includes both popup and inline modes. Please try out demo to see how it works.
```
`X-editable`可以搭配`jquery, bootstrap, jquery-ui`一起使用,而我們還要再搭配`Flask_wtf`一起使用,並且如果您需求搭配`Bootstrap 4`的話,可以另外下載相對應的文件。<sub>(見相關資源連結)</sub>
## 範例
範例取自個人自己寫的記帳網頁片段,因此看了可能會覺得混亂,但是精神抓的到相信可以應用在自己的需求上,敬請見諒。
### 設置`widgets`
實作上我們會利用`flask_wtf`的`widgets`來設置表單屬性,再利用該`widgets`來建立表單,見註解說明。
```python
from wtforms.widgets import HTMLString, html_params
class WidgetError(Exception):
"""方便debug,設置一個Exception"""
pass
class XEditableWidget:
def __call__(self, field, **kwargs):
# 從FieldList取得Field,並且建立x-editable連結
subfield = field.pop_entry()
value = kwargs.pop("value", "")
kwargs.setdefault('data-role', 'x-editable')
# 判斷是否存在url,如不存在就拋出異常
# 該url主要是這個ajax的目標網址
if not kwargs.get('url'):
raise WidgetError('url required')
kwargs['data-url'] = kwargs.pop("url")
kwargs.setdefault('id', field.id)
kwargs.setdefault('name', field.name)
kwargs.setdefault('href', '#')
# pk value,ajax更新資料的時候用的到
if not kwargs.get('pk'):
raise WidgetError('pk required')
kwargs['data-pk'] = kwargs.pop("pk")
# 判斷欄位格式,如有不支援的就拋出異常
if isinstance(subfield, StringField):
kwargs['data-type'] = 'text'
elif isinstance(subfield, BooleanField):
kwargs['data-type'] = 'select'
elif isinstance(subfield, RadioField):
kwargs['data-type'] = 'select'
elif isinstance(subfield, SelectField):
kwargs['data-type'] = 'select'
elif isinstance(subfield, DateField):
kwargs['data-type'] = 'date'
elif isinstance(subfield, DateTimeField):
kwargs['data-type'] = 'datetime'
elif isinstance(subfield, IntegerField):
kwargs['data-type'] = 'number'
elif isinstance(subfield, TextAreaField):
kwargs['data-type'] = 'textarea'
else:
raise WidgetError('Unsupported field type: %s' % (type(subfield),))
return HTMLString('<a %s>%s</a>' % (html_params(**kwargs), value))
```
### 設置表單`Form`
我們利用`FieldList`來建立表單,注意到`min_entries`設置為1,因為`x-editable`最少需要一個`entries`
```python
class XEditableFormItems(FlaskForm):
"""建立項目清單_for x-editable"""
# 務必設置min_entries=1, 這是 x-editable的最低需求
item_category = FieldList(StringField(
), widget=XEditableWidget(), min_entries=1)
item_name = FieldList(StringField(
), widget=XEditableWidget(), min_entries=1)
item_remark = FieldList(StringField(
), widget=XEditableWidget(), min_entries=1)
```
配合你欄位的實際格式,寫入`FieldList`,並且指定`widget`為稍早所設置的`XEditableWidget`,範例中設置的格式皆為`StringField`
### 設置路由`Route, ViewFunction`
路由會分為顯示以及編輯,編輯的路由我們會另外搭配`ajax`實作即時更新,首先是顯示項目清單的路由:
```python=
@daily_cost_bp.route('/items/cr/', methods=['GET'])
def items_cr():
"""建立項目清單
function:
單純表單呈現,實際寫入路由寫至其它地方
寫入花費項目的部份寫入`items_c`
更新花費項目的部份寫入'items_u'
"""
form_items = FormItems()
form_category = FormItemCategory()
items = Items.query.order_by(Items.item_category).all()
item_number = len(items)
xform = XEditableFormItems()
for i in range(item_number):
xform.item_name.append_entry()
xform.item_category.append_entry()
xform.item_remark.append_entry()
return render_template('daily/items_cu.html',
form_items=form_items,
items=items,
xform=xform,
form_category=form_category)
```
第14行:取得清單總筆數
第16行:產生與清單總筆數相對應的欄位數目
註:這部份是在下測試多次得到的成功方式,如果有更好的方式請指導
現在設置更新的路由,搭配照片說明,照片操作為點擊`item_category`更新,如下:

```python=
@daily_cost_bp.route('/items/u/', methods=['POST'])
@login_required
def items_u():
"""提供花費項目更新的路由"""
xform = XEditableFormItems(request.form)
if xform.validate_on_submit():
for x in xform:
if getattr(x, 'last_index', None):
items = Items.query.get(x.last_index)
setattr(items, x.name, x.data.pop())
db.session.commit()
return json.dumps({'success': True}), 200, {'ContentType': 'application/json'}
```
第1行:methods設置為`POST`
第5行:`request.form`內容`ImmutableMultiDict([('item_remark-7', 'i.e. 高速公路過路費 ')])`
第5行:`xform`內容`{'item_category': [''], 'item_name': [''], 'item_remark': ['i.e. 高速公路過路費 '], 'csrf_token': ''}`
第6行:迴圈所看的就是`item_category, item_name, item_remark`這三個欄位的資料
第7行:`x`是`<class 'wtforms.fields.core.FieldList'>`物件
第8行:`last_index`取得的就是`pk`值,僅於有修正的欄位會取得
第9行:利用`pk`值來取得資料
第10行:`x.name`就是欄位名稱,`x.data`就是這次編輯的內容
上面搭配個人所寫的記帳網頁為例說明,整個迴圈所做的事情就是取得該筆資料的`FieldList`內的所有欄位`StringField`,然後判斷有編輯的是那一個欄位,再將那個欄位的值更新,因此以畫面範例為例,迴圈會執行3+1次,1指的是判斷`csrf_token`。
### 前端控制
前端的部份,主要是利用`jquery`來實作,如下:
```htmlmixed=
{% extends "base.html" %}
{% from 'bootstrap/form.html' import render_form, render_field %}
{% block page_content %}
{# 移除部份不需要的程式碼 #}
<div>
<table class="table table-striped">
<thead>
<tr>
<td>item_category</td>
<td>item_name</td>
<td>item_remark</td>
</tr>
</thead>
<tbody id="mybody">
{% for item in items %}
<tr>
<td>{{ xform.item_category(pk=item.id, value=item.item_category, url=url_for('daily_cost.items_u')) }}</td>
<td>{{ xform.item_name(pk=item.id, value=item.item_name, url=url_for('daily_cost.items_u')) }}</td>
<td>{{ xform.item_remark(pk=item.id, value=item.item_remark, url=url_for('daily_cost.items_u')) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock page_content %}
{% block scripts %}
{{ super() }}
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap-editable.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-editable.css') }}" type="text/css">
<script>
var csrf_token = "{{ csrf_token() }}";
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrf_token);
}
}
});
$(document).ready(function () {
$('#mybody a').editable({
placement: 'bottom',
params: function (params) {
// make x-editable act like a normal form field
var newParams = {};
newParams[params.name + '-' + params.pk] = params.value;
return newParams;
}
});
});
</script>
{% endblock %}
```
第16行:利用迴圈渲染表單,並且在迴圈中綁定欄任pk值、資料、以及更新路由
第29、30行:引用需求的前端文件
第32~39行:`Flask_wtf`標準利用`ajax POST`資料至後端的方式,帶`csrf_token`,多一層保護。
第40~50行:實作`x-editable`,主要將表單內所有的超連結一次性的註冊
### 結果
實作結果如下圖:

我們發現到每一個欄位都變成一個超連結,點擊之後會開一小窗<sub>(這取決於你使用的模式)</sub>,如下:

在編輯之後再點擊藍色小按鈕就可以無跳頁更新,這對使用者感受會有不錯的加分效果。
## 結論
很抱歉這次的範例是利用自己寫的記帳網頁來做說明,這可能造成在閱讀的時候不是那麼直觀,但是相信有前面打下來的基礎,一定可以很快的上手,瞭解如何實作`inplace-edit`。