-
Notifications
You must be signed in to change notification settings - Fork 0
/
08-editing-posts.md.erb
260 lines (198 loc) · 9.37 KB
/
08-editing-posts.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
---
title: Editing Posts
slug: editing-posts
date: 0008/01/01
number: 8
level: starter
photoUrl: http://www.flickr.com/photos/ikewinski/9473337133/
photoAuthor: Mike Lewinski
contents: Add a form for editing your posts.|Set up edit permissions.|Restrict which properties can be edited.
paragraphs: 29
---
Now that we can create posts, the next step is being able to edit and delete them. While the UI code to do so is fairly simple, this is a good time to talk about how Meteor manages user permissions.
Let's first hook up our router. We'll add a route to access the post edit page and set its data context:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage',
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/posts/:_id/edit', {
name: 'postEdit',
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/submit', {name: 'postSubmit'});
var requireLogin = function() {
if (! Meteor.user()) {
if (Meteor.loggingIn()) {
this.render(this.loadingTemplate);
} else {
this.render('accessDenied');
}
} else {
this.next();
}
}
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "15~18" %>
### The Post Edit Template
We can now focus on the template. Our `postEdit` template will be a fairly standard form:
~~~html
<template name="postEdit">
<form class="main form page">
<div class="form-group">
<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"/>
</div>
</div>
<div class="form-group">
<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"/>
</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" %>
And here's the `post_edit.js` file that goes with it:
~~~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
alert(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" %>
By now most of that code should be familiar to you.
We have two template event callbacks: one for the form's `submit` event, and one for the delete link's `click` event.
The delete callback is extremely simple: suppress the default click event, then ask for confirmation. If you get it, obtain the current post ID from the Template's data context, delete it, and finally redirect the user to the homepage.
The update callback is a little longer, but not much more complicated. After suppressing the default event and getting the current post, we get the new form field values from the page and store them in a `postProperties` object.
We then pass this object to Meteor's `Collection.update()` Method using the [`$set`](http://docs.mongodb.org/manual/reference/operator/update/set/) operator (which replaces a set of specified fields while leaving the others untouched), and use a callback that either displays an error if the update failed, or sends the user back to the post's page if the update succeeded.
### Adding Links
We should also add edit links to our posts so that users have a way to access the post edit page:
~~~html
<template name="postItem">
<div class="post">
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
<p>
submitted by {{author}}
{{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
</div>
<a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
</div>
</template>
~~~
<%= caption "client/templates/posts/post_item.html" %>
<%= highlight "5~8" %>
Of course, we don't want to show you an edit link to somebody else's form. This is where the `ownPost` helper comes in:
~~~js
Template.postItem.helpers({
ownPost: function() {
return this.userId === Meteor.userId();
},
domain: function() {
var a = document.createElement('a');
a.href = this.url;
return a.hostname;
}
});
~~~
<%= caption "client/templates/posts/post_item.js" %>
<%= highlight "2~4" %>
<%= screenshot "8-1", "Post edit form." %>
<%= commit "8-1", "Added edit posts form." %>
Our post edit form is looking good, but you won't be able to actually edit anything right now. What's going on?
### Setting Up Permissions
Since we've previously removed the `insecure` package, all client-side modifications are currently being denied.
To fix this, we'll set up some permission rules. First, create a new `permissions.js` file inside `lib`. This makes sure our permissions logic loads first (and is available in both environments):
~~~js
// check that the userId specified owns the documents
ownsDocument = function(userId, doc) {
return doc && doc.userId === userId;
}
~~~
<%= caption "lib/permissions.js" %>
In the [Creating Posts](/chapter/creating-posts) chapter, we got rid of the `allow()` Methods because we were only inserting new posts via a server Method (which bypasses `allow()` anyway).
But now that we're editing and deleting posts from the client, let's go back to `posts.js` and add this `allow()` block:
~~~js
Posts = new Mongo.Collection('posts');
Posts.allow({
update: function(userId, post) { return ownsDocument(userId, post); },
remove: function(userId, post) { return ownsDocument(userId, post); }
});
//...
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "3~6" %>
<%= commit "8-2", "Added basic permission to check the post's owner." %>
### Limiting Edits
Just because you can edit your own posts, doesn't mean you should be able to edit *every* property. For example, we don't want users to be able to create a post and then assign it to somebody else.
So we'll use Meteor's `deny()` callback to ensure users can only edit specific fields:
~~~js
Posts = new Mongo.Collection('posts');
Posts.allow({
update: function(userId, post) { return ownsDocument(userId, post); },
remove: function(userId, post) { return ownsDocument(userId, post); }
});
Posts.deny({
update: function(userId, post, fieldNames) {
// may only edit the following two fields:
return (_.without(fieldNames, 'url', 'title').length > 0);
}
});
//...
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "8~13" %>
<%= commit "8-3", "Only allow changing certain fields of posts." %>
We're taking the `fieldNames` array that contains a list of the fields being modified, and using [Underscore](http://underscorejs.org/)'s `without()` Method to return a sub-array containing the fields that are *not* `url` or `title`.
If everything's normal, that array should be empty and its length should be 0. If someone is trying anything funky, that array's length will be 1 or more, and the callback will return `true` (thus denying the update).
You might have noticed that nowhere in our post editing code do we check for duplicate links. This means a user could submit a link and then edit it to change its URL to bypass that check. The solution to this issue would be to also use a Meteor method for the edit post form, but we'll leave this as an exercise to the reader.
<% note do %>
### Method Calls vs Client-side Data Manipulation
To create posts, we are using a `postInsert` Meteor Method, whereas to edit and delete them, we are calling `update` and `remove` directly on the client and limiting access via `allow` and `deny`.
While it's tempting to use `allow` and `deny` when things are relatively straightforward and do things directly from the client, the truth is that this approach is prone to security issues and in fact is no longer officially recommended.
So while we cover both techniques here, it's probably always better to use a Method, especially if you start needing to do things that should be outside the user's control (such as timestamping a new post or assigning it to the correct user).
Method calls are also useful in a few other scenarios:
- When you need to know or return values via callback rather than waiting for the reactivity and synchronization to propagate.
- For heavy database functions that would be too expensive to ship a large collection over.
- To summarize or aggregate data (e.g. count, average, sum).
[Check out our blog](https://www.discovermeteor.com/blog/meteor-methods-client-side-operations/) for a more in-depth exploration of this topic.
<% end %>