DEV Community

Cover image for Sveltekit การทำงานกับ remote function [Part 3]
Nutchapon Makelai
Nutchapon Makelai

Posted on

Sveltekit การทำงานกับ remote function [Part 3]

สวัสดีครับเพื่อนๆ! 👋 กลับมาเจอกันอีกแล้วในซีรีส์ 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}`);
    }
);
Enter fullscreen mode Exit fullscreen mode

และในทางกลับกันเจ้าฟังก์ชัน 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>
Enter fullscreen mode Exit fullscreen mode

ซึ่งใช่แล้วครับ เจ้าตัวฟังก์ชัน 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>
Enter fullscreen mode Exit fullscreen mode

ซึ่งเจ้าพวก 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) => { /* ... */ });
Enter fullscreen mode Exit fullscreen mode

พวกเราก็จะมีฟอร์มที่หน้าตาประมาณนี้

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>
Enter fullscreen mode Exit fullscreen mode

และเนื่องจากฟอร์มของเราในรอบนี้ดันมีช่องให้อัปโหลดไฟล์ (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']);
Enter fullscreen mode Exit fullscreen mode

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) => { /* ... */ },
);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

หรือจะเขียนอีกแบบโดยใช้ 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>
Enter fullscreen mode Exit fullscreen mode

และก็เหมือนกับกรณีของช่อง 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)