-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy path09-errors.md.erb
554 lines (415 loc) · 22.8 KB
/
09-errors.md.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
---
title: Thông báo
slug: errors
date: 0009/01/01
number: 9
points: 10
photoUrl: http://www.flickr.com/photos/ikewinski/9413892879/
photoAuthor: Mike Lewinski
contents: Tạo cơ cấu tốt hơn để hiển thị lỗi và thông báo.|Thực hiện kiểm tra form chặt chẽ hơn.|Thêm thông báo lỗi tức thời cho form.
paragraphs: 31
---
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](http://growl.info/) 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):
~~~js
// Local (client-only) collection
Errors = new Mongo.Collection(null);
~~~
<%= caption "client/helpers/errors.js" %>
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.
~~~js
throwError = function(message) {
Errors.insert({message: message});
};
~~~
<%= caption "client/helpers/errors.js" %>
Đ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:
~~~html
<template name="layout">
<div class="container">
{{> header}}
{{> errors}}
<div id="main" class="row-fluid">
{{> yield}}
</div>
</div>
</template>
~~~
<%= caption "client/templates/application/layout.html" %>
<%= highlight "4" %>
Bây giờ hãy cùng tạo template `errors` và `error` trong `errors.html`:
~~~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>
~~~
<%= caption "client/templates/includes/errors.html" %>
<% note do %>
### 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.
<% end %>
Chúng ta chỉ cần tạo helper cho template nữa là mọi thứ sẽ ổn!
~~~js
Template.errors.helpers({
errors: function() {
return Errors.find();
}
});
~~~
<%= caption "client/templates/includes/errors.js" %>
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õ:
~~~js
throwError("I'm an error!");
~~~
<%= screenshot "9-1", "Testing error messages." %>
<%= commit "9-1", "Basic error reporting." %>
<% note do %>
### 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](http://kadira.io).
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.
<% end %>
### 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:
~~~js
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});
});
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
<%= highlight "13,17" %>
Chúng ta sẽ làm điều tương tự cho sự kiện của helper `postEdit`:
~~~js
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});
}
});
},
//...
});
~~~
<%= caption "client/templates/posts/post_edit.js" %>
<%= highlight "15" %>
<%= commit "9-2", "Actually use the error reporting." %>
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:
<%= screenshot "9-2", "Triggering an error" %>
### 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:
~~~css
@keyframes fadeOut {
0% {opacity: 0;}
10% {opacity: 1;}
90% {opacity: 1;}
100% {opacity: 0;}
}
//...
.alert {
animation: fadeOut 2700ms ease-in 0s 1 forwards;
//...
}
~~~
<%= caption "client/stylesheets/style.css" %>
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.
<% note do %>
### 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.
<% end %>
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:
<%= screenshot "9-3", "Stack overflow." %>
Đ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).
~~~js
Template.errors.helpers({
errors: function() {
return Errors.find();
}
});
Template.error.rendered = function() {
var error = this.data;
Meteor.setTimeout(function () {
Errors.remove(error._id);
}, 3000);
};
~~~
<%= caption "client/templates/includes/errors.js" %>
<%= highlight "7~12" %>
<%= commit "9-3", "Clear errors after 3 seconds." %>
Hàm callback [`rendered`](http://docs.meteor.com/#template_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:
~~~html
<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>
~~~
<%= caption "client/templates/posts/post_submit.html" %>
<%= highlight "3,7,10,14" %>
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.
~~~js
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' : '';
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
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:
~~~js
Session.set('postSubmitErrors', {title: 'Warning! Intruder detected. Now releasing robo-dogs.'});
~~~
<%= caption "Browser console" %>
<%= screenshot "9-4", "Red alert! Red alert!" %>
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):
~~~js
//...
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;
}
//...
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "3~13" %>
Chúng ta sẽ gọi hàm này từ helper sự kiện `postSubmit`:
~~~js
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});
});
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
<%= highlight "10~12" %>
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 đó.
<%= screenshot "9-5", "Caught red-handed." %>
### 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:
~~~js
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
};
}
});
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "9~11" %>
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:
~~~js
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”.
<%= commit "9-4", "Validate post contents on submission." %>
### 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:
~~~html
<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>
~~~
<%= caption "client/templates/posts/post_edit.html" %>
<%= highlight "3,7,10,14" %>
Sau đó, với template helper:
~~~js
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');
}
}
});
~~~
<%= caption "client/templates/posts/post_edit.js" %>
<%= highlight "1~12,25~27,32" %>
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ế:
~~~js
//...
Posts.deny({
update: function(userId, post, fieldNames, modifier) {
var errors = validatePost(modifier.$set);
return errors.title || errors.url;
}
});
//...
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "3~8" %>
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.
<%= commit "9-5", "Validate post contents when editing." %>