รับมือกับความผิดพลาด

9

แปลไปแล้ว

ในบทนี้ คุณจะได้

  • สร้างกลไกแสดงข้อผิดพลาดและข้อความต่างๆ
  • ตรวจสอบความถูกต้องของข้อมูลอย่างเข้มข้น
  • แสดงรายงานข้อผิดพลาดลงบนฟอร์ม
  • การใช้แค่ไดอะล็อกพื้นฐานอย่าง alert() เพื่อแสดงข้อความเตือนเมื่อเกิดปัญหากับข้อมูลที่ส่งเข้ามาทางฟอร์มไม่ใช่อะไรที่น่าพอใจนัก ที่สำคัญคือ มันไม่ได้ช่วยอะไรเรื่อง UX เลย ซึ่งพวกเราน่าจะทำได้ดีกว่านั้น

    โดยเราน่าจะสร้างกลไกรายงานข้อผิดพลาดขึ้นใหม่ให้ดีกว่าเดิม สามารถแจ้งเตือนผู้ใช้ได้ว่าเกิดอะไรขึ้น โดยไม่ขัดขวางการทำงานที่ดำเนินไปตามปกติ

    ซึ่งสิ่งที่เราจะสร้างนี้ก็คือ ระบบง่ายๆใช้แสดงข้อผิดพลาด ที่ด้านมุมขวาบนของหน้าจอ คล้ายๆกับแอพของ Mac ที่ชื่อ Growl

    รู้จักกับคอลเลกชั่นแบบโลคอล (Local Collections)

    ก่อนจะเริ่ม เราจำเป็นต้องสร้างคอลเลกชั่นเพื่อใช้เก็บข้อผิดพลาดซะก่อน โดยให้เก็บเฉพาะข้อผิดพลาดของเซสชั่นที่กำลังใช้งานอยู่เท่านั้น และไม่จำเป็นต้องจัดเก็บลงฐานข้อมูลด้วย โดยเราจะสร้างคอลเลกชั่นใหม่นี้ให้เป็น คอลเลกชั่นแบบโลคอล หมายความว่า คอลเลกชั่นนี้จะถูกเก็บไว้ที่เบราว์เซอร์เท่านั้น ไม่มีการส่งกลับไปที่เซิร์ฟเวอร์

    เพื่อให้เป็นไปตามนี้ เราก็จะสร้างคอลเลกชั่นของข้อผิดพลาดไว้ในโฟลเดอร์ client (เพื่อให้เรียกใช้จากฝั่งไคลเอนต์เท่านั้น) โดยระบุชื่อของคอลเลกชั่น MongoDB เป็น null (เนื่องจากเราจะไม่เก็บข้อมูลของคอลเลกชั่นนี้ลงฐานข้อมูลที่เซิร์ฟเวอร์เลย)

    // Local (client-only) collection
    Errors = new Mongo.Collection(null);
    
    client/helpers/errors.js

    เมื่อมีคอลเลกชั่นแล้ว เราก็สร้างฟังก์ชั่น throwError เพื่อใส่ข้อผิดพลาดเข้าไปในนั้น โดยเราไม่ต้องกังวลเรื่อง allow หรือ deny หรือความปลอดภัยอื่นๆ เนื่องจากคอลเลกชั่นนี้เป็นของผู้ใช้คนปัจจุบันเท่านั้น

    throwError = function(message) {
      Errors.insert({message: message})
    }
    
    client/helpers/errors.js

    ข้อดีของการนำคอลเลกชั่นแบบโลคอลมาใช้เก็บข้อผิดพลาด ที่เหมือนกับคอลเลกชั่นแบบอื่่นๆ ก็คือ ความเป็นรีแอคทีฟ ซึ่งหมายความว่า เราสามารถแสดงข้อความผิดพลาดได้ในแบบรีแอคทีฟเหมือนๆกับที่เราแสดงข้อมูลจากคอลเลกชั่นอื่นๆนั่นเอง

    แสดงข้อผิดพลาด

    เราจะแทรกข้อผิดพลาดไว้ที่ส่วนบนของไฟล์เลย์เอาท์ของเรา ดังนี้

    <template name="layout">
      <div class="container">
        {{> header}}
        {{> errors}}
        <div id="main">
          {{> yield}}
        </div>
      </div>
    </template>
    
    client/templates/application/layout.html

    และสร้างเทมเพลท errors และ error ในไฟล์ errors.html

    <template name="errors">
      <div class="errors">
        {{#each errors}}
          {{> error}}
        {{/each}}
      </div>
    </template>
    
    <template name="error">
      <div class="alert alert-danger" role="alert">
        <button type="button" class="close" data-dismiss="alert">&times;</button>
        {{message}}
      </div>
    </template>
    
    client/templates/includes/errors.html

    เทมเพลทคู่

    คุณอาจสังเกตุเห็นว่าเราได้ใส่เทมเพลทสองตัวในไฟล์เดียวกัน ที่ผ่านมาเราใช้แบบ “หนึ่งไฟล์ หนึ่งเทมเพลท” สำหรับ Meteor แล้ว การที่เราเอาเทมเพลททั้งหมดมารวมไว้ในไฟล์เดียวกันไม่ทำให้เกิดปัญหาอะไร (แต่มันอาจทำให้เราสับสนได้ ถ้าเอามารวมไว้ที่ main.html ไฟล์เดียว)

    ในกรณีนี้ เนื่องจากเทมเพลททั้งสองค่อนข้างสั้น เราเลยขอยกเว้นและนำมันมารวมไว้ที่ไฟล์เดียวกันเพื่อให้ไฟล์ที่เก็บไว้ทั้งหมดดูโล่งขึ้นอีกนิด

    ตอนนี้เราก็เหลือแค่สร้างตัวช่วยเทมเพลท จากนั้นเราก็พร้อมจะไปกันต่อ!

    Template.errors.helpers({
      errors: function() {
        return Errors.find();
      }
    });
    
    client/templates/includes/errors.js

    ถึงตรงนี้ คุณก็พร้อมที่จะทดสอบการแสดงข้อผิดพลาดนี้ด้วยตัวเองแล้ว แค่เปิดคอนโซลของเบราว์เซอร์และพิมพ์

    throwError("I'm an error!");
    
    Testing error messages.
    Testing error messages.

    ข้อผิดพลาดสองรูปแบบ

    เป็นเรื่องสำคัญที่เราต้องแยกแยะความแตกต่างระหว่างข้อผิดพลาดในระดับแอพ app-level และในระดับโค้ด code-level

    ข้อผิดพลาดในระดับแอพ โดยทั่วไปเกิดจากการทำงานของผู้ใช้ และผู้ใช้งานก็สามารถจัดการพวกมันได้ ที่เห็นได้ชัดคือ ข้อผิดพลาดจากการตรวจสอบ ข้อผิดพลาดจากสิทธิการใช้งาน ข้อผิดพลาดจาก not-found และอื่นๆ ซึ่งข้อผิดพลาดเหล่านี้เป็นสิ่งที่เราต้องแสดงต่อผู้ใช้ เพื่อช่วยให้พวกเค้าแก้ไขปัญหาที่กำลังเกิดขึ้นได้

    ข้อผิดพลาดในระดับโค้ดนั้น เป็นอีกเรื่องนึง ส่วนใหญ่เกิดมาจากบั๊กในโค้ดที่คุณเขียน คาดเดาไม่ได้ และคุณก็ ไม่อยาก จะแสดงให้ผู้ใช้เห็นโดยตรง แต่อาจจะแค่ต้องการติดตามมันด้วยบริการติดตามข้อผิดพลาดที่เปิดให้บริการอยู่ก็พอ (เช่นที่ Kadira )

    โดยในบทนี้เราจะเน้นที่ข้อผิดพลาดในระดับแอพเท่านั้น ไม่ใช่การไล่หาบั๊กแต่อย่างใด

    สร้างข้อผิดพลาด

    ตอนนี้เราก็รู้วิธีแสดงข้อผิดพลาดแล้ว แต่ก่อนที่เราจะเห็นมันเราก็ต้องทำให้มันเกิดขึ้นซะก่อน ซึ่งที่ผ่านมาเราได้เตรียมโค้ดรองรับเมื่อมีข้อผิดพลาดไว้ดีอยู่แล้ว เช่น การเตือนเมื่อข่าวที่โพสท์ซ้ำกัน ตอนนี้เราก็แค่เปลี่ยนฟังก์ชัน alert ในตัวช่วยเหตุการณ์ postSubmit ให้เป็นฟังก์ชัน throwError ดังนี้

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        };
    
        Meteor.call('postInsert', post, function(error, result) {
          // display the error to the user and abort
          if (error)
            return throwError(error.reason);
    
          // show this result but route anyway
          if (result.postExists)
            throwError('This link has already been posted');
    
          Router.go('postPage', {_id: result._id});  
        });
      }
    });
    
    client/templates/posts/post_submit.js

    จากนั้น เราก็จะทำเหมือนกันที่ตัวช่วยเหตุการณ์ postEdit

    Template.postEdit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var currentPostId = this._id;
    
        var postProperties = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        }
    
        Posts.update(currentPostId, {$set: postProperties}, function(error) {
          if (error) {
            // display the error to the user
            throwError(error.reason);
          } else {
            Router.go('postPage', {_id: currentPostId});
          }
        });
      },
      //...
    });
    
    client/templates/posts/post_edit.js

    คอมมิท 9-2

    Actually use the error reporting.

    มาทดสอบกันดูหน่อย ลองสร้างข่าวใหม่โดยป้อน URL เป็น http://meteor.com ให้ซ้ำกับข่าวเดิมที่สร้างไว้แล้ว คุณก็จะเห็นอะไรแบบนี้

    Triggering an error
    Triggering an error

    ลบข้อผิดพลาด

    คุณน่าจะสังเกตุเห็นว่า ข้อความผิดพลาดต่างๆจะเลือนหายไปเองในเวลาไม่กี่วินาที ทั้งนี้ก็เพราะความมหัศจรรย์ของ CSS ที่เราใส่ไว้ในสไตล์ชีตเมื่อตอนเริ่มต้นของหนังสือเล่มนี้

    @keyframes fadeOut {
      0% {opacity: 0;}
      10% {opacity: 1;}
      90% {opacity: 1;}
      100% {opacity: 0;}
    }
    
    //...
    
    .alert {
      animation: fadeOut 2700ms ease-in 0s 1 forwards;
      //...
    }
    
    client/stylesheets/style.css

    ที่เราทำคือ สร้างอนิเมชั่น fadeOut ใน CSS ให้มี 4 คีย์เฟรม โดยกำหนดค่าความทึบแสงให้แตกต่างกัน (ณ ตำแหน่ง 0%, 10%, 90%, และ 100% ของการเกิดอนิเมชั่น) และใช้อนิเมชั่นนี้กับคลาส .alert

    โดยอนิเมชั่นนี้จะใช้เวลาทั้งหมด 2700 มิลลิวินาที ด้วยค่าที่กำหนดคือ ใช้สูตรเวลาแบบ ease-in , รันแบบหน่วง 0 วินาที , รันหนึ่งครั้ง และให้แสดงที่คีย์เฟรมสุดท้ายหลังจากรันจบ

    อนิเมชั่นแบบไหนดี

    คุณอาจกำลังสงสัยว่าทำไมเราใช้อนิเมชั่นแบบ CSS (ซึ่งถูกกำหนดไว้ล่วงหน้าและอยู่นอกเหนือการควบคุมของแอพ) แทนที่จะใช้อนิเมชั่นที่ควบคุมจาก Meteor เอง

    ถึงแม้ Meteor จะรองรับการสร้างอนิเมชั่นได้หลากหลาย แต่เนื่องจากเราต้องการให้บทนี้เน้นที่ข้อผิดพลาด เราจึงเลือกที่จะใช้อนิเมชั่นแบบง่ายๆของ CSS และเก็บสิ่งที่น่าสนใจไว้ในบทอนิเมชั่นโดยเฉพาะ

    ดูเหมือนว่าจะใช้การได้แล้ว แต่ถ้าคุณลองทำให้เกิดข้อผิดพลาดหลายๆครั้ง (เช่น ป้อนค่าลิงก์ที่เหมือนๆกันซักสามครั้ง) คุณก็จะเห็นข้อผิดพลาดนั้นเลื่อนตำแหน่งลงมาเรื่อยๆ

    Stack overflow.
    Stack overflow.

    ที่เป็นแบบนี้ก็เพราะในขณะที่ตัว .alert ดูเลือนหายไป แต่มันยังคงอยู่ใน DOM ไม่ได้หายไปไหน ซึ่งเป็นเรื่องที่เราต้องแก้ไข

    สถานะการณ์แบบนี้จะเข้าทาง Meteor พอดี เนื่องจากคอลเลกชั่นข้อผิดพลาดเป็นแหล่งข้อมูลแบบรีแอคทีฟ สิ่งที่เราต้องทำเพื่อกำจัดข้อผิดพลาดเก่าก็แค่ลบมันออกจากคอลเลกชั่นเท่านั้น!

    โดยเราจะใช้คำสั่ง Meteor.setTimeout เพื่อกำหนดให้ฟังก์ชั่น callback ทำการลบข้อผิดพลาดออกหลังจากหมดเวลาที่ตั้งไว้ (ในกรณีนี้คือ 3000 มิลลิวินาที)

    Template.errors.helpers({
      errors: function() {
        return Errors.find();
      }
    });
    
    Template.error.onRendered(function() {
      var error = this.data;
      Meteor.setTimeout(function () {
        Errors.remove(error._id);
      }, 3000);
    });
    
    client/templates/includes/errors.js

    คอมมิท 9-3

    Clear errors after 3 seconds.

    ฟังก์ชัน callback ของเหตุการณ์ onRendered จะทำงานหลังจากที่เบราว์เซอร์ได้แสดงเทมเพลทแล้ว โดย this ในฟังก์ชัน callback คือ ตัวเทมเพลทที่กำลังใช้งานอยู่ และ this.data ก็คือข้อมูลที่ถูกแสดงนั่นเอง (ในกรณีนี้คือ ข้อผิดพลาด)

    ค้นหาความถูกต้อง

    จนถึงตรงนี้เรายังไม่ได้กำหนดวิธีการตรวจสอบหน้าฟอร์มของเราไว้เลย ซึ่งอย่างน้อยที่สุดที่เราควรทำก็คือ ให้ผู้ใช้ป้อนข่าวที่มีทั้ง URL และชื่อเข้ามา ดังนั้นเราก็ต้องทำให้แน่ใจว่าพวกเค้าจะทำอย่างนั้นได้

    โดยเราจะทำสองอย่างเพื่อเตือนให้ผู้ใช้รู้ว่ามีข้อมูลตรงไหนที่ขาดหายไป อย่างแรก เราจะใส่ CSS class ที่ div ตัวนอกของฟิลด์ที่มีปัญหา และอย่างที่สอง เราจะแสดงข้อความผิดพลาดที่มีประโยชน์ข้างใต้ฟิลด์นั้น

    เราเริ่มด้วยการเตรียมเทมเพลท postSubmit ให้รองรับตัวช่วยใหม่ตามนี้

    <template name="postSubmit">
      <form class="main form page">
        <div class="form-group {{errorClass 'url'}}">
          <label class="control-label" for="url">URL</label>
          <div class="controls">
              <input name="url" id="url" type="text" value="" placeholder="Your URL" class="form-control"/>
              <span class="help-block">{{errorMessage 'url'}}</span>
          </div>
        </div>
        <div class="form-group {{errorClass 'title'}}">
          <label class="control-label" for="title">Title</label>
          <div class="controls">
              <input name="title" id="title" type="text" value="" placeholder="Name your post" class="form-control"/>
              <span class="help-block">{{errorMessage 'title'}}</span>
          </div>
        </div>
        <input type="submit" value="Submit" class="btn btn-primary"/>
      </form>
    </template>
    
    client/templates/posts/post_submit.html

    สังเกตุด้วยว่าเราส่งค่าพารามิเตอร์ (url และ title ตามลำดับ) ไปที่ฟังก์ชั่นตัวช่วยแต่ละตัว โดยเรียกใช้ตัวช่วยตัวเดียวกันซ้ำสองครั้ง แต่เปลี่ยนการทำงานของมันตามค่าพารามิเตอร์ที่ส่งเข้าไป

    ได้เวลาสนุกกันแล้ว ตอนนี้เราจะลองเอาตัวช่วยพวกนี้มาใช้ให้เกิดประโยชน์กันดู

    เราจะใช้ เซสชั่น เพื่อเก็บค่าอ็อบเจกต์ postSubmitErrors ที่มีข้อความผิดพลาดอยู่ข้างใน เมื่อผู้ใช้เริ่มทำงานกับฟอร์ม อ็อบเจกต์นี้จะเปลี่ยนแปลงค่าไปและทำให้เกิดการอัพเดทที่หน้าจอแบบรีแอคทีฟ

    แรกสุด เราจะกำหนดค่าเริ่มต้นให้อ็อบเจกต์นี้เมื่อเทมเพลท postSubmit ถูกสร้างขึ้น เพื่อให้แน่ใจว่าผู้ใช้จะไม่เห็นข้อความผิดพลาดเดิมจากการใช้งานก่อนหน้านั้น

    จากนั้นเราจะสร้างตัวช่วยเทมเพลทขึ้นมาสองตัว ที่คอยตรวจดูค่าคุณสมบัติ field ของ Session.get('postSubmitErrors') (โดยที่ field เป็นได้ทั้ง url หรือ title ขึ้นอยู่กับว่าเราเรียกใช้ตัวช่วยเทมเพลทจากตรงไหน)

    โดยตัวช่วย errorMessage จะคืนค่าข้อความผิดพลาดมาให้ แต่ errorClass จะตรวจดูว่า มี ข้อความหรือไม่ และคืนค่า has-error ถ้าพบว่ามีข้อความอยู่

    Template.postSubmit.onCreated(function() {
      Session.set('postSubmitErrors', {});
    });
    
    Template.postSubmit.helpers({
      errorMessage: function(field) {
        return Session.get('postSubmitErrors')[field];
      },
      errorClass: function (field) {
        return !!Session.get('postSubmitErrors')[field] ? 'has-error' : '';
      }
    });
    
    //...
    
    client/templates/posts/post_submit.js

    คุณสามารถทดสอบตัวช่วยว่าทำงานถูกต้องหรือไม่ โดยเปิดคอนโซลของเบราว์เซอร์และป้อนโค้ดต่อไปนี้

    Session.set('postSubmitErrors', {title: 'Warning! Intruder detected. Now releasing robo-dogs.'});
    
    Browser console
    Red alert! Red alert!
    Red alert! Red alert!

    ขั้นตอนต่อไปคือ ผูกค่าของเซสชั่นอ็อบเจกต์ postSubmitErrors เข้ากับฟอร์ม

    ก่อนที่จะทำตรงนั้น ให้เราสร้างฟังก์ชันใหม่ validatePost ใน posts.js เพื่อใช้ตรวจดูอ็อบเจกต์ post และคืนอ็อบเจกต์ errors ที่ประกอบด้วยข้อความผิดพลาดที่เกิดขึ้น (โดยตั้งชื่อคีย์เป็น title หรือ url ตามชื่อฟิลด์ที่ไม่มีข้อมูล )

    //...
    
    validatePost = function (post) {
      var errors = {};
    
      if (!post.title)
        errors.title = "Please fill in a headline";
    
      if (!post.url)
        errors.url =  "Please fill in a URL";
    
      return errors;
    }
    
    //...
    
    lib/collections/posts.js

    ซึ่งเราจะเรียกใช้ฟังก์ชันนี้จากตัวช่วยเหตุการณ์ postSubmit

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        };
    
        var errors = validatePost(post);
        if (errors.title || errors.url)
          return Session.set('postSubmitErrors', errors);
    
        Meteor.call('postInsert', post, function(error, result) {
          // display the error to the user and abort
          if (error)
            return throwError(error.reason);
    
          // show this result but route anyway
          if (result.postExists)
            throwError('This link has already been posted');
    
          Router.go('postPage', {_id: result._id});  
        });
      }
    });
    
    client/templates/posts/post_submit.js

    ให้สังเกตุด้วยว่า ที่เราใช้ return ก็เพื่อยกเลิกการทำงานของตัวช่วยเมื่อเกิดความผิดพลาดขึ้น แต่ไม่ใช่เพราะเราต้องการคืนค่านี้ออกมา

    Caught red-handed.
    Caught red-handed.

    การตรวจสอบฝั่งเซิร์ฟเวอร์

    ดูเหมือนว่าเรายังทำไม่เสร็จซะทีเดียว เราได้ตรวจสอบว่ามีการป้อนข้อมูล URL และชื่อข่าวที่ฝั่ง ไคลเอนต์ แต่บนฝั่ง เซิร์ฟเวอร์ ล่ะ อาจมีใครบางคนพยายามป้อนข่าวแบบว่างๆ ด้วยการเรียกใช้เมธอด postInsert ผ่านคอนโซลของเบราว์เซอร์โดยตรงก็ได้

    ถึงแม้ว่าเราไม่จำเป็นต้องแสดงข้อความผิดพลาดบนเซิร์ฟเวอร์ เราก็ยังสามารถใช้ฟังก์ชัน validatePost ตัวเดิมได้ เว้นเสียแต่ว่า ครั้งนี้เราจะเรียกใช้มันจากในเมธอด postInsert ไม่ใช่แค่เรียกใช้จากตัวช่วยเหตุการณ์เท่านั้น

    Meteor.methods({
      postInsert: function(postAttributes) {
        check(this.userId, String);
        check(postAttributes, {
          title: String,
          url: String
        });
    
        var errors = validatePost(postAttributes);
        if (errors.title || errors.url)
          throw new Meteor.Error('invalid-post', "You must set a title and URL for your post");
    
        var postWithSameLink = Posts.findOne({url: postAttributes.url});
        if (postWithSameLink) {
          return {
            postExists: true,
            _id: postWithSameLink._id
          }
        }
    
        var user = Meteor.user();
        var post = _.extend(postAttributes, {
          userId: user._id, 
          author: user.username, 
          submitted: new Date()
        });
    
        var postId = Posts.insert(post);
    
        return {
          _id: postId
        };
      }
    });
    
    lib/collections/posts.js

    ย้ำอีกครั้งว่า ผู้ใช้งานผ่านหน้าจอปกติไม่ควรต้องเห็นข้อความ “You must set a title and URL for your post” นี้ เพราะมันจะปรากฎให้เห็นเฉพาะกับผู้ที่ใช้งานโดยไม่ผ่านหน้าจอปกติ แต่ใช้งานผ่านคอนโซลโดยตรงเท่านั้น

    ลองทดสอบกันดู โดยเปิดคอนโซลของเบราว์เซอร์ แล้วลองป้อนโพสท์ข่าวที่ไม่มี URL ตามนี้

    Meteor.call('postInsert', {url: '', title: 'No URL here!'});
    

    ถ้าเราทำทุกอย่างถูกต้อง คุณจะได้รับโค้ดข้อมูลที่ค่อนข้างเยอะกลับมาพร้อมด้วยข้อความ “You must set a title and URL for your post”

    คอมมิท 9-4

    Validate post contents on submission.

    ตรวจสอบเมื่อทำการแก้ไข

    ก่อนจะจบงาน เราจะใช้วิธีการตรวจสอบแบบเดียวกันนี้กับหน้าฟอร์ม edit ของเราเช่นกัน โดยโค้ดที่ได้จะดูคล้ายๆกัน ตัวแรกคือเทมเพลท

    <template name="postEdit">
      <form class="main form page">
        <div class="form-group {{errorClass 'url'}}">
          <label class="control-label" for="url">URL</label>
          <div class="controls">
              <input name="url" id="url" type="text" value="{{url}}" placeholder="Your URL" class="form-control"/>
              <span class="help-block">{{errorMessage 'url'}}</span>
          </div>
        </div>
        <div class="form-group {{errorClass 'title'}}">
          <label class="control-label" for="title">Title</label>
          <div class="controls">
              <input name="title" id="title" type="text" value="{{title}}" placeholder="Name your post" class="form-control"/>
              <span class="help-block">{{errorMessage 'title'}}</span>
          </div>
        </div>
        <input type="submit" value="Submit" class="btn btn-primary submit"/>
        <hr/>
        <a class="btn btn-danger delete" href="#">Delete post</a>
      </form>
    </template>
    
    client/templates/posts/post_edit.html

    ต่อมาก็ตัวช่วยเทมเพลท

    Template.postEdit.onCreated(function() {
      Session.set('postEditErrors', {});
    });
    
    Template.postEdit.helpers({
      errorMessage: function(field) {
        return Session.get('postEditErrors')[field];
      },
      errorClass: function (field) {
        return !!Session.get('postEditErrors')[field] ? 'has-error' : '';
      }
    });
    
    Template.postEdit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var currentPostId = this._id;
    
        var postProperties = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        }
    
        var errors = validatePost(postProperties);
        if (errors.title || errors.url)
          return Session.set('postEditErrors', errors);
    
        Posts.update(currentPostId, {$set: postProperties}, function(error) {
          if (error) {
            // display the error to the user
            throwError(error.reason);
          } else {
            Router.go('postPage', {_id: currentPostId});
          }
        });
      },
    
      'click .delete': function(e) {
        e.preventDefault();
    
        if (confirm("Delete this post?")) {
          var currentPostId = this._id;
          Posts.remove(currentPostId);
          Router.go('postsList');
        }
      }
    });
    
    client/templates/posts/post_edit.js

    ก็เหมือนกับที่เราทำกับฟอร์มสร้างข่าว เราต้องตรวจสอบความถูกต้องของข่าวที่เซิร์ฟเวอร์ด้วย ยกเว้นแต่ว่า เราไม่ได้ใช้เมธอดเพื่อแก้ไขข่าว แต่เรียกใช้ update โดยตรงจากไคลเอนต์ ถ้าคุณยังจำได้

    นั่นหมายความว่า เราจะต้องเพิ่มฟังก์ชัน callback แบบ deny ตัวใหม่เข้าไปแทน

    //...
    
    Posts.deny({
      update: function(userId, post, fieldNames, modifier) {
        var errors = validatePost(modifier.$set);
        return errors.title || errors.url;
      }
    });
    
    //...
    
    lib/collections/posts.js

    ให้สังเกตุว่าตัวแปร post ที่รับเข้ามาคือ ข่าว เดิม ซึ่งในกรณีนี้เราต้องการตรวจสอบความถูกต้องของการ อัพเดท เราถึงเรียกใช้ validatePost กับค่าคุณสมบัติ $set ของตัว modifier (เหมือนที่ใช้ใน Posts.update({$set: {title: ..., url: ...}}))

    ที่ใช้แบบนี้ได้ก็เพราะว่า ใน modifier.$set ประกอบด้วยtitle และ url เหมือนกับที่อ็อบเจกต์ post ทั้งตัวมี และยังหมายความได้อีกว่า การอัพเดทแค่ title หรือ url ตัวใดตัวหนึ่งเพียงตัวเดียว จะไม่สามารถเกิดขึ้นได้แน่นอน ซึ่งในการใช้งานจริงไม่น่ามีปัญหา

    คุณอาจสังเกตุเห็นว่า ฟังก์ชัน callback แบบ deny นี้เป็นตัวที่สอง เมื่อเราเพิ่มฟังก์ชัน callback แบบ denyเข้าไปหลายตัว การทำงานจะถูกยกเลิกเมื่อตัวใดตัวหนึ่งมีค่าเป็น true ซึ่งในกรณีนี้หมายความว่า update จะเกิดขึ้นได้กับฟิลด์ title และ url เท่านั้น โดยตัวใดตัวหนึ่งต้องไม่มีค่าว่างด้วย

    คอมมิท 9-5

    Validate post contents when editing.