# 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`更新,如下: ![](https://i.imgur.com/lCFSHHz.png) ```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`,主要將表單內所有的超連結一次性的註冊 ### 結果 實作結果如下圖: ![](https://i.imgur.com/B4Saqyo.png) 我們發現到每一個欄位都變成一個超連結,點擊之後會開一小窗<sub>(這取決於你使用的模式)</sub>,如下: ![](https://i.imgur.com/F37lIJ7.png) 在編輯之後再點擊藍色小按鈕就可以無跳頁更新,這對使用者感受會有不錯的加分效果。 ## 結論 很抱歉這次的範例是利用自己寫的記帳網頁來做說明,這可能造成在閱讀的時候不是那麼直觀,但是相信有前面打下來的基礎,一定可以很快的上手,瞭解如何實作`inplace-edit`。