Flask: จัดการกับ Form ให้ง่ายขึ้นด้วย Flask WTForm

Published on

Authored by Pete. Pittawat Taveekitworachai.


หากใครที่ได้เริ่มต้นใช้ Flask ร่วมกับ Jinja ซึ่งเป็น Template Engine และต้องจัดการกับ Form คงคุ้นเคยกับ request.form.get("name") อะไรประมาณนี้กันเป็นอย่างดี เพื่อเข้าถึงข้อมูลที่ส่งมาจาก Front-end แน่นอนว่า เมื่อได้รับข้อมูลมาแล้วเราจะต้องมีการ Validate หรือยืนยันว่าข้อมูลเป็นไปตามรูปแบบที่กำหนดไว้หรือไม่อีกครั้ง ก่อนนำข้อมูลไปจัดการต่อ หากเขียนเป็นขั้นตอนคร่าว ๆ เราจะได้ว่า

  1. สร้าง Form ในฝั่ง Front-end เช่น
    <!-- ... -->
    <form action="{{ url_for('parks') }}" method="POST">
    	<label for="name">Your name</label>
    	<input name="name" type="text">
    </form>
    <!-- ... -->

parks.html

  1. สร้าง Endpoint ใน Flask
    # ...
    @app.route("/parks", methods=["GET", "POST"])
    def parks():
    	if request.method == "POST":
        	name = request.form.get("name")
        	# Process data
    # ...

main.py

