สวัสดีครับเพื่อนๆ! 👋 กลับมาเจอกันอีกแล้วในซีรีส์ SvelteKit Remote function! จากบทความก่อนๆ ที่เราลุยตระกูล query กันไปแบบจัดเต็ม ทั้งดึงข้อมูลธรรมดา มัดรวมรีเควสต์ (batch) และดึงแบบเรียลไทม์ (live) หวังว่าเพื่อนๆ คงจะสนุกกับการ "ดึง" ข้อมูลกันไปแล้วนะครับ 😎
แต่วันนี้... เราจะมาเปลี่ยนโหมดกันบ้าง! เพราะเมื่อมี "ดึง" ก็ต้องมี "ดัน"!! เอยย "ส่ง" ถูกไหมครับ? 😅 ถ้าเราอยากจะส่งข้อมูล เพื่อบันทึก (POST) หรืออัปเดต (PUT) อะไรบางอย่างกลับไปที่ Server ผ่าน FormData ล่ะจะต้องทำยังไง?
ขอต้อนรับทุกคนเข้าสู่โลกของ Remote function ประเภท form 📝 เจ้าตัวนี้เกิดมาเพื่อช่วยเราจัดการกับการส่งข้อมูลโดยเฉพาะ (Mutation) โดยในบทความนี้ เราจะมาค่อยๆ ปูพื้นฐานทำความเข้าใจกันตั้งแต่เรื่องการจัดการช่องรับข้อมูล (Fields) กันก่อนสำหรับบทความนี้ และในบทความถัดๆไปจะเล่าถึงการ Validate ข้อมูลใน form, การนำค่าใน form มาแสดงผล รวมถึงการจัดการกับข้อมูลที่ถูกส่งกลับมาหรือการเปลี่ยนเส้นทางไปยังส่วนอื่นๆของเว็บ (Returns and redirects) กันอีกทีนะครับ
เอาละ!! พร้อมที่จะบอกลาความยุ่งยากของการจัดการฟอร์มแบบเดิมๆ แล้วหรือยังครับ? ถ้าพร้อมแล้ว เตรียมนิ้วให้พร้อมโค้ดแล้วลุยกันเลย! 🚀
📄form
ด้วยฟังก์ชัน form ของเราอย่างที่บอกมันเกิดมาเพื่อจัดการกับการส่งข้อมูลไปยัง server โดยเฉพาะซึ่งโดยหลักแล้ว ตัว callback function ที่มันรับมาจะมี parameter ที่รับค่าจาก form ที่ส่งเข้ามาเป็นประเภทข้อมูล FormData
src/routes/blog/data.remote.js
import * as v from 'valibot';
import { error, redirect } from '@sveltejs/kit';
import { form } from '$app/server';
import * as db from '$lib/server/database';
import * as auth from '$lib/server/auth';
export const createPost = form(
// ส่วนของ Validation schema
v.object({
title: "v.pipe(v.string(), v.nonEmpty()),"
content:v.pipe(v.string(), v.nonEmpty())
}),
//Callback function ที่รับค่า data ที่เป็น FormData เข้ามา
async ({ title, content }) => {
// เช็ค auth เรื่องของ User ว่าถูกต้องไหม
const user = await auth.getUser();
if (!user) error(401, 'Unauthorized');
const slug = title.toLowerCase().replace(/ /g, '-');
// ทำการ Insert เข้าไปยัง Database
await db.sql`
INSERT INTO post (slug, title, content)
VALUES (${slug}, ${title}, ${content})
`;
// ทำการ Redirect ผู้ใช้ไปยังหน้า blog ใหม่ที่ถูกสร้าง
redirect(303, `/blog/${slug}`);
}
);
และในทางกลับกันเจ้าฟังก์ชัน form ก็จะคืนค่า object ที่เราสามารถนำไป spread ใส่เข้าไปยัง <form> element ในส่วนของ client ได้ด้วย ซึ่งเจ้าตัว Callback function ด้านบนนั้นจะทำงานก็ต่อเมื่อเกิด event ประเภท form submit เท่านั้น
src/routes/blog/new/+page.svelte
<script>
import { createPost } from '../data.remote';
</script>
<h1>Create a new post</h1>
<!-- vvvvvvvvvvv สามารถนำ `form` มา spread ได้เลย -->
<form {...createPost}>
<!-- คอนเท้นต์ของ form ก็ใส่เข้ามาตรงส่วนนี้ -->
<button>Publish!</button>
</form>
ซึ่งใช่แล้วครับ เจ้าตัวฟังก์ชัน form ที่ถูกส่งกลับมานั้นมันเป็น object ที่จะมี properties ของ method รวมถึง action อยู่ด้วยซึ่งนั้นสามารถทำให้การส่งข้อมูลฟอร์ม ของเราจะทำงานได้แม้ว่าเราจะไม่มีสามารถเข้าถึง JavaScript ได้นั้นเอง ที่มากไปกว่านั้นมันยังมี attachment ที่จะทำให้ฟอร์มของเรามีความสามารถแบบ progressively enhances อยู่ด้วย ซึ่งเจ้านี้แหละมันเหมือนเวทมนต์เลยก็ว่าได้ เพราะว่าเมื่อไหร่ที่เราสามารถเข้าถึง JavaScript ได้ การส่งข้อมูลฟอร์มของเราจะป้องกันและไม่ทำให้หน้าเว็บของเราเกิดการโหลดใหม่ซ้ำ (reload)
และเหมือนกับฟังก์ชัน query ที่เคยกล่าวไว้ในบทความก่อน ๆ นั้น เมื่อไหร่ที่ callback function ของเราทำการรับค่าข้อมูลเข้าไปกับมันด้วย เราจะต้องทำการ Validate ข้อมูลนั้น ซึ่งนั้นก็สามารถทำได้ง่ายๆเช่นกัน โดยการใส่ Validation schema ที่เป็น Standard Schema ให้เป็น argument ตัวแรกของเจ้าฟังก์ชัน form
📊Fields
โดยตัวฟอร์มของเราเนี่ย จะประกอบไปด้วยช่องกรอกข้อมูล (fields) หลายๆ ช่องรวมกัน ซึ่งหน้าตาและประเภทของช่องพวกนี้จะถูกกำหนดโดยตัว schema ที่เราตั้งไว้นั่นเองครับ 📝
อย่างในกรณีของฟังก์ชัน createPost เราก็จะมีอยู่ 2 ฟิลด์หลักๆ คือ title (หัวข้อ) และ content (เนื้อหา) ซึ่งข้อมูลทั้งสองตัวนี้ก็จะเป็นประเภทข้อความ (strings) ทั้งคู่เลย ✍️
ทีนี้ เวลาที่เราอยากจะดึงพวก attributes ต่างๆ ของแต่ละฟิลด์ออกมาแปะใช้งาน เราก็แค่เรียกใช้ method ที่ชื่อว่า .as(...) แล้วระบุประเภทของช่อง input ที่เราอยากจะใช้งานลงไปได้เลย 🛠️
และสำหรับช่อง input ส่วนใหญ่นั้น เรายังสามารถแอบส่งค่าที่ 2 (second argument) พ่วงเข้าไปเพิ่มได้ด้วยนะ หน้าตาจะเป็นแบบนี้เลย 👉 .as(type, value) ซึ่งเจ้านี่จะเอาไว้ใช้สำหรับควบคุมค่า (value) ที่จะถูกแสดงผลออกมาบนหน้าเว็บนั่นเองครับ: ✨
src/routes/blog/new/+page.svelte
<form {...createPost}>
<label>
<h2>Title</h2>
<input {...createPost.fields.title.as('text')} />
</label>
<label>
<h2>Write your post</h2>
<textarea {...createPost.fields.content.as('text')}></textarea>
</label>
<button>Publish!</button>
</form>
ซึ่งเจ้าพวก attribute เหล่านี้แหละที่จะเป็นตัวช่วยให้ SvelteKit รู้ว่าต้องตั้งค่าช่อง input เป็นประเภทไหน กำหนดชื่อ (name) สำหรับแพ็คข้อมูลส่งไปให้ handler จัดการต่อ และที่เจ๋งสุดคือมันช่วยเติมค่า (value) กลับลงไปในฟอร์มให้แบบอัตโนมัติด้วย! (อย่างเช่น เวลาผู้ใช้กด submit แล้วเกิด error กรอกข้อมูลไม่ผ่าน เขาจะได้ไม่ต้องมานั่งพิมพ์ใหม่ทั้งหมดให้เมื่อยมือยังไงล่ะ 😌) แถมยังช่วยเซ็ตสถานะ aria-invalid เพื่อบอกระบบได้ด้วยนะว่าช่องนี้ข้อมูลพังหรือกรอกผิด 🚨
และในส่วนการที่เราส่งค่าที่ 2 พ่วงเข้าไปใน .as(...) เนี่ย จะมีประโยชน์สุดๆ เวลาที่เราต้องเรนเดอร์ฟอร์มโดยดึงข้อมูลที่มีอยู่แล้วมาแสดงครับ อย่างเช่นตอนทำ "ฟอร์มแก้ไขข้อมูล" (edit form) หรือเวลาที่เราต้องเสกฟอร์มหน้าตาเหมือนกันออกมาหลายๆ อันผ่านลูป for(...) 🔄
ซึ่งนอกจากมันจะช่วยตั้งค่าเริ่มต้นตอนที่ฟอร์มถูกเรนเดอร์ขึ้นมาครั้งแรกแล้ว เจ้านี่ยังเป็นตัวคุมค่าของช่อง input นั้นๆ เวลาที่ฟอร์มถูกสั่ง reset (ล้างค่า) กลับไปเป็นค่าตั้งต้นอีกด้วยนะ 🧹
📝 จุดที่ต้องโน้ตไว้เพิ่มเติมคือ:
สำหรับ input ประเภท radio, submit และ hidden จะบังคับว่า ต้องใส่ ค่าที่ 2 นี้เสมอนะครับ
ส่วน checkbox จะต้องใส่ก็ต่อเมื่อมันทำหน้าที่เป็นหนึ่งในตัวเลือกของฟิลด์ข้อมูลแบบ array
และข้อควรระวังสุดท้ายคือ... เรา ไม่สามารถ ใช้วิธีนี้เติมค่าให้กับ input ประเภท file (อัปโหลดไฟล์) ได้นะครับ 📁❌
ตัว attribute
nameที่ระบบสร้างขึ้นมาให้ จะใช้รูปแบบการเขียนอ้างอิงแบบ JS object เลยครับ (หน้าตาก็ประมาณนี้nested.array[0].value)แต่มีข้อควรระวังนิดนึงคือ ตอนนี้มัน ยังไม่รองรับ พวก key ที่เป็น string แบบที่ต้องใช้เครื่องหมายคำพูด (quotes) ครอบนะครับ อย่างเช่น
object['nested-array'][0].valueแบบนี้จะยังเอามาใช้ไม่ได้เด้อ 🚫และถ้าเราแอบไปแงะดูการทำงานหลังบ้าน (Under the hood) ⚙️ สำหรับช่อง
inputประเภทติ๊กถูก (boolean checkbox) และช่องตัวเลข (number) ตัวแอตทริบิวต์ชื่อ (name) ของมันจะถูกแอบเติมคำนำหน้าด้วยb:และn:ตามลำดับครับถามว่าทำไปทำไม? ก็เพื่อเป็นการส่งซิก 🤫 กระซิบบอก SvelteKit ให้รู้ว่า "เฮ้ย! ช่วยแปลงค่าพวกนี้จาก
stringให้กลับมาเป็นbooleanหรือnumberตามที่มันควรจะเป็นให้หน่อยนะ" ก่อนที่ข้อมูลจะถูกส่งข้ามไปสู่ขั้นตอนการตรวจสอบ (validation) อีกทีนั่นเองครับ ระบบเขาคิดมาเผื่อแล้ว ✨
ซึ่งไอ้เจ้า Fields ทั้งหลายที่เราทำเนี่ยมันสามารถถูกซ้อนเข้าไปอยู่ใน object (nested object) หรือ array (nested array) ได้ ซึ่งค่า (value) ของมันก็สามารถเป็นได้ทั้งข้อความ (string) ตัวเลข (number) ค่า True/False (boolean) หรือจะเป็น object ของไฟล์ (File objects) ได้เช่นกัน เช่นตัวอย่างถ้าเรามีข้อมูล schema ดังนี้
data.remote.js
const datingProfile = v.object({
name: v.string(),
photo: v.file(),
info: v.object({
height: v.number(),
likesDogs: v.optional(v.boolean(), false)
}),
attributes: v.array(v.string())
});
export const createProfile = form(datingProfile, (data) => { /* ... */ });
พวกเราก็จะมีฟอร์มที่หน้าตาประมาณนี้
src/routes/profile/new/+page.svelte
<script>
import { createProfile } from './data.remote';
const { name, photo, info, attributes } = createProfile.fields;
</script>
<form {...createProfile} enctype="multipart/form-data">
<label>
<input {...name.as('text')} /> Name
</label>
<label>
<input {...photo.as('file')} /> Photo
</label>
<label>
<input {...info.height.as('number')} /> Height (cm)
</label>
<label>
<input {...info.likesDogs.as('checkbox')} /> I like dogs
</label>
<h2>My best attributes</h2>
<input {...attributes[0].as('text')} />
<input {...attributes[1].as('text')} />
<input {...attributes[2].as('text')} />
<button>submit</button>
</form>
และเนื่องจากฟอร์มของเราในรอบนี้ดันมีช่องให้อัปโหลดไฟล์ (file input) รวมอยู่ด้วย เราเลยต้องไม่ลืมแปะ attribute enctype="multipart/form-data" เข้าไปที่ตัวฟอร์มด้วยนะครับ ส่วนพวกค่าข้อมูลอย่าง info.height และ info.likesDogs ระบบก็จะจัดการแปลงร่าง (coerce) ให้กลับไปเป็นตัวเลข (number) และค่าความจริง (boolean) ให้เองตามลำดับเลย สบายใจได้! ✨
💡 แวะโน้ตไว้นิดนึงเรื่อง Checkbox:
ธรรมชาติของช่อง checkbox เนี่ย ถ้าผู้ใช้ไม่ได้ติ๊กเลือกไว้ ❌ ข้อมูลของมันจะ ไม่ถูกรวม ส่งไปกับก้อนFormDataที่ SvelteKit เอามาใช้ประกอบร่างข้อมูลนะครับ
เพราะฉะนั้น ในตอนที่เราตั้งตัว schema เราเลยต้องทำเผื่อไว้ โดยตั้งให้ค่านี้เป็นแบบ "มีหรือไม่มีก็ได้" (optional) ด้วยนะ
ถ้าใครใช้ Valibot ก็ให้เขียนแบบนี้:v.optional(v.boolean(), false)แทนที่จะใช้แค่v.boolean()เพียวๆ
ส่วนถ้าใครเป็นสาย Zod ก็จะเขียนหน้าตาประมาณนี้ครับ:z.coerce.boolean<boolean>()
และในกรณีที่พวกเราใช้งานช่อง input แบบกลุ่มอย่าง radio และ checkbox (ที่เป็นตัวเลือกหลายๆ อันภายใต้ฟิลด์หัวข้อเดียวกัน) กฎเหล็กก็คือ การบังคับต้องระบุค่า (value) เข้าไปเป็น argument ตัวที่สองในคำสั่ง .as(...) เสมอ แบบนี้เลย 👇
constants.js
export const operatingSystems = /** @type {const} */ (['windows', 'mac', 'linux']);
export const languages = /** @type {const} */ (['html', 'css', 'js']);
data.remote.js
import { operatingSystems, languages } from './constants';
export const survey = form(
v.object({
operatingSystem: v.picklist(operatingSystems),
languages: v.optional(v.array(v.picklist(languages)), []),
}),
(data) => { /* ... */ },
);
src/routes/coding/+page.svelte
<form {...survey}>
<h2>Which operating system do you use?</h2>
{#each operatingSystems as os}
<label>
<input {...survey.fields.operatingSystem.as('radio', os)}>
{os}
</label>
{/each}
<h2>Which languages do you write code in?</h2>
{#each languages as language}
<label>
<input {...survey.fields.languages.as('checkbox', language)}>
{language}
</label>
{/each}
<button>submit</button>
</form>
หรือจะเขียนอีกแบบโดยใช้ select หรือ select multiple ก็ได้นะ โค้ดหน้า +page.svelte ก็จะกลายเป็น
<form {...survey}>
<h2>Which operating system do you use?</h2>
<select {...survey.fields.operatingSystem.as('select')}>
{#each operatingSystems as os}
<option>{os}</option>
{/each}
</select>
<h2>Which languages do you write code in?</h2>
<select {...survey.fields.languages.as('select multiple')}>
{#each languages as language}
<option>{language}</option>
{/each}
</select>
<button>submit</button>
</form>
และก็เหมือนกับกรณีของช่อง
checkboxที่ไม่ได้ถูกติ๊กนั่นแหละครับ... ถ้าผู้ใช้ไม่ได้เลือกตัวเลือกอะไรมาเลย ข้อมูลที่วิ่งส่งมาก็จะหายวับและมีค่ากลายเป็น undefined ไปโดยปริยาย 👻ด้วยเหตุผลนี้เอง สำหรับฟิลด์อย่าง
languages(ที่อาจจะให้เลือกได้หลายภาษา) เราเลยต้องเขียนตัว schema เผื่อทางหนีทีไล่ไว้โดยใช้คำสั่งv.optional(v.array(...), [])(เพื่อให้มันแอบเซ็ตค่าเริ่มต้นเป็น array ว่างๆ [] ให้ถ้าไม่มีคนเลือก) แทนที่จะไปบังคับโหดๆ ว่าต้องมี array ส่งมาเสมอด้วยคำสั่งv.array(...)เพียวๆ ครับ 🛡️✨
เป็นยังไงกันบ้างครับเพื่อนๆ กับการใช้งาน form แบบฉบับ SvelteKit Remote function 🤩 ในส่วนของการจัดการฟิลด์ข้อมูลต่างๆ มันดูคลีนและสมูทขึ้นกว่าเดิม ไหมครับ? หรือมันเพิ่มความลำบากกว่าเดิมนะเนี่ย 😵💫😵💫
แต่เดี๋ยวก่อน... มหากาพย์เรื่องฟอร์มของเรายังไม่จบลงง่ายๆ แค่นี้ครับ! เพราะสิ่งที่เราทำกันไปวันนี้เป็นเพียงแค่จุดเริ่มต้นเท่านั้น
ถ้าใครกำลังมีคำถามในหัวว่า "แล้วถ้าอยากให้ฟอร์มมีการ Validate แบบ ทั้งฝั่ง Client ด้วยจะทำยังไงละ ?? แล้วจะเอาข้อมูลในฟอร์มมาแสดงได้ยังไง ??" 🤯
เก็บคำถามนั้นไว้ครับ! เพราะในบทความถัดไป เราจะมาจัดกันต่อในหัวข้อของการ Validate 🔍 รวมถึงการ Get/Set ข้อมูลจาก form และสุดท้ายก็จะเป็นเรื่องการนำค่าที่ถูกส่งกลับ (Return Data) มาใช้งานได้อย่างไรด้วย ↩️
รับรองว่าบทความหน้าจะช่วยปลดล็อกสกิลให้เพื่อนๆ จัดการฟอร์มบน SvelteKit ได้อยู่หมัดในทุกสถานการณ์แน่นอน! เตรียมตัวรอติดตามกันได้เลยครับ แล้วเจอกันใหม่นะเพื่อนๆ 😉✨
Top comments (0)