Thông báo
9Percentage Translated
In this chapter, you will:
Việc đơn thuần sử dụng dialog alert()
của trình duyệt để cảnh báo người dùng khi có vấn đề với việc submit form thường không làm thoả mãn, và chắc chắn việc đó cũng không tạo ra UX tốt được. Chúng ta có thể làm tốt hơn.
Để thay thế, chúng ta sẽ xây dựng một cơ cấu thông báo lỗi linh hoạt hơn để thông báo cho người dùng điều gì đang diễn ra mà không phá hỏng luồng ứng dụng.
Chúng ta sẽ cài đặt một hệ thống đơn giản hiển thị lỗi ở góc trên bên phải cửa sổ, giống như ứng dụng Growl nổi tiếng của hệ điều hành Mac.
Giới thiệu về Local Collections
Để bắt đầu, chúng ta cần tạo một collection để lưu trữ lỗi. Để cho những thông báo lỗi này chỉ hữu hiệu với session hiện tại và không cần phải lưu dài hạn, chúng ta sẽ sử dụng một thứ mới, đó là tạo collection cục bộ (local collection).Điều này có nghĩa là collection Errors
chỉ tồn tại trong trình duyệt và sẽ không đồng bộ ngược với server.
Để đạt được điều này, chúng ta tạo thông báo lỗi bên trong thư mục client
(để cho collection chỉ tồn tại phía client), và với tên MongoDB cho collection là null
(vì dữ liệu của collection sẽ không bao giờ được lưu vào cơ sở dữ liệu phía server):
// Local (client-only) collection
Errors = new Mongo.Collection(null);
Bây giờ, sau khi đã tạo được collection, chúng ta có thể thêm hàm throwError
để gọi khi muốn thêm thông báo lỗi. Chúng ta không cần phải lo lắng về allow
hoặc deny
hoặc bất kỳ vấn đề bảo mật nào, vì collection “cục bộ” đối với người dùng hiện tại.
throwError = function(message) {
Errors.insert({message: message});
};
Điểm thuận lợi để lưu thông báo lỗi với collection cục bộ là, cũng như mọi collection khác, nó tương tác lại – nghĩa là chúng ta có thể hiển thị lỗi một cách có tương tác giống như hiển thị bất kỳ dữ liệu collection nào khác.
Hiển thị lỗi
Chúng ta sẽ thêm thông báo lỗi ở phía trên cùng của layout chính:
<template name="layout">
<div class="container">
{{> header}}
{{> errors}}
<div id="main" class="row-fluid">
{{> yield}}
</div>
</div>
</template>
Bây giờ hãy cùng tạo template errors
và error
trong 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">×</button>
{{message}}
</div>
</template>
Templates ghép đôi
Bạn sẽ nhận ra rằng chúng ta đang đặt hai template vào trong cùng một file. Cho đến bây giờ, chúng ta đã bám sát quy ước “một file, một template”, nhưng Meteor vẫn hoạt động tốt dù tất cả template của chúng ta được đặt vào một file chung (mặc dù điều này sẽ tạo ra một file main.html
rất lộn xộn!).
Trong trường hợp hiện tại, vì cả hai template đều khá là ngắn gọn, chúng ta có thể tạo ra ngoại lệ và để chúng vào trong cùng một file. Việc này làm cho kho chứa của chúng ta sạch sẽ hơn.
Chúng ta chỉ cần tạo helper cho template nữa là mọi thứ sẽ ổn!
Template.errors.helpers({
errors: function() {
return Errors.find();
}
});
Bạn có thể thử thông báo lỗi vừa tạo bằng tay. Bạn chỉ cần mở console trình duyệt và gõ:
throwError("I'm an error!");

Hai loại Lỗi
Tại thời điểm này, việc phân biệt giữ lỗi “cấp độ ứng dụng” và lỗi “cấp độ code” là rất quan trọng.
Lỗi cấp độ ứng dụng thường tạo ra do hành động của người dùng, và người dùng có thể làm gì đó sau khi nó xảy ra. Những lỗi này bao gồm cả lỗi về kiểm tra form, lỗi về quyền truy cập, lỗi “không tìm thấy”, và nhiều lỗi khác nữa. Đây là những lỗi mà bạn muốn hiển thị cho người dùng để chỉ cho họ cách sửa bất kỳ vấn đề gì họ đang mắc phải.
Lỗi cấp độ code thì khác, là những lỗi xảy ra không mong muốn do sai sót khi code, và bạn thường không muốn trực tiếp hiển thị ra cho người dùng. Thay vào đó là theo dõi bằng công cụ từ bên thứ ba, ví dụ như là Kadira.
Trong chương này, chúng ta sẽ tập trung vào việc khắc phục loại lỗi thứ nhất, chứ không tập trung vào việc khắc phục lỗi sai sót do code.
Tạo thông báo lỗi
Chúng ta đã biết cách hiển thị thông báo lỗi, nhưng chúng ta vẫn cần phải kích hoạt trước khi có thể nhìn thấy. Chúng ta vừa thực hiện một bối cảnh lỗi khá tốt: cảnh báo về việc bị trùng lặp bài viết. Đơn giản hãy thay thế alert
trong helper sự kiện postSubmit
với hàm throwError
vừa thiết lập:
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});
});
}
});
Chúng ta sẽ làm điều tương tự cho sự kiện của helper 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});
}
});
},
//...
});
Hãy thử tạo một bài viết và thêm vào URL là http://meteor.com
. Do URL này đã được gắn với một bài viết trước đó, bạn sẽ thấy:

