Try   HackMD

Flask實作_ext_21_Flask-XEditable

tags: flask flask_ext python xeditable

這並不是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的話,可以另外下載相對應的文件。(見相關資源連結)

範例

範例取自個人自己寫的記帳網頁片段,因此看了可能會覺得混亂,但是精神抓的到相信可以應用在自己的需求上,敬請見諒。

設置widgets

實作上我們會利用flask_wtfwidgets來設置表單屬性,再利用該widgets來建立表單,見註解說明。

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

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實作即時更新,首先是顯示項目清單的路由:

@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更新,如下:

@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來實作,如下:

{% 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,主要將表單內所有的超連結一次性的註冊

結果

實作結果如下圖:

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

在編輯之後再點擊藍色小按鈕就可以無跳頁更新,這對使用者感受會有不錯的加分效果。

結論

很抱歉這次的範例是利用自己寫的記帳網頁來做說明,這可能造成在閱讀的時候不是那麼直觀,但是相信有前面打下來的基礎,一定可以很快的上手,瞭解如何實作inplace-edit