Heads up: this recipe relies on some code that has been pre-written for you, available in the recipe's repository on GitHub. During this recipe, our focus will only be on implementing a simple blog using React. If you find yourself asking "we didn't cover that, did we?", make sure to check the source on GitHub.
4 |
5 |
6 |
7 |
Additional Packages
8 |
This recipe relies on several other packages that come as part of Base, the boilerplate kit used here on The Meteor Chef. The packages listed below are merely recipe-specific additions to the packages that are included by default in the kit. Make sure to reference the Packages Included list for Base to ensure you have fulfilled all of the dependencies.
9 |
10 |
11 | ### Prep
12 | - **Time**: ~2-3 hours
13 | - **Difficulty**: Advanced
14 | - **Additional knowledge required**: [basic usage of React](https://themeteorchef.com/recipes/getting-started-with-react/), [understanding props and state](https://themeteorchef.com/snippets/understanding-props-and-state-in-react) in React, and using [Flow Router](https://themeteorchef.com/snippets/using-flow-router-with-react/) with React.
15 |
16 | ### What are we building?
17 | HD Buff is a curated video streaming service (like [MUBI.com](https://mubi.com)). It's a really small service built by three enthusiastic film professionals that focuses on all the films you've never seen (as long as they're available in HD). They're looking to start a blog to keep up with their customers and asked us how to get it done. During our initial meetings, they mentioned that they'll want to extend its feature set in the long-run, but for now they just need a quick, simple way to write posts.
18 |
19 | In this recipe, we'll be helping the HD Buff crew to build a simple blog using Meteor and React. In terms of features, they said as long as someone can log in, write a post, and customers can sort those posts by tags later, they'll be happy. Oh, and they said Markdown is a _must-have_.
20 |
21 | Before we get to work, here's a quick example of what we're after:
22 |
23 |
24 |
25 | A simple blog for HD Buff.
26 |
27 |
28 | Ready to get to work? Let's do it!
29 |
30 | ### Ingredients
31 | Before we start building, make sure that you've installed the following packages and libraries in your application. We'll use these at different points in the recipe, so it's best to install these now so we have access to them later.
32 |
33 | #### Meteor packages
34 |
35 |
Terminal
36 |
37 | ```bash
38 | meteor add ongoworks:speakingurl
39 | ```
40 |
41 | We'll rely on the `ongoworks:speakingurl` package to help us generate URL friendly slugs based on our `post-titles-like-this`.
42 |
43 |
Terminal
44 |
45 | ```bash
46 | meteor add themeteorchef:commonmark
47 | ```
48 | We'll use the `themeteorchef:commonmark` package to help us parse the [Markdown](https://daringfireball.net/projects/markdown/) posts will be written with on the client.
49 |
50 |
Terminal
51 |
52 | ```bash
53 | meteor add momentjs:moment
54 | ```
55 | To help us parse dates on posts, we'll rely on the `momentjs:moment` package.
56 |
57 | ### Setting up an auth flow
58 | To get us up and running, our first task will be to set up a basic authentication workflow. We want to organize our routes so that only members of the HD Buff team can get access to creating new posts and editing existing ones. To do this, we're going to rely on creating an `App` component in React that will determine when and where users should be routed.
59 |
60 | We've already [set up some routes](https://github.com/themeteorchef/building-a-blog-with-react/tree/master/code/both/routes) (we've split these into two groups: `public` and `authenticated`), so let's focus on building out the `App` component each of the routes is using to render the view. If you're not familiar with this pattern, take a peek at [this snippet](https://themeteorchef.com/snippets/authentication-with-react-and-flow-router/) which will walk you through all of the finer details of what we'll cover below.
61 |
62 | To get started, let's create our `App` component now.
63 |
64 |
;
99 | }
100 | });
101 | ```
102 |
103 | Lots of code! Don't panic. Let's focus down in the `render()` method of our component first. Here, we're working to determine _what_ the user should be able to see. As this `App` component is used for every route in the app, we can trust that it will always be the first "stopping point" for users. Because of this, here, we can implement the necessary functionality to decide whether or not they can see a certain page or not.
104 |
105 | The part we want to pay attention to is `{ this.data.loggingIn ? : this.getView() }`. In this one line, we're asking the question "is the app currently logging in a user?" If someone _is_ being logged in, we want to display the [``](https://github.com/themeteorchef/building-a-blog-with-react/blob/master/code/client/components/global/loading.jsx) component for them (a simple SVG graphic that we [animate with CSS](https://github.com/themeteorchef/building-a-blog-with-react/blob/master/code/client/stylesheets/components/global/_loading.scss)). If not, we want to fire the `getView()` method we've defined up above in the component.
106 |
107 | Up above, `getView()` is doing something similar to what we're doing in our `render()` method. At this point, we're asking _another_ question: "can this person view the page they're trying to access?" If the answer is yes, we reveal the page by returning `this.props.yield` (remember, from the current route, this is where we're telling `ReactLayout.render()` where to render the component being passed). If we went to `/login`, `this.props.yield` would equal the `` component. Make sense?
108 |
109 | Conversely, if we get a negative response from `this.data.canView()`, we simply reveal the `` component in place of the requested route/component. Wait, what? This is a bit confusing at first. While you might expect us to want to perform a redirect to `/login` here, it's much easier to just reveal the `` component directly instead. Why? Well, consider that if we do this, the URL doesn't change in the browser's navigation bar.
110 |
111 | So, if we're trying to access a protected route like `/posts`, when the user logs in (using the form we've revealed via the `` component), they will automatically get the component intended for the `/posts` route. Let that sink in. The basic idea is that by using this pattern, we're removing the need to store _where_ the user was headed if we determine they need to login. Instead, we let the browser do the work and simply "open the gates" once we've authenticated the user. Pretty neat, eh?
112 |
113 | #### Wiring up `canView()` and `isPublic()`
114 |
115 | With all of this in mind, the next question is, "how are `this.data.canView()` and `this.data.isPublic()` working?" This is where everything in our `App` component comes together. In the `canView()` method, we're asking whether or not the current route name is considered "public" (meaning it's accessible to anyone), or, if a user is currently logged in (using `!!` to convert the result of `Meteor.user()`—an object—to a `Boolean`). If either of these return `true`, we return the requested view/component.
116 |
117 | The magic of this happens inside of the `isPublic()` method. Notice that we're passing in the current route name via `FlowRouter.getRouteName()`. With this, we simply take the value and test it against an array of route names (pay attention, these are the `name` values defined on our routes, not the paths), seeing if the passed value exists in the array. If it _does_, this means that the route is public and okay to access. If it _doesn't_ exist, that means the route is protected and we should defer to `Meteor.user()` to see if a user is logged in. If they _are_, they see the protected route as expected. If not, they get the `` component like we outlined above!
118 |
119 | With this in place, we now have a functioning auth flow for our app. This means that anyone from HD Buff can login and get access to the editor we'll build next, but the public is kept out. Nice!
120 |
121 |
122 |
Holy components, Batman!
123 |
We've taken a decidely shorter path to explaining the organization of our authentication flow. You may be wondering, "but what about that <AppHeader /> component?" Throughout this recipe, we may skip over components like this, so make sure to check the source on GitHub if something isn't clear. If the concepts being used are not clear, make sure to defer to the links in the "Additional knowledge required" list in the Prep section of this post up top. These guides will help you to understand what's happening here.
124 |
125 |
126 | ### Building the editor
127 | Next up, we need to implement the editor where the HD Buff team will actually manage content on their blog. This will require three steps: adding the ability to list existing posts, the ability to create new posts, and the ability to edit posts in a form. Before we start our work on these components, though, we need to set up a collection where all of our data will live. To help us out later, we'll be using some Schema-foo to automate the creation of some of the data we'll need.
128 |
129 | #### Setting up a collection and schema
130 | Let's get our collection set up now. To start, let's get a simple definition in place and lock down our allow/deny rules (this will help us to keep the client secure and force all database operations to happen on the server).
131 |
132 |
/collections/posts
133 |
134 | ```javascript
135 | Posts = new Mongo.Collection( 'posts' );
136 |
137 | Posts.allow({
138 | insert: () => false,
139 | update: () => false,
140 | remove: () => false
141 | });
142 |
143 | Posts.deny({
144 | insert: () => true,
145 | update: () => true,
146 | remove: () => true
147 | });
148 | ```
149 |
150 | Simple enough! Here we create our new collection assigning it to the global variable `Posts` and then set up our `allow` and `deny` rules. Again, this is a security practice. Here, we're saying that we want to deny _all_ database operations on the client and we do not want to allow _any_ database operations on the client (specifically for our `Posts`) collection. This means that later, we'll need to use Meteor methods in order to manage our database. We do this because allow and deny rules are [finicky and error prone](https://www.discovermeteor.com/blog/allow-deny-challenge-results/). Using methods does add a little work to our plate, but gives us peace of mind for later.
151 |
152 | Next up, we need to define a schema. For this, we're going to rely on the [aldeed:collection2](https://themeteorchef.com/snippets/using-the-collection2-package/) package that comes with [Base](https://github.com/themeteorchef/base/tree/base-react). Because some of the rules here are a little more complicated than others, let's add the basic ones first and then pepper in the more complicated stuff.
153 |
154 |
/collections/posts.js
155 |
156 | ```javascript
157 | let PostsSchema = new SimpleSchema({
158 | "published": {
159 | type: Boolean,
160 | label: "Is this post published?",
161 | autoValue() {
162 | if ( this.isInsert ) {
163 | return false;
164 | }
165 | }
166 | },
167 | "updated": {
168 | type: String,
169 | label: "The date this post was last updated on.",
170 | autoValue() {
171 | return ( new Date() ).toISOString();
172 | }
173 | },
174 | "title": {
175 | type: String,
176 | label: "The title of this post.",
177 | defaultValue: "Untitled Post"
178 | },
179 | "content": {
180 | type: String,
181 | label: "The content of this post.",
182 | optional: true
183 | },
184 | "tags": {
185 | type: [ String ],
186 | label: "The tags for this post.",
187 | optional: true
188 | }
189 | });
190 |
191 | Posts.attachSchema( PostsSchema );
192 | ```
193 |
194 | Okay! Here, we have the basic parts of our schema. **This is not all of the fields we'll need**, just the ones with simple rulesets. Here, `published` is being used to determine whether or not the current post is published. This is a `Boolean` value, meaning it's either `true` (is published) or `false` (is not published). Notice that protect our authors, when we insert a new post we're automatically setting the value of this field to `false`. Neat, eh? This means that no matter what, if we insert a new post it will have its published value set to `false`. They'll thank us later!
195 |
196 | Next, `updated` is doing something similar, however, setting the current date as an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) string. A little further down, `title` and `content` are just going to be simple `String` values. For `tags`, this will just be validating as an `Array` of `String`s. Notice that both `content` and `tags` are being made optional. Why's that? Well, technically we'll be allowing our authors to create new posts _without_ any content or tags. While this isn't likely to happen all of the time, we may find authors wanting to save a post idea but not publish it yet (remember, posts are _not_ published by default). Finally, to save us some time, notice that we've already attached our schema to our `Posts` collection.
197 |
198 | Now for the tricky part! Right now the HD Buff team isn't terribly concerned about content owernship. They've alluded to the idea that whoever last touched a post is going to be considered the owner. To make our lives easy, we can piggyback on `autoValue()` again, this time however, setting the author name automatically. Let's add it in:
199 |
200 |
/collections/posts.js
201 |
202 | ```javascript
203 | let PostsSchema = new SimpleSchema({
204 | "published": {
205 | type: Boolean,
206 | label: "Is this post published?",
207 | autoValue() {
208 | if ( this.isInsert ) {
209 | return false;
210 | }
211 | }
212 | },
213 | "author": {
214 | type: String,
215 | label: "The ID of the author of this post.",
216 | autoValue() {
217 | let user = Meteor.users.findOne( { _id: this.userId } );
218 | if ( user ) {
219 | return `${ user.profile.name.first } ${ user.profile.name.last }`;
220 | }
221 | }
222 | },
223 | [...]
224 | });
225 | ```
226 |
227 | Nothing _too_ crazy. Here, we're automatically setting the value of the `author` field on _any_ changes to posts (inserts, updates, etc.) with the name of the current user. To get their name, notice that we take `this.userId` (this is automatically provided by the collection2 package) and pass it to a call to `Meteor.users.findOne()`. From there, if we get a user back we grab the `first` and `last` name of the user from their `profile`'s `name` property and concatenate them into a String using ES2015's [template strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings) feature. Phew! Now, if "Joe Buff" is logged in and creates a new post (or edits an existing one), he will be set as the author. If "Jane Buff" does the same, _she_ will be set as the author! Pretty slick.
228 |
229 | Okay. Just one more of these to knock out and our schema is ready. This may be frustrating, but realize that we're saving ourselves a lot of effort later! Next, we need to account for duplicate `slug` values (`the-title-formatted-for-a-url-like-this`) for our posts automatically. Let's take a look:
230 |
231 |
/collections/posts.js
232 |
233 | ```javascript
234 | let PostsSchema = new SimpleSchema({
235 | [...]
236 | "title": {
237 | type: String,
238 | label: "The title of this post.",
239 | defaultValue: "Untitled Post"
240 | },
241 | "slug": {
242 | type: String,
243 | label: "The slug for this post.",
244 | autoValue() {
245 | let slug = this.value,
246 | existingSlugCount = Posts.find( { _id: { $ne: this.docId }, slug: slug } ).count(),
247 | existingUntitled = Posts.find( { slug: { $regex: /untitled-post/i } } ).count();
248 |
249 | if ( slug ) {
250 | return existingSlugCount > 0 ? `${ slug }-${ existingSlugCount + 1 }` : slug;
251 | } else {
252 | return existingUntitled > 0 ? `untitled-post-${ existingUntitled + 1 }` : 'untitled-post';
253 | }
254 | }
255 | },
256 | [...]
257 | });
258 | ```
259 |
260 | Don't give up! This is a bit trickier than our other `autoValue()`s but not too scary. Here, our goal is to determine whether or not a post with the same slug as the one currently being managed. This means that if we have a post called `my-great-film-review` and then try to add another with the same title, the slug will be set to `my-great-film-review-1`. This prevents overwriting and collisions in our URLs later and encourages authors to use more unique post titles.
261 |
262 | To do this, we're creating two queries and counting the number of posts returned _by_ those queries. First, we check whether the current slug already exists in the database (we use a [JavaScript RegEx](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) to do this)—notice we filter out this post from the results to avoid renaming its slug with `{ $ne: this.docId }`—returning the number of existing posts with the same slug. Read that a few times! It's a bit of a brain buster. In essence, this returns us the number of posts that have the same `slug` value as the post being inserted or updated, excluding that post from the count (`$ne = "Does Not Equal"`).
263 |
264 | Next, we do something very similar but this time for posts with the slug `untitled-post`. Wait, what? As we'll see in a little bit, whenever we create a new post we'll be giving it the title "Untitled Post" to start with. Here, we account for this and ensure that the slug value is handled properly. Down below, then, we check whether or not a slug is being passed. If one _is_ passed, we use our `existingSlugCount` suffixing the count plus one of matching posts to the returned value. If no posts exist, we return the value as-is.
265 |
266 | If we _do not_ have a slug passed (meaning we're creating a new post and want this to be automated), we check our `existingUntitled` count and update the slug in the exact same way we handle existing slugs. Woah. This is a pretty powerful chunk of code, so read it over a few times! This will save us and the HD Buff team a lot of frustration later.
267 |
268 | Phew. We're making good progress but we still have a lot to do. Let's keep chuggin'. Next up, we need to implement our ability to insert new posts and then display a list of those posts for editors to select from.
269 |
270 | #### Creating and listing posts for editors
271 | All right! Now we're getting in to the meat of this recipe. Now, we need a way to do two things: create a new post and list all of the posts in the system for the HD Buff team. To start, let's scope out the basic structure of our component along with its listing feature. Pay close attention as we'll be making reference to a few things that we won't cover directly but will link up to the code for.
272 |
273 |
/client/components/views/posts-list.jsx
274 |
275 | ```javascript
276 | PostsList = React.createClass({
277 | mixins: [ ReactMeteorData ],
278 | getMeteorData() {
279 | Meteor.subscribe( 'postsList' );
280 |
281 | return {
282 | posts: Posts.find().fetch().map( ( post ) => {
283 | return { uid: post._id, href: `/posts/${ post._id }/edit`, label: post.title };
284 | })
285 | };
286 | },
287 | renderPostsList() {
288 | if ( this.data.posts.length > 0 ) {
289 | return ;
290 | } else {
291 | return No posts found.;
292 | }
293 | },
294 | render() {
295 | return
296 |
297 |
298 |
299 | { this.renderPostsList() }
300 |
301 | ;
302 | }
303 | });
304 | ```
305 |
306 | A few things to noice. First, down in our `render()` method, in order to render our list of posts we're calling to a method `renderPostsList()` we've defined further up in our component. Our goal here is to conditionally display either a list of posts—if any exist—or, a message that reads "No posts found." Up in the `renderPostsList()` method, we can see this taking place. Inside, we begin by testing the length of `this.data.posts` (we'll cover this in detail soon).
307 |
308 | If it's greater than `0` (meaning posts were found), we go ahead and render the [``](https://github.com/themeteorchef/building-a-blog-with-react/blob/master/code/client/components/generic/list-group.jsx) component in [Base](https://github.com/themeteorchef/base/tree/base-react). Alternatively, if we _do not_ find any posts in the database, we display the [``](https://github.com/themeteorchef/building-a-blog-with-react/blob/master/code/client/components/generic/alerts/warning-alert.jsx) component (a stylized variation of the [``](https://github.com/themeteorchef/building-a-blog-with-react/blob/master/code/client/components/generic/alerts/alert.jsx) component. Review that wiring a few times. All we're doing is rendering out the list of posts, or, display an alert message.
309 |
310 | Let's review how the data is getting into the component to make sense of this. Up in the `getMeteorData()` method (this is the one that we get from Meteor's `react` package and is responsible for reactivity in the component), we begin to fetch data by subscribing to a publication called `postsList`. Real quick, let's see what that's returning.
311 |
312 |
/server/publications/posts-list.js
313 |
314 | ```javascript
315 | Meteor.publish( 'postsList', () => {
316 | return Posts.find();
317 | });
318 | ```
319 |
320 | Un-der-whel-ming! Pretty simple here. Because we're on the admin side of things, we simply want to return _all_ of the posts in the database. This means that all authors should have access to _all_ posts. Remember, ownership isn't a priority for HD Buff right now; they just want to get posts published without a lot of fuss. So far so good?
321 |
322 | Back in our `` component, let's review how we're returning this data from `getMeteorData()`.
323 |
324 |
/client/components/views/posts-list.jsx
325 |
326 | ```javascript
327 | PostsList = React.createClass({
328 | mixins: [ ReactMeteorData ],
329 | getMeteorData() {
330 | Meteor.subscribe( 'postsList' );
331 |
332 | return {
333 | posts: Posts.find().fetch().map( ( post ) => {
334 | return { uid: post._id, href: `/posts/${ post._id }/edit`, label: post.title };
335 | })
336 | };
337 | },
338 | [...]
339 | });
340 | ```
341 |
342 | Here, we're returning a `posts` property (this will be accessible via `this.data.posts` elsewhere in the component) and assigning it to a query to find all of the posts returned from our publication. Next, we `fetch()` that list so we get it back as an `Array` (remember, by default we get a MongoDB cursor from `Posts.find()`) and then we `map()` over that array. Phew! Inside of our map, we give each of our returned posts a slightly different structure, including a `uid`, `href`, and `label` property. What gives?
343 |
344 | What we're doing here is formatting the array of posts being returned from `this.data.posts` to match the API of our `` component. It will be expecting `uid`, `href`, and `label` as props, so we take care of this here so the component can just handle the render. A little strange, so spend some time with the connection between this and the `` we're outputting when we have posts.
345 |
346 | Okay, moving right along. Now for something a little easier: creating new posts.
347 |
348 | #### Creating new posts
349 |
350 | This part is super simple. Let's look back at our `render()` method for our posts list. Notice that inside, we have a button with an `onClick` prop that's wired up to a method on our component called `handleNewPost()`. The idea here is that when we click this button, we'll fire this method which will call to the server for us. Real quick, here are the essentials:
351 |
352 |
/client/components/views/posts-list.jsx
353 |
354 | ```javascript
355 | PostsList = React.createClass({
356 | [...]
357 | handleNewPost() {
358 | Meteor.call( 'newPost', ( error, postId ) => {
359 | if ( error ) {
360 | Bert.alert( error.reason, 'danger' );
361 | } else {
362 | FlowRouter.go( `/posts/${ postId }/edit` );
363 | Bert.alert( 'All set! Get to typin\'', 'success' );
364 | }
365 | });
366 | },
367 | [...]
368 | render() {
369 | return
370 |
371 |
372 |
373 | { this.renderPostsList() }
374 |
375 | ;
376 | }
377 | });
378 | ```
379 |
380 | For creating a new post, we're not taking in any arguments from the user. Remember all of that "Untitled Post" beeswax from earlier? This is where it comes together. Let's take a peek at this `newPost` method we're dialing up on the server.
381 |
382 |
/server/methods/insert/posts.js
383 |
384 | ```javascript
385 | Meteor.methods({
386 | newPost() {
387 | return Posts.insert( {} );
388 | }
389 | });
390 | ```
391 |
392 | Wait...hahahaha. Yep. That is seriously _it_. All of that work we did in our schema is paying off right here. How the heck is this working? Well, consider that for all of the fields in our schema, each either has:
393 |
394 | 1. A default value.
395 | 2. An automatically set value.
396 | 3. Is optional.
397 |
398 | Combined, this means that when we insert a "blank" object into our collection, our schema is kicking in and automatically populating the required fields for us! Get outta here. Nope. Serious. Grab a pen and write home about this. You're officially a badass! Isn't this cool? Go ahead, beep your own horn.
399 |
400 | If we look back at our component real quick, we can see that we're taking the returned post ID from our method (remember, when we call `.insert()` on a collection without a callback, Meteor returns the new document's `_id` value) and redirecting to the "editor" view for working on our post FlowRouter.go( '/posts/${ postId }/edit' );. This is our next stop! Now we need to wire up our editor to actually manage and publish posts.
401 |
402 | #### Editing content
403 | This is going to take _a lot_ of work. Don't let that spook you! In honesty, our `` component looks scarier than it actually is because we're using up a lot of vertical space to list out props on our components. To step through everything efficiently, we're going to start by wiring up our data for our component _first_ and then discuss how we're making use of it. There's a lot of repetition in concepts here, so pay close attention to the first few to make sure you have a solid grasp on what we're doing.
404 |
405 |
/client/components/views/editor.jsx
406 |
407 | ```javascript
408 | Editor = React.createClass({
409 | mixins: [ ReactMeteorData ],
410 | getMeteorData() {
411 | Meteor.subscribe( 'editor', this.props.post );
412 |
413 | return {
414 | post: Posts.findOne( { _id: this.props.post } )
415 | };
416 | },
417 | [...]
418 | render() {
419 | if ( !this.data.post ) { return ; }
420 |
421 | return [...]
422 | }
423 | });
424 | ```
425 |
426 | Easy does it to start! Notice that in terms of loading in data, this is all pretty simplistic. First, up in `getMeteorData()`, notice that we're pulling in the value `this.props.post` in order to subscribe to our data and filter down _which_ post we're currently editing. But wait, where is that coming from? Our route! Let's take a quick peek to see how it's being passed down.
427 |
428 |
/both/routes/authenticated.jsx
429 |
430 | ```javascript
431 | [...]
432 |
433 | authenticatedRoutes.route( '/posts/:_id/edit', {
434 | name: 'editor',
435 | action( params ) {
436 | ReactLayout.render( App, { yield: } );
437 | }
438 | });
439 | ```
440 |
441 | In our [authenticated routes group](https://github.com/themeteorchef/building-a-blog-with-react/blob/master/code/both/routes/authenticated.jsx), we're defining our path (the one we redirected to in the previous step after creating a post and linked each of our list items to) to our editor. Notice that in the `action()` method, we're pulling in the `params` object for the route and on our invocation of ``, we're passing `post={ params._id }`, or, the `_id` value from our URL! By passing this into our props, whenever our URL changes to a new post, our component will automatically get access to its `_id` straight from the router. Swish.
442 |
443 | Back in our ``, then, we subscribe to our `editor` publication passing in the post ID and then—to compensate for React's speed when moving between views—pass the ID to our `Posts.findOne()` as well. This ensures that when we go to a different post, we don't get stuck on the previous one because that's what our component sees in the minimongo collection (the result we'd get if we left this as a plain `Posts.findOne()`). Safety first!
444 |
445 | Speaking of safety, we also need to account for our data not being ready down in our `render()` method. Notice that we first check to see if `this.data.post` is defined and if it's _not_, we return an empty div until it _is_ ready. Once it _is_ ready, we return our component's actual markup (we'll do this next). So far so good? Okay, **strap on your goggles, we're going downhill at full speed from here**!
446 |
447 |
/client/components/views/editor.jsx
448 |
449 | ```javascript
450 | Editor = React.createClass({
451 | [...]
452 | generateSlug( event ) {
453 | let { setValue } = ReactHelpers,
454 | form = this.refs.editPostForm.refs.form,
455 | title = event.target.value;
456 |
457 | setValue( form, '[name="postSlug"]', getSlug( title, { custom: { "'": "" } } ) );
458 | },
459 | getLastUpdate() {
460 | if ( this.data ) {
461 | let { formatLastUpdate } = ReactHelpers,
462 | post = this.data.post;
463 |
464 | return `${ formatLastUpdate( post.updated ) } by ${ post.author }`;
465 | }
466 | },
467 | getTags() {
468 | let post = this.data.post;
469 |
470 | if ( post && post.tags ) {
471 | return post.tags.join( ', ' );
472 | }
473 | },
474 | handleSubmit( event ) {
475 | event.preventDefault();
476 | },
477 | render() {
478 | if ( !this.data.post ) { return ; }
479 |
480 | return
481 |
482 |
483 |
541 |
542 | ;
543 | }
544 | });
545 | ```
546 |
547 | Seriously?! I warned you! We've got a lot going on here so let's work our way through it. First, the basics. Down in our `render()` method, let's inspect each of the [``](https://github.com/themeteorchef/building-a-blog-with-react/blob/master/code/client/components/generic/forms/form-control.jsx) components being added. The part we want to pay attention to on each, here, is the `defaultValue` property. Notice that for the first four fields we're rendering, we're setting this equal to `this.data.post && this.data.post.`. What gives?
548 |
549 | This is a trick picked up from reader [Patrick Lewis](https://twitter.com/patrickml1). While we may expect this to return a `Boolean` value, in JSX, the interpretation is to say if both of these return `true` (exist), then _return_ the last value in the chain. This helps us to avoid a bunch of ternary operators littering our code. It's a bit cryptic the first time you see it, but once you start to use it you won't want to go back!
550 |
551 | What this all translates to, then, is that we're setting the default value for each of our input fields _if_ a value exists for that field on `this.data.post`. This covers two scenarios: editing a new post we just created with _some data_ and coming back later to edit a post with everything. If you've been having trouble wrapping your head around the importance of components and React, this is it: extreme frugality in respect to reusing interface components! This is like [extreme couponing](https://www.youtube.com/watch?v=8lFWTGog__I) for developers.
552 |
553 | For our final input—where we'll render a list of tags—we make a call to a method up above `this.getTags()`. It's pretty simple but important, so let's take a closer look.
554 |
555 |
/client/components/views/editor.jsx
556 |
557 | ```javascript
558 | getTags() {
559 | let post = this.data.post;
560 |
561 | if ( post && post.tags ) {
562 | return post.tags.join( ', ' );
563 | }
564 | }
565 | ```
566 |
567 | Because our list of tags will be stored as an _array_, when returning it back to the editor, we need to "prettify it" as a string. For example, we're expecting something like this `[ 'tacos', 'burritos', 'sandwiches' ]` but want to set the value of our tags input to `tacos, burritos, sandwiches`. Using this method `getTags()`, we accomplish this by pulling in the post data and if we discoer it has tags set, use a JavaScript `.join( ', ' )` to return our array as a comma-separated string!
568 |
569 |
570 |
Why not use a component?
571 |
As of writing, adding third-party components is tricky. The original scope for this recipe was to include a token input, however, the experience of implementing it was confusing to say the least. Unless you're comfortable getting your hands dirty, usage of third-party components is unadvised until Meteor adds proper support for require in Meteor 1.3.
572 |
573 |
574 | Continuing down a similar path, let's slide up our component a little further and look at the `getLastUpdate()` method we're using toward the top of our component's `render()` method.
575 |
576 |
/client/components/views/editor.jsx
577 |
578 | ```javascript
579 | getLastUpdate() {
580 | if ( this.data ) {
581 | let { formatLastUpdate } = ReactHelpers,
582 | post = this.data.post;
583 |
584 | return `${ formatLastUpdate( post.updated ) } by ${ post.author }`;
585 | }
586 | }
587 | ```
588 |
589 | A little more involved, but about the same. Here, we check if our data is available and if it is, we assign a few variables. The first is a bit funky. Here, we're using ES2015 object destructuring to say "give us the `formatLastUpdate()` method that's defined in the global `ReactHelpers` object." What's that? This is a collection of helpers that is being [added to the React port of Base](https://github.com/themeteorchef/building-a-blog-with-react/blob/master/code/client/helpers/react.js) (the TMC starter kit).
590 |
591 | Here, `formatLastUpdate()` is responsible for taking the date our post was last marked as updated—remember, we set this in our schema to be an ISO8601 string—and returning it as a human readable string like `January 13th, 2016 10:30 am` (behind the scenes we're using the `momentjs:moment` packag we installed earlier). To close the loop here, we concatenate this result with the word `by` and the value of `post.author`, which, again, is the name of the most recent user to update this post (set by `autoValue()` in our schema). Pretty wild stuff!
592 |
593 | Okay. Next up is something really neat! To make the editing experience nice and simple for the folks at HD Buff, we want to auto-generate slugs based on whatever title they set on the post. To do this, notice that in our `render()` method, the `` component for our post's title has an `onChange={ this.generateSlug }` prop. Can you guess what's happening here? Whenever this input changes (meaning a user types in it), we want to grab that value, convert it into a slug `like-this-right-here` and set it on the slug field beneath the title. Let's take a peek at the `generateSlug()` method we're calling.
594 |
595 |
/client/components/views/editor.jsx
596 |
597 | ```javascript
598 | generateSlug( event ) {
599 | let { setValue } = ReactHelpers,
600 | form = this.refs.editPostForm.refs.form,
601 | title = event.target.value;
602 |
603 | setValue( form, '[name="postSlug"]', getSlug( title, { custom: { "'": "" } } ) );
604 | }
605 | ```
606 |
607 | We're building up a lot of knowledge here! Notice that again, we're pulling in one of the methods `setValue` from our `ReactHelpers` global (this will help us set the slug we generate on the slug input). Next, we do something funky involving React's refs. Due to the shape of our [``](https://github.com/themeteorchef/building-a-blog-with-react/blob/master/code/client/components/generic/forms/form.jsx) component, we need to get access to that form by calling `this.refs.editPostForm.refs.form`. If you look down in our `render()` method, you'll notice `` has a prop `ref="editPostForm"` on it. This is React's way of identifying a DOM node within a component. By calling `this.refs.editPostForm.refs.form`, we're essentially grabbing the _nested_ ref of the `` component to access the actual DOM node for the form. _Gasp_. That was long-winded. If that's confusing, take a peek at the wiring of the `` component [here](https://github.com/themeteorchef/building-a-blog-with-react/blob/master/code/client/components/generic/forms/form.jsx).
608 |
609 | Next, we grab the value of `event.target`, or, our title input. This works because the "event" that's taking place is happening on our title input. By proxy, then, `event.target` is equal to our title input. With _all of this_ bundled up, we make a call to `setValue()`, passing the context for where our field will be—`form`—the name attribute of the element we want to set the value on—our slug input—and finally, we use the `getSlug()` method from the `ongoworks:speakingurl` package we installed earlier to generate the `slugified-version-of-our-title`.
610 |
611 | Yikes! That seems like a lot, but step through it and it will make sense. With this in place, whenever we edit the value of our title input, our slug will automatically be generated and set on the slug input! Nice work. That was a big one to solve.
612 |
613 | Last but not least for this form, let's get some validation wired up and then handle saving changes on the server!
614 |
615 | #### Saving the form
616 | Here comes the turkey. This is a big step, but should actually look somewhat familiar if you've worked with validation in the past. What we want to do now is validate the fields in our editor and if they're good to go, push an update of our post to the server. If you're new to validation, take a peek at this post on [jQuery validation](https://themeteorchef.com/snippets/validating-forms-with-jquery-validation/), the underying package we'll use to validate our form here.
617 |
618 |
/client/components/views/editor.jsx
619 |
620 | ```javascript
621 | validations() {
622 | let component = this;
623 |
624 | return {
625 | rules: {
626 | postTitle: {
627 | required: true
628 | }
629 | },
630 | messages: {
631 | postTitle: {
632 | required: "Hang on there, a post title is required!"
633 | }
634 | },
635 | submitHandler() {
636 | let { getValue, isChecked } = ReactHelpers;
637 |
638 | let form = component.refs.editPostForm.refs.form,
639 | post = {
640 | _id: component.props.post,
641 | title: getValue( form, '[name="postTitle"]' ),
642 | slug: getValue( form, '[name="postSlug"]' ),
643 | content: getValue( form, '[name="postContent"]' ),
644 | published: isChecked( form, '[name="postPublished"]' ),
645 | tags: getValue( form, '[name="postTags"]' ).split( ',' ).map( ( string ) => {
646 | return string.trim();
647 | })
648 | };
649 |
650 | Meteor.call( 'savePost', post, ( error, response ) => {
651 | if ( error ) {
652 | Bert.alert( error.reason, 'danger' );
653 | } else {
654 | Bert.alert( 'Post saved!', 'success' );
655 | }
656 | });
657 | }
658 | };
659 | },
660 | handleSubmit( event ) {
661 | event.preventDefault();
662 | }
663 | ```
664 | Woah buddy! A lot going on here but nothing too crazy. Notice that we have two methods output here: `validations()` and `handleSubmit()`. Here, `handleSubmit()` is responsible for "terminating" the default behavior of our form's `onSubmit` method—we can see this being attached to our `` component in our `render()`—and instead, deferring submission to our validation's `submitHandler()` method. This is a bit strange, but allows us to get our form validated and handle the submission without a lot of running around.
665 |
666 | Inside `validations()`—this is also attached to our `` component as a prop—we add a single rule for our `postTitle` input. This is ensuring that `postTitle` is _not_ blank when our user submits the form. If it is, they'll be asked to correct it before they submit the form. Once the form is all green, we get to work in our `submitHandler()`. At this point, we're building up the object we'll send to the server to update our post.
667 |
668 | Here, we're making use of a few more helpers from our `ReactHelpers` object. At this point, all we're doing is fetching the current values from each of the fields in our form. Two to point out: `published` and `tags`. Notice that for published, we're using the helper `isChecked` to determine whether or not the "Published?" box is checked. For `tags`, we're grabbing the current value of the input and then _splitting_ it into an array. To make sure our data is clean, we use a map to return an array of with each value obtained from the string with a `trim()`. This ensures that none of our tags have any trailing spaces (e.g. tag vs. `tag`).
669 |
670 | So far so good? Once we have this, it's up to the server to save the post!
671 |
672 | #### Updating the post on the server
673 |
674 | Let's take a look at our `savePost` method on the server. Hint: it's really simple.
675 |
676 |
/server/methods/update/posts.js
677 |
678 | ```javascript
679 | Meteor.methods({
680 | savePost( post ) {
681 | check( post, Object );
682 |
683 | let postId = post._id;
684 | delete post._id;
685 |
686 | Posts.upsert( postId, { $set: post } );
687 | }
688 | });
689 | ```
690 |
691 | Yep, pretty simple! Here, we're not doing much. First, we [check](https://themeteorchef.com/snippets/using-the-check-package/) that the value we get from the client is an `Object`. Next, we assign the value of `post._id` to a variable `postId` and then delete it from the main `post` object (we don't want to include this in the value we pass to our `upsert` below). Finally, we perform an `upsert` on our `Posts` collection, passing in our post's contents via a MongoDB `$set` method. Why use an `upsert` here?
692 |
693 | Well, remember that when creating our new posts, we didn't set a content or tags field. With an upsert, we get the best of doing an update and an insert. If a field doesn't exist on the object we're trying to update, the upsert will create it for us. If it already exists, it will just update the value! This means that we don't have to fight with missing fields conflicting with those that already exist. We just call an `upsert` and let Mongo handle the stick parts for us.
694 |
695 | Awesome. At this point, we've got our posts inserting and everything wired up for admins. Next, let's focus on getting posts output for our readers. This will be much simpler and should go pretty fast!
696 |
697 | ### Listing posts in the index (and tag pages)
698 | Great work so far. We've come a long way, but we're not quite done. Next, we need to wire up a list of posts for HD Buff's readers. This will be the main stream of _published_ posts. In tandem with this, we're also going to wire up a way to make this list filterable by tag. Let's dive in. Our `` component is pretty simple compared to what we've accomplished so far, so let's take a quick tour of how everything is working here (a lot of repeated concepts).
699 |
700 |
;
749 | }
750 | });
751 | ```
752 |
753 | This should look super familiar. At this point, we're starting to repeat a lot of the patterns we've introduced so far. Let's do a quick breeze through this component, explaining the important pieces. Sound good?
754 |
755 | First, let's call attention to how we're pulling data. Remember, our goal for this component is to pull in data in one of two ways: either filtered by tag, or, with no filter (all published posts on the site). To achieve this, up in our call to `getMeteorData()`, we're doing a little `if`-foo to determine whether or not we're trying to render a list of posts by tag. If we detect the prop `this.props.tag`, we assume that yes, we want to filter by tag.
756 |
757 | In this case, we take the tag and pass it to our publication `tagsIndex` (we'll look at this soon) and then set `query` (the value we'll pass to the query set to the `posts` value of our object returned from `getMeteorData()`) to only gives us back the posts where the current tag is in the `tags` array of the post. Said another way, if we return a list of posts from the server like this:
758 |
759 | ```javascript
760 | { _id: '1', tags: [ 'peanuts', 'butter', 'oil' ] }
761 | { _id: '2', tags: [ 'tacos', 'almonds', 'cookies' ] }
762 | { _id: '3', tags: [ 'peanuts', 'sandwiches', 'political-differences' ] }
763 | ```
764 |
765 | if we pass the tag `peanuts`, we'd only expect to get back posts `1` and `3` (those are the only posts with `peanuts` in their `tags` array). Making sense?
766 |
767 | A little less complex, if we _do not_ detect `this.props.tag`, we simply want to subscribe to `postsIndex`. Let's take a look at those two side-by-side now.
768 |
769 |
/server/publications/posts-index.js
770 |
771 | ```javascript
772 | Meteor.publish( 'postsIndex', function() {
773 | return Posts.find( { published: true } );
774 | });
775 |
776 | Meteor.publish( 'tagsIndex', function( tag ) {
777 | check( tag, String );
778 | return Posts.find( { published: true, tags: { $in: [ tag ] } } );
779 | });
780 | ```
781 |
782 | About what we'd expect. Notice for `postsIndex` we simply return _all_ of the posts who's `published` flag is set to `true`. Down in `tagsIndex`, we do something similar, however, also passing in the `tag` from the client using an identical `{ $in: [ tag ] }` check like we did on the client. This guarantees that we only get back posts with the matching tag. Neat!
783 |
784 | Back in our `` component, the rest is pretty simple. If we're on a tag page we decide to render a slightly different page header displaying the name of the currently selected tag. For `renderPosts()`, if we have a set of posts we want to map over those returning an instance of `` (we'll look at this next). Otherwise, we return a "No posts found." message just like we did for editors earlier. Boom! Making progress.
785 |
786 | Real quick, let's take a peek at that `` component as it's got a little more going on then meets the eye.
787 |
788 | #### The `` component
789 |
790 | The `` component is reusing a lot of the same techniques as explained above, except for one. Let's take a look at the full component and then talk about the one that stands out.
791 |
792 |
/client/components/generic/post.jsx
793 |
794 | ```javascript
795 | Post = React.createClass({
796 | getPostTitle() {
797 | let post = this.props.post;
798 |
799 | if ( this.props.singlePost ) {
800 | return
;
829 | }
830 | });
831 | ```
832 |
833 | Okay! A lot of this is pretty straightforward. Here, we're rendering the actual contents of a post out for display. Notice that down in our `render()` method, we're simply grabbing the contents of our post and then displaying them in the UI. Because we'll be using this component conditionally (in either a list of posts or as a single post), we've set up a few methods to help us negotiate that process. For `getPostTitle()`, notice that we're checking whether or not our component has a `this.props.singlePost` value.
834 |
835 | In our last step, we'll wire up a component that makes use of this. If we _do_ detect this value, we want to return a plain `` tag containing our title _without_ a link. If we're not on a single post, we return an `` tag with a link _to_ our post. Think about that one! If we're in a list of posts, we'll want a link so we can read the post. If we're on a single post, we're already viewing it so we don't need the link.
836 |
837 | For our tags, there's not much mystery. Inside of `renderTags()`, if we get a list of tags for our post, we simply map over each and return an `` tag with a link to that tag's page (this makes use of the code we wrote in the previous step) along with the name of the tag. Simple!
838 |
839 | The big scary part (not really) of this is Markdown. In React, it's advised to be cautious when setting HTML directly on a component. Most of the time this isn't necessary, however, with Markdown conversion, we _will_ need to set HTML directly. Why? Well, when we get our post back here on the client, what we're actually getting back is a string of Markdown. In order to convert that into HTML, we need a way to do that and then set the value in our component.
840 |
841 | Here, if we look at `` we can see this taking place in the prop `dangerouslySetInnerHTML`. Spooky! This method is aptly named as outside of scenarios like this, setting this value can leave a component vulnerable to [XSS](https://facebook.github.io/react/tips/dangerously-set-inner-html.html) attacks. Fortunately in this case, we're safe as we know what we're putting into the DOM (at that location) has been properly sanitized. To handle that sanitization, notice that we're making a call to `this.getHTML()`.
842 |
843 | If we look at that method, it's pretty simple. All we're doing here is returning an object with a property `__html`, equal to the result of converting our Markdown into HTML. We get the `parseMarkdown()` method from the `themeteorchef:commonmark` package we installed earlier. In passing our string of Markdown to it, we get back a sanitized, HTML-ified version of our posts contents. We set this on the `__html` propery of the object we're returning and React takes care of the rest! Note: that `__html` property in an object thing is specific to `dangerouslySetInnerHTML`. If we pass our string directly, React will throw a tantrum.
844 |
845 | That's all we need to know for our `` component! One last step: rendering a single post. This is quick and painless, so let's take a look.
846 |
847 | ### Displaying a single post
848 | Fortunately, with our `` component wired up, displaying a single post is pretty easy. Let's dump out the whole component and explain what's happening.
849 |
850 |
/client/components/views/single-post.jsx
851 |
852 | ```javascript
853 | SinglePost = React.createClass({
854 | mixins: [ ReactMeteorData ],
855 | getMeteorData() {
856 | let sub = Meteor.subscribe( 'singlePost', this.props.slug );
857 |
858 | return {
859 | post: Posts.findOne( { slug: this.props.slug } ),
860 | ready: sub.ready()
861 | };
862 | },
863 | render() {
864 | if ( !this.data ) { return ; }
865 | return
866 |
867 |
868 |
869 | ;
870 | }
871 | });
872 | ```
873 |
874 | Pretty close to what we've been up to. Notice that down in our `render()` method, we're wiring up our data source to the `` component we just wrapped up. That's the bulk of this component! The one thing we want to call attention to is how we're loading in our data. Notice that up in `getMeteorData()`, we're assigning our subscription to `sub`, and then making the response to its `.ready()` handle available at `this.data.ready`. We make use of `this.data.ready` down below in our call to ``.
875 |
876 | What's up with this? This is a safeguard. What we want to avoid is React moving too fast and rendering with the wrong data. By tying into `sub.ready()`, we're waiting to return data to our `` component until we're absolutely certain we have the post data we need. We can do this because our call to `Meteor.subscribe( 'singlePost' );` is updating when we change posts (e.g. clicking on another post in our list). Let's take a peek at our publication real quick to understand that.
877 |
878 |
/server/publications/single-post.js
879 |
880 | ```javascript
881 | Meteor.publish( 'singlePost', ( postSlug ) => {
882 | check( postSlug, String );
883 |
884 | return Posts.find( { slug: postSlug } );
885 | });
886 | ```
887 |
888 | Making sense? When we move to a new post, we grab its slug value from our route and pass it into `` as a prop. Then, in the component we subscribe using that same slug, returning only the post that matches that slug in the database. Nice and tidy!
889 |
890 | Back in our component, then, whenever we make the change to a new post, we're updating the slug and our subscription re-responds to `sub.ready()`. Neat! This means that we avoid any issues with displaying the incorrect post, allowing React's speed to play nicely with Meteor's reactivity.
891 |
892 | That's it! At this point, we've successfully fulfilled HD Buff's request for a simple blog! We have the ability to post, view a list of posts, sort posts by tag, and view a single post. We even added a means for managing posts behind the scenes for HD Buff's team. Great work!
893 |
894 | ### Wrap up & summary
895 | In this recipe we learned how to create a simple blog using Meteor and the React user interface library. We learned how to wire up a large collection of components, working with things like nesting, passing around props, and wiring up a data source with `ReactMeteorData`. Further, we learned how to manage an editing interface for writing posts as well as how to get those posts to display back for users. We even learned how to make use of collection2's `autoValue()` method to do some heavy lifting for us!
--------------------------------------------------------------------------------
/code/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/code/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | settings-production.json
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/code/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esnext": true
3 | }
4 |
--------------------------------------------------------------------------------
/code/.meteor/.finished-upgraders:
--------------------------------------------------------------------------------
1 | # This file contains information which helps Meteor properly upgrade your
2 | # app when you run 'meteor update'. You should check it into version control
3 | # with your project.
4 |
5 | notices-for-0.9.0
6 | notices-for-0.9.1
7 | 0.9.4-platform-file
8 | notices-for-facebook-graph-api-2
9 | 1.2.0-standard-minifiers-package
10 | 1.2.0-meteor-platform-split
11 | 1.2.0-cordova-changes
12 | 1.2.0-breaking-changes
13 |
--------------------------------------------------------------------------------
/code/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 |
--------------------------------------------------------------------------------
/code/.meteor/.id:
--------------------------------------------------------------------------------
1 | # This file contains a token that is unique to your project.
2 | # Check it into your repository along with the rest of this directory.
3 | # It can be used for purposes such as:
4 | # - ensuring you don't accidentally deploy one app on top of another
5 | # - providing package authors with aggregated statistics
6 |
7 | 1ipv1tp12xzfzhba088c
8 |
--------------------------------------------------------------------------------
/code/.meteor/cordova-plugins:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/themeteorchef/building-a-blog-with-react/490adbaff18da19be8915ebe4115ea3a083683ad/code/.meteor/cordova-plugins
--------------------------------------------------------------------------------
/code/.meteor/packages:
--------------------------------------------------------------------------------
1 | # Meteor packages used by this project, one per line.
2 | #
3 | # 'meteor add' and 'meteor remove' will edit this file for you,
4 | # but you can also edit it by hand.
5 |
6 | accounts-password
7 | accounts-base
8 | jquery
9 | check
10 | audit-argument-checks
11 | themeteorchef:jquery-validation
12 | twbs:bootstrap
13 | browser-policy
14 | meteorhacks:npm
15 |
16 |
17 | themeteorchef:bert
18 | meteorhacks:ssr
19 | standard-minifiers
20 |
21 |
22 | npm-container
23 | ecmascript
24 | digilord:faker
25 | kadira:flow-router
26 | kadira:blaze-layout
27 | meteorhacks:fast-render
28 | meteor-base
29 | session
30 | templating
31 | fourseven:scss
32 | stevezhu:lodash
33 | reactive-var
34 | reactive-dict
35 | aldeed:collection2
36 | tracker
37 | react
38 | random
39 | kadira:react-layout
40 | ongoworks:speakingurl
41 | themeteorchef:commonmark
42 | momentjs:moment
43 |
--------------------------------------------------------------------------------
/code/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/code/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@1.2.1
2 |
--------------------------------------------------------------------------------
/code/.meteor/versions:
--------------------------------------------------------------------------------
1 | accounts-base@1.2.2
2 | accounts-password@1.1.4
3 | aldeed:collection2@2.5.0
4 | aldeed:simple-schema@1.3.3
5 | audit-argument-checks@1.0.4
6 | autoupdate@1.2.4
7 | babel-compiler@5.8.24_1
8 | babel-runtime@0.1.4
9 | base64@1.0.4
10 | binary-heap@1.0.4
11 | blaze@2.1.3
12 | blaze-tools@1.0.4
13 | boilerplate-generator@1.0.4
14 | browser-policy@1.0.5
15 | browser-policy-common@1.0.4
16 | browser-policy-content@1.0.6
17 | browser-policy-framing@1.0.6
18 | caching-compiler@1.0.0
19 | caching-html-compiler@1.0.2
20 | callback-hook@1.0.4
21 | check@1.1.0
22 | chuangbo:cookie@1.1.0
23 | coffeescript@1.0.11
24 | cosmos:browserify@0.9.2
25 | ddp@1.2.2
26 | ddp-client@1.2.1
27 | ddp-common@1.2.2
28 | ddp-rate-limiter@1.0.0
29 | ddp-server@1.2.2
30 | deps@1.0.9
31 | diff-sequence@1.0.1
32 | digilord:faker@1.0.7
33 | ecmascript@0.1.6
34 | ecmascript-runtime@0.2.6
35 | ejson@1.0.7
36 | email@1.0.8
37 | fortawesome:fontawesome@4.4.0
38 | fourseven:scss@3.4.1
39 | geojson-utils@1.0.4
40 | hot-code-push@1.0.0
41 | html-tools@1.0.5
42 | htmljs@1.0.5
43 | http@1.1.1
44 | id-map@1.0.4
45 | jquery@1.11.4
46 | jsx@0.2.3
47 | kadira:blaze-layout@2.2.0
48 | kadira:flow-router@2.7.0
49 | kadira:react-layout@1.5.3
50 | livedata@1.0.15
51 | localstorage@1.0.5
52 | logging@1.0.8
53 | meteor@1.1.10
54 | meteor-base@1.0.1
55 | meteorhacks:async@1.0.0
56 | meteorhacks:fast-render@2.10.0
57 | meteorhacks:inject-data@1.4.1
58 | meteorhacks:npm@1.5.0
59 | meteorhacks:picker@1.0.3
60 | meteorhacks:ssr@2.2.0
61 | minifiers@1.1.7
62 | minimongo@1.0.10
63 | momentjs:moment@2.11.1
64 | mongo@1.1.3
65 | mongo-id@1.0.1
66 | npm-bcrypt@0.7.8_2
67 | npm-container@1.2.0
68 | npm-mongo@1.4.39_1
69 | observe-sequence@1.0.7
70 | ongoworks:speakingurl@6.0.0
71 | ordered-dict@1.0.4
72 | promise@0.5.1
73 | random@1.0.5
74 | rate-limit@1.0.0
75 | react@0.14.3
76 | react-meteor-data@0.2.4
77 | react-runtime@0.14.3
78 | react-runtime-dev@0.14.3
79 | react-runtime-prod@0.14.3
80 | reactive-dict@1.1.3
81 | reactive-var@1.0.6
82 | reload@1.1.4
83 | retry@1.0.4
84 | routepolicy@1.0.6
85 | service-configuration@1.0.5
86 | session@1.1.1
87 | sha@1.0.4
88 | spacebars@1.0.7
89 | spacebars-compiler@1.0.7
90 | srp@1.0.4
91 | standard-minifiers@1.0.2
92 | stevezhu:lodash@3.10.1
93 | templating@1.1.5
94 | templating-tools@1.0.0
95 | themeteorchef:bert@2.1.0
96 | themeteorchef:commonmark@1.1.0
97 | themeteorchef:jquery-validation@1.14.0
98 | tracker@1.0.9
99 | twbs:bootstrap@3.3.5
100 | ui@1.0.8
101 | underscore@1.0.4
102 | url@1.0.5
103 | webapp@1.2.3
104 | webapp-hashing@1.0.5
105 |
--------------------------------------------------------------------------------
/code/README.md:
--------------------------------------------------------------------------------
1 | # The Meteor Chef - Base
2 | A starting point for Meteor apps.
3 |
4 |