Làm sạch thông báo lỗi
Bạn vừa nhận ra rằng thông báo lỗi biến mất sau khi đã xuất hiện một vài giây. Điều này thực sự là do một đoạn CSS chúng ta đã thêm vào stylesheet ngay từ lúc bắt đầu cuốn sách:
@keyframes fadeOut {
0% {opacity: 0;}
10% {opacity: 1;}
90% {opacity: 1;}
100% {opacity: 0;}
}
//...
.alert {
animation: fadeOut 2700ms ease-in 0s 1 forwards;
//...
}
Chúng ta đang định nghĩa một đoạn CSS animaton fadeOut
để đặc tả 4 keyframe cho thuộc tính độ trong suốt (0%, 10%, 90%, và 100% của khoảng thời gian diễn animation) và áp dụng vào class .alert
.
Đoạn animation sẽ chạy trong 2700 mili giây, sử dụng phương trình đo thời gian ease-in
, chạy với độ trễ là 0 giây, chạy đúng một lần, và cuối cùng dừng lại ở keyframe cuối cùng khi mọi thứ đã xong xuôi.
Animations vs Animations
Có thể bạn đang tự hỏi tại sao mình lại dùng animation trên nền CSS (thứ được định nghĩa trước và ngoài khả năng kiểm soát của cúng ta), thay vì dùng animation được quản lý bởi chính bản thân Meteor.
Trong khi Meteor đúng là có cung cấp sự hỗ trợ cho việc chèn animation, chúng ta muốn chương này tập trung vào thông báo lỗi. Vì vậy chúng ta sử dụng animation CSS đơn giản và chúng ta sẽ dành những công việc trang hoàng trong chương về Animation.
Hiện tại mọi thứ đã hoạt động, nhưng nếu bạn kích hoạt nhiều lỗi (bằng việc submit cùng một đường dẫn ba lần chẳng hạn), bạn sẽ nhận ra rằng chúng bị chồng đống lên nhau:

Điều này là do trong khi thành phần .alert
biến mất khi nhìn bằng mắt nhưng thực ra vẫn tồn tại trong DOM. Chúng ta cần sửa điều này.
Đây chính là một trong những tình huống mà Meteor toả sáng. Vì collection Errors
tương tác lại, tất cả việc chúng ta cần làm là xoá thông báo lỗi cũ ra khỏi collection!
Chúng ta sẽ dùng hàm Meteor.setTimeout
để đặc tả một hàm callback sẽ được chạy sau mỗi khoảng thời gian tạm ngừng (trong trường hợp này là 3000 mili giây).
Template.errors.helpers({
errors: function() {
return Errors.find();
}
});
Template.error.rendered = function() {
var error = this.data;
Meteor.setTimeout(function () {
Errors.remove(error._id);
}, 3000);
};
Hàm callback rendered
kích hoạt mỗi khi template được đưa ra trình duyệt. Bên trong hàm callback, this
tham chiếu tới template hiện tại, và this.data
để chúng ta truy cập tới dữ liệu của object đang được đưa ra (render). Trong trường hợp của chúng ta, chính là một thông báo lỗi.
Lục tìm Kiểm tra
Cho đến bây giờ, chúng ta vẫn chưa thêm một kiểm tra (validation) nào cho form. Ít nhất thì chúng ta cũng mong muốn người dùng cung cấp cả URL và tựa đề cho bài viết mới. Vì vậy hãy cùng chắc chắn họ sẽ làm như vậy.
Chúng ta sẽ làm hai việc để chỉ ra những trường bị thiếu: thứ nhất, chúng ta sẽ đưa ra một class CSS đặc biệt has-error
cho vào div
cha của bất kỳ trường nào của form có vấn đề. Thứ hai, chúng ta sẽ hiển thị một thông báo lỗi hữu ích như bên dưới.
Để bắt đầu, hãy thêm vào template postSubmit
những helper sau:
<template name="postSubmit">
<form class="main form">
<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>
Chú ý rằng chúng ta đã thêm vào tham số (theo trình tự là url
và title
) cho mỗi helper. Điều này giúp chúng ta có thể dùng lại helper cho cả hai lần, thay đổi hoạt động dựa vào tham số.
Bây giờ sẽ là phần thú vị: làm cho những helper này thực sự hoạt động.
Chúng ta sẽ dùng Session để lưu object postSubmitErrors
chứa bất kỳ lỗi tiềm tàng nào. Khi mà người dùng tương tác với form, object này sẽ thay đổi, nghĩa là sẽ tương tác để hiển thị lại nội dung và hình thức của form.
Trước tiên, chúng ta sẽ khởi tạo object mỗi khi template postSubmit
được tạo. Điều này giúp chắc chắn là người dùng sẽ không thấy lỗi cũ tồn đọng từ trang truy cập trước đó.
Sau đó chúng ta định nghĩa hai template helper. Chúng cùng nhìn vào thuộc tính field
của Session.get('postSubmitErrors')
(khi mà field
là url
hoặc title
tuỳ thuộc vào nơi chúng ta gọi helper).
Trong khi errorMessage
chỉ đơn giản trả về bản thân tin thông báo, errorClass
kiểm tra sự có mặt của tin thông báo và trả về has-error
nếu có lỗi tồn tại.
Template.postSubmit.created = 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' : '';
}
});
Bạn có thể kiểm tra rằng helper của chúng ta đang hoạt động đúng bằng việc mở console trình duyệt và gõ vào như bên dưới:
Session.set('postSubmitErrors', {title: 'Warning! Intruder detected. Now releasing robo-dogs.'});