แต่จะเห็นได้ว่าในการสร้าง Endpoint นั้นเรามักจะต้องทำ Pattern ซ้ำ ๆ คือการ Validate ข้อมูลก่อนนำไปใช้ต่อ และอีกสิ่งหนึ่งที่มักนำไปสู่ Error ก็คือการใช้ String ที่เป็นชื่อในการดึงข้อมูลออกมา จะดีกว่ามั้ยถ้าเราสามารถทำสิ่งต่าง ๆ เหล่านี้ให้ดีขึ้นได้ (แอบบอกไว้ตรงนี้เลยว่า ท้ายบทความเรามีโบนัสเล็ก ๆ สำหรับคนที่ใช้ Bootstrap คู่กับ Flask อีกด้วย


Flask WTForm

ปฏิเสธไม่ได้ว่า Form เป็นหนึ่งในเรื่องปวดหัว และ Pain มาก ๆในการเขียนโปรแกรม ไม่ว่าจะเป็นการทำอะไรซ้ำไปซ้ำมา เพื่อดึงข้อมูล ความเยอะของ Form ไหนจะต้องจัดการเรื่องของ Validation อีก นั่นทำให้เราเห็นได้ว่าในหลาย ๆ Programming Language/Frameworks/Libraries มักจะนำเสนอ Solution ต่าง ๆ ไม่ว่าจะโดย Owner เองหรือว่า Community ออกมา

ใน Flask เองก็เช่นกัน เรามี Flask WTForm ที่จะช่วยให้การจัดการกับ Form ทำได้ง่ายยิ่งขึ้น ด้วยการสร้าง Class ขึ้นมารวมข้อมูลที่เกี่ยวข้องไว้ด้วยกัน รวมถึงการที่มาพร้อมกับ Built-in Validators ที่ทำให้เราเรียกใช้ได้ง่ายอีกด้วย

ขั้นตอนแรกก็คือการ Install เข้าไปใน Project ของเราก่อนด้วย

    pip install Flask-WTF

จากนั้นจึง Import เข้ามาในโปรเจค

    from flask_wtf import FlaskForm # 1.
    from wtforms import StringField # 2.
    from wtforms.validators import DataRequired # 3.

main.py

  1. โดยบรรทัดแรกเราทำการ Import FlaskForm ซึ่งจะต้องเป็น Base Class สำหรับ Extend เพื่อให้ Class ของ Form ที่เรากำลังจะสร้างขึ้นได้ความสามารถต่าง ๆ มา
  2. บรรทัดที่ 2 เป็นการ Import ประเภทของ Input Field เข้ามา ซึ่งมีด้วยกันหลากหลายประเภท ดูเพิ่มเติมได้ที่ Fields
  3. บรรทัดที่ 3 เป็นการ Import Validators ที่ Built-in มากับ WTForm สามารถดูเพิ่มเติมได้ที่ Validators

หลังจากที่ Import เรียบร้อยแล้วเราต้องทำการตั้ง secrey_key ให้กับ Flask Application ของเรา เนื่องจาก Flask WTF นั้นมี Built-in สำหรับจัดการ CSRF Token มาให้ด้วย เพื่อให้เราสามารถสร้าง Form ที่ปลอดภัยได้มากยิ่งขึ้น

    app = Flask(__name__)
    app.secret_key = "your-secret-key-should-not-be-here"

ขั้นต่อไป ทุก ๆ ครั้งที่เราต้องการสร้าง Form ขึ้นมา เราจะทำการสร้าง Class สำหรับ Form นั้น ๆ

    class SimpleForm(FlaskForm): # 1.
    	name = StringField(label="Your name", validators=[DataRequired()] # 2.

main.py

  1. สำหรับ Class ที่ต้องการจะประกาศว่าเป็น Class สำหรับการจัดการข้อมูลที่เกี่ยวกับ Form นั้น ๆ จะต้องมีการ Extends FlaskForm เสมอ
  2. ในที่นี้ คือการสร้าง Input Field เก็บไว้ในตัวแปร name ซึ่งจะถูกใช้สำหรับเข้าถึงข้อมูลต่อไป โดย name เป็นประเภท StringField ซึ่งเป็น Field สำหรับข้อความ มีการระบุ Label ว่า "Your name" ซึ่งจะถูกนำไปใช้ใน <label></label> และมีการเพิ่ม Validators โดยที่ตำแหน่งที่ 0 เป็น DataRequired() หมายถึงว่า Field นี้จะต้องมีข้อมูลกลับมาด้วยเสมอ

หลังจากที่เราสร้าง Class สำหรับ Form ของเราเรียบร้อยแล้ว ขั้นถัดไปคือการส่ง Form นี้ไปให้ Front-end แสดงผล และจัดการกับข้อมูลที่ได้รับกลับมา โดยเราอาจเขียน Endpoint ของเราได้แบบนี้ ข้อสังเกตเล็กน้อยคือ Endpoint ที่เราสร้างขึ้นมานั้นจำเป็นต้องรองรับ Methods POST สำหรับการส่งค่า Form กลับมา

    @app.route("/parks", methods=["GET", "POST"])
    def parks():
    	simple_form = SimpleForm()
        
        # ...
        
        return render_template("parks.html", form=simple_form)

main.py

และในฝั่งของ Jinja สามารถ Access เข้า Form เพื่อนำมาแสดงผลได้ดังนี้ โดยตั้ง novalidate ไว้เพื่อให้เห็นการทำงานของ Flask WTForm ได้ชัดเจนยิ่งขึ้น

    <!-- ... -->
    <form action="{{ url_for('parks') }}" method="POST" novalidate>
    	{{ form.csrf_token }}
    	{{ form.name.label }} {{ form.name(size=20) }}
        <input type="submit">
    </form>
    <!-- ... -->

parks.html

ขั้นถัดไปเราจะนำข้อมูลออกมาใช้ โดยใน Function parks() เราสามารถจัดการกับ Form ได้ผ่าน simple_form ที่เราสร้างขึ้นมา

    @app.route("/parks", methods=["GET", "POST"])
    def parks():
        simple_form = SimpleForm()
        
        # -- ADD --
    
        if simple_form.validate_on_submit():
            name = simple_form.name.data
            print(name)
            # Do something with name
            return redirect(url_for('home'))
        else:
            print("Name is required")
            # OR send some error message to front-end
            
        # -- -- --
    
        return render_template("parks.html", form=simple_form)

สามารถดูตัวอย่างโค้ดแบบเต็ม ๆ ได้ที่ GitHub


Bonus! สำหรับผู้ที่ใช้ Flask Bootstrap

สำหรับใครที่ใช้ Bootstrap ในโปรเจคคู่ไปกับ Flask ก็คงมีโอกาสได้ใช้งาน Flask-Bootstrap ซึ่งต้องบอกว่าเจ้า Package ตัวนี้เกิดมาคู่กับ Flask WTForm จริง ๆ แน่นอว่าถึงเราจะมี Class ที่ Inherit มาจาก FlaskForm มาช่วยเราแล้ว แต่ชีวิตเองก็ไม่ได้ง่ายดายไปซะทั้งหมด เพราะว่า Front-end เรายังคงต้องสร้าง Input ขึ้นมาเองแบบซ้ำ ๆ หรือเราอาจจะเลี่ยงไปใช้ Loop ก็ทำให้โค้ดเราซับซ้อนขึ้นโดยไม่จำเป็น

โดยในไฟล์ Jinja Template parks.html ของเราสามารถที่จะสั่ง Import เพิ่มเติม

    {% import 'bootstrap/wtf.html' as wtf %}

parks.html และเราสามารถแทนที่โค้ดในส่วนนี้

    <form action="{{ url_for('parks') }}" method="POST" novalidate>
    	{{ form.csrf_token }}
    	{{ form.name.label }} {{ form.name(size=20) }}
        <input type="submit">
    </form>

ด้วย

    {{ wtf.quick_form(form) }}

ซึ่งข้อแนะนำเพิ่มเติมคือพอเป็นแบบนี้แล้วปุ่ม Submit ของเราจะหายไป เราสามารถแก้ไขได้ด้วยการใช้ SubmitField เริ่มจากการ Import เข้ามาเพิ่มเติม

    from wtforms import StringField, SubmitField

และเพิ่มให้กับ Class SimpleForm ของเรา

    class SimpleForm(FlaskForm):
    	name = StringField(label="Your name", validators=[DataRequired()]
        submit = SubmitField(label="Submit")

ก็จะทำให้เราได้ปุ่ม Submit ของเรากลับมา

สามารถดูโค้ดแบบเต็ม ๆ ได้ที่ GitHub


📚 Hope you enjoy reading! 📚