Bước tiếp theo là thực sự lắp ráp Session object postSubmitErrors
tới form.
Trước khi làm vậy, chúng ta sẽ tạo một hàm validatePost
trong posts.js
để xem trong object post
, và trả về object errors
mà chứa đựng bất kỳ lỗi xác đáng nào (tức là, khi mà trường title
hoặc url
bị thiếu):
//...
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;
}
//...
Chúng ta sẽ gọi hàm này từ helper sự kiện 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});
});
}
});
Chú ý rằng chúng ta đang dùng return
chỉ để huỷ thực thi của helper nếu như có lỗi xảy ra mà không phải bởi vì chúng ta muốn trả về giá trị này ở chỗ nào đó.

Kiểm tra phía Server
Chúng ta vẫn chưa thực sự kết thúc. Chúng ta đang kiểm tra sự có mặt của URL và tựa đề ở phía client, nhưng còn về phía server thì sao? Sau tất cả, rất có thể sẽ có ai đó sẽ cố nhập vào bài viết trống bằng tay với việc gọi method postInsert
thông qua console trình duyệt.
Mặc dù chúng ta không cần hiển thị thông báo lỗi nào ở phía server, chúng ta vẫn sẽ dùng cùng hàm validatePost
. Ngoại trừ việc là lần này chúng ta sẽ gọi bên trong method postInsert
, và không chỉ với sự kiện helper:
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
};
}
});
Xin được nhắc lại, người dùng thường không cần phải thấy thông báo “You must set a title and URL for your post”. Nó sẽ chỉ hiện ra nếu như ai đó muốn vượt qua giao diện người dùng chúng ta đã làm cẩn thận, bằng cách là sử dụng trực tiếp console.
Để kiểm tra điều này, mở cửa sổ console trình duyệt và thử gõ bài viết không có URL:
Meteor.call('postInsert', {url: '', title: 'No URL here!'});
Nếu bạn đã thực hiện công việc đầy đủ, bạn sẽ thấy được một đoạn code đáng sợ đi kèm với thông báo “You must set a title and URL for your post”.
Kiểm tra lỗi khi Biên tập
Để mọi thứ hợp lý, chúng ta cũng sẽ dùng đoạn kiểm tra với việc biên tập bài viết. Mã code sẽ trông khá giống. Đầu tiên, với template:
<template name="postEdit">
<form class="main form">
<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>
Sau đó, với template helper:
Template.postEdit.created = 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');
}
}
});
Cũng giống như chúng ta đã làm cho form submit bài viết, chúng ta cũng muốn kiểm tra bài viết trên server. Bạn sẽ phải nhớ là chúng ta không dùng một method để biên tập bài viết, mà sẽ gọi update
trực tiếp từ client.
Điều này có nghĩa là chúng ta sẽ cần có hàm callback deny
thay thế:
//...
Posts.deny({
update: function(userId, post, fieldNames, modifier) {
var errors = validatePost(modifier.$set);
return errors.title || errors.url;
}
});
//...
Chú ý rằng tham số post
tham chiếu đến bài viết đang tồn tại. Trong trường hợp này, chúng ta muốn kiểm tra việc update, do đó chúng ta sẽ gọi validatePost
với nội dung modifier
của thuộc tính $set
(như trong Posts.update({$set: {title: ..., url: ...}})
).
Điều này hoạt động vì modifier.$set
chứa cùng thuộc tính title
và url
như toàn thể object url
. Dĩ nhiên, nó không nghĩa là mọi cập nhật bộ phận chỉ ảnh hưởng tới title
hoặc url
sẽ thất bại, nhưng trong thực hành điều này không phải là một vấn đề.
Bạn có thể nhận ra rằng đây là callback deny
thứ hai của chúng ta. Khi thêm vào nhiều callback deny
, hành động sẽ thất bại nếu như bất kỳ một trong số chúng trả về true
. Trong trường hợp này, điều đó có nghĩa là update
sẽ thành công chỉ khi mà nó hướng tới trường title
và url
, và không có trường nào trong hai trường bị trống.
Or discuss the book's contents here: