Movies Example
Out of the box, Vulcan comes with many features such as handling posts and comments, generating a newsletter, and much more.
But it’s important to understand that you can also use the underlying framework powering these features directly and build a completely different type of app.
So in this tutorial, we’ll focus on understanding this framework and seeing how to build a simple paginated list of movies.
The completed code for this tutorial can be found in the example-movies
package.
Video Tutorial
Note: this video differs slightly from the following tutorial when it comes to defining the user
resolver on the userId
field. Both approaches will work, but make sure you check the tutorial to get the most up-to-date syntax.
Short & Long Versions
This tutorial includes a shorter version that uses some of Vulcan’s default presets to save you time. If you’d rather to this short version, just skip every section marked with a *
sign.
Set Up
Clone the vulcan-starter repository to get a working Vulcan project.
Then, make sure you’re only including the packages you need. This means everything in your .meteor/packages
file should be commented out except for the core packages, a language package, and an accounts package.
Here’s a quick overview of the main packages:
vulcan:core
: Vulcan’s core libraries.vulcan:forms
: the library used for generating and submitting forms.vulcan:routing
: sets up and initializes routing and server-side rendering.vulcan:users
: user management (groups, permissions, etc.).
From these packages, we only need to keep vulcan:core
, which will automatically include all the other packages Vulcan needs to work.
We’ll also keep vulcan:i18n-en-us
, since it contains strings for core UI features, as well as Meteor’s accounts-password
package to activate password log in.
Finally, open a terminal at the root of the vulcan-starter directory and install the node packages: npm install
Creating a Package
The next step will be creating a Meteor package to hold our code. This will be a local package, meaning it will live inside your repo, but we’ll still have all the advantages of regular, remote packages (easy to enable/disable, restricted scope, ability to specify dependencies, etc.).
Create a new my-package
directory under /packages
. We’ll use the following file structure:
1 | my-package |
package.js
is your package manifest, and it tells Meteor which files to load.client/main.js
andserver/main.js
are the client and server entry points.modules/index.js
lists all the various modules that make up our package.
Set up all the directories along with the blank files within. Once this is done, let’s start with the package.js
file:
1 | Package.describe({ |
The api.use
block defines our package’s dependencies: vulcan:core
, vulcan:forms
, and vulcan:accounts
, a package that provides a set of React components for managing log in and sign up.
We then define our two client and server endpoints using api.mainModule
. Create lib/client/main.js
and lib/server/main.js
, and paste in the following line in both:
1 | import '../modules/index.js'; |
Finally, let’s include the Bootstrap stylesheet in lib/stylesheets/bootstrap.min.css
to make our app look a little better. You can get it from here.
The last step to activate your package is enabling it using the meteor add
command to add it to your .meteor/packages
file:
1 | meteor add my-package |
It may seem like not much happened, but once Meteor restart our custom package will now be active and loaded.
Our First Component
We now have the basic structure of our package, so let’s get to work. We’ll create a new component and a new route to display it.
First, create a new components
directory inside lib
if you haven’t done so yet and add the movies
directory inside of it. In components/movies
add a new file named MoviesList.jsx
containing a MoviesList
component:
1 | import React, { PropTypes, Component } from 'react'; |
We’ll also create a new components.js
file inside modules
so we can import our new component and make it available globally:
1 | import '../components/movies/MoviesList.jsx'; |
Then we need to import the components.js
inside of our modules/index.js
before going to the next step.
1 | import './components.js'; |
Routing
We can now create a route to display this component. Create a new routes.js
file inside modules
:
1 | import { addRoute } from 'meteor/vulcan:core'; |
In case you’re wondering, addRoute
is a very thin wrapper over React Router.
Make sure to also import routes.js
inside modules/index.js
:
1 | import './routes.js'; |
If everything worked properly, open a terminal at the root of the vulcan-starter directory and type npm start
you should now be able to head to http://localhost:3000/
and see your MoviesList
component show up.
The Schema
We want to display a list of movies, which means querying for data as well as setting up basic insert, edit, and remove operations. But before we can do any of that, we need to define what a “movie” is. In other words, we need a schema.
Vulcan uses JSON schemas based on the SimpleSchema npm module. You can also check out the Collections & Schemas section if you want to learn more.
Create schema.js
inside modules/movies
:
1 | const schema = { |
Note that we’re setting up an onCreate
function on the createdAt
field to initialize it to the current timestamp whenever a new document is inserted.
And we’re also setting canRead: ['guests']
on every field to make sure they’re visible to non-logged-in users (who belong to the default guests
group). By default, any schema field is kept private, so we need to make sure we don’t forget this step if we want our data to be publicly accessible.
Setting Up a Collection
We’re now ready to set up our Movies
collection. Create a new collection.js
file inside modules/movies
:
1 | import { createCollection } from 'meteor/vulcan:core'; |
createCollection
takes in a collection name, type name (in other words, the GraphQL type of documents belonging to this collection), a schema (as well as a few other objecs we’ll learn about later) and sets up a fully working GraphQL data layer for you!
As we did previously with routes.js
, import collection.js
from index.js
:
1 | // The Movies collection |
At this point it might not look like much has changed, but we now have a functional GraphQL schema! You can see it by opening up the Meteor shell in a terminal window (by typing meteor shell
from within your app directory) and typing:
1 | import {GraphQLSchema} from 'meteor/vulcan:lib' |
But even though we have a schema, we can’t actually query for a document yet because we don’t have any query resolvers.
Note: Default Resolvers & Mutations
Vulcan provides a set of default resolvers to speed things up, but for the sake of learning how things work this tutorial will show you how to write resolvers yourself.
If you’d rather skip this and use the defaults for now, just use the following code in collection.js
, and simply ignore any section concerning the resolvers.js
or mutations.js
files (they will be marked with a *
sign):
1 | import { createCollection, getDefaultResolvers, getDefaultMutations } from 'meteor/vulcan:core'; |
The code above throws an error Error: Type "CreateMovieDataInput" not found in document.
This is because we need to set permission to mutate the collection. This will be done later in the Actions, Groups, & Permissions chapter. For now, to avoid this error, you can comment out the line defining the mutations in the collection, but don’t forget to uncomment it once we have set the permissions.
Custom Query Resolvers*
(Note: you can skip this section if you’re using default resolvers and mutations)
In a nutshell, a resolver tells the server how to respond to a specific GraphQL query, and you can learn more about them in the Resolvers section.
Let’s start simple. Create a new resolvers.js
file inside modules/movies
and add:
1 | const resolvers = { |
Then import this new file from collection.js
:
1 | import { createCollection } from 'meteor/vulcan:core'; |
Seeding The Database
We can try out our new query resolver using GraphiQL, but first we need some data. Create a new seed.js
file inside server
:
1 | import Movies from '../modules/movies/collection.js'; |
This time, we don’t want to import this on the client, so we’ll import it directly from our server/main.js
endpoint:
1 | import '../modules/index.js'; |
Head to http://localhost:3000/graphiql and type:
1 | query moviesQuery { |
You should get a list of movie names and creation dates on the right. If you’d like, try requesting more fields:
1 | query moviesQuery { |
As you can see, the great thing about GraphQL is that you can specify exactly which piece of data you need!
More Advanced Resolvers*
(Note: you can skip this section if you’re using default resolvers and mutations)
Although our resolver works, it’s fairly limited. As it stands, we can’t filter, sort, or paginate our data. Even though we might not need these features right away, now is a good time to set things up in a more future-proof way.
Our resolver will accept a terms object that can specify filtering and sorting options, which is then transformed into a MongoDB-compatible object by the Movies.getParameters()
function. Additionally, we’ll add a totalCount
field that contains the number of documents matching these terms:
1 | const resolvers = { |
Our resolvers take three arguments:
root
: a link back to the root of the current GraphQL node. You can safely ignore this for now.args
: an object containing the query arguments passed from the client.context
: an object enabling access to our collections, as well as utilities such asgetViewableFields
(which is used to get a list of all fields viewable for the current user for a given collection).
Once we’ve figured out the correct selector
and options
object from the terms
and the current user, all that’s left is to make the actual database query using Movies.find()
.
The User Resolver
If you inspect the collection schema for a movie, you’ll notice it contains a userId
field that contains a string.
But that string, while useful internally, isn’t much use to our app’s users. Instead, we’d much rather display the reviewer’s name. In order to do so, we’ll resolver the userId
field as a user
object containing all the data we need.
Go back to your schema.js
file, and add the following resolveAs
property to the userId
field:
1 | userId: { |
We are doing four things here:
- Specifying that the field should be named
user
in the API. - Specifying that the
user
field returns an object of GraphQL typeUser
. - Defining a
resolver
function that indicates how to retrieve that object. - Specifying that in addition to this new
user
object, we want the originaluserId
field to still be available as well.
To test this, go back to GraphiQL and send the following query to our server:
1 | query moviesQuery { |
You should see new fields returned by the server:
- Inside the
results
there should be theuserId
field, an objectuser
containing the username of the creator. You can also query all the fields from theUsers
collection, such asemail
. - Additionally to the
results
object, there should be atotalCount
field that contains the number of movies queried.
Displaying Data
Now that we know we can access data from the client, let’s see how to actually load and display it within our app.
We’ll need two pieces for this: a container component that loads the data, and a presentational component that displays it. Fortunately, Vulcan comes with a set of built-in higher-order container components which we can use out of the box, so we can focus on the presentational components.
Create a new MoviesItem.jsx
component inside components/movies
:
1 | import React, { PropTypes, Component } from 'react'; |
Don’t forget to import our new component in components.js
:
1 | import '../components/movies/MoviesItem.jsx'; |
We’ll also come back to the MoviesList
component and use a GraphQL fragment (which we’ll define in the next section) to specify what data we want to load using the withMulti
higher-order component, and then show it using MoviesItem
:
1 | import React, { PropTypes, Component } from 'react'; |
We want to provide MoviesList
with the results
document list and the currentUser
property, so we wrap it with withMulti
and withCurrentUser
.
Fragments
At this stage, we’ll probably get an error message saying the fragment we’re using is not yet defined. To remedy this, create a new fragments.js
file inside modules/movies
:
1 | import { registerFragment } from 'meteor/vulcan:core'; |
And don’t forget to import it within collection.js
:
1 | import { createCollection } from 'meteor/vulcan:core'; |
Once you save, you should finally get your prize: a shiny new movie list displayed right there on your screen!
User Accounts
So far so good, but we can’t yet do a lot with our app. In order to give it a little more potential, let’s add user accounts to MoviesList
:
1 | import React, { PropTypes, Component } from 'react'; |
Yay! You can now log in and sign up at your own leisure. Note that the <Components.AccountsLoginForm />
component is a ready-made accounts UI component that comes from the vulcan:accounts
package.
Mutations*
(Note: you can skip this section if you’re using default resolvers and mutations)
Now that we’re logged in, we can start interacting with our data. Let’s build a simple form for adding new movies.
Before we can build the user-facing part of this feature though, we need to think about how the insertion will be handled server-side, using Mutations.
Create a new mutations.js
file in modules/movies
:
1 | import { createMutator, Utils } from 'meteor/vulcan:core'; |
This mutation performs a simple check for the presence of a logged-in user and whether they can perform the action, and then passes on the document
property to one of Vulcan’s boilerplate mutations, createMutator
.
Let’s pass it on to our createCollection
function in collection.js
:
1 | import { createCollection } from 'meteor/vulcan:core'; |
At this state, your application should be crashing with the message Error: Type "CreateMovieDataInput" not found in document.
. This will be fixed once we set the permissions to mutate the data.
Actions, Groups, & Permissions
The mutation’s check
function checks if the user can perform an action named movies.create
. We want all logged-in users (known as the members
group) to be able to perform this action, so let’s take care of this by creating a new movies/permissions.js
file:
1 | import Users from 'meteor/vulcan:users'; |
And adding it to collection.js
as usual:
1 | import { createCollection } from 'meteor/vulcan:core'; |
Note that in this specific case, creating an action and checking for it is a bit superfluous, as it boils down to checking if the user is logged in. But this is a good introduction to the permission patterns used in Vulcan, which you can learn more about in the Groups & Permissions section.
One more thing! By default, all schema fields are locked down, so we need to specify which ones the user should be able to insert as part of a “new document” operation.
Once again, we do this through the schema. We’ll add an canCreate
property to any “insertable” field and set it to [members]
to indicate that a field should be insertable by any member of the members
group (in other words, regular logged-in users):
We add document-level permissions to secure document access. It will also allow us to check permissions client-side.
1 | const schema = { |
While we’re at it we’ll also specify which fields can be edited via canUpdate
. In this case, the three fields we want members to be able to write to are name
, year
, and review
. Finally, we’ll also give the review
field a textarea
form control.
At this point it’s worth pointing out that for mutations like inserting and editing a document, we have two distinct permission “checkpoints”: first, the mutation’s check
function checks if the user can perform the mutation at all. Then, each of the mutated document’s fields is checked individually to see if the user should be able to mutate it.
This makes it easy to set up collections with admin-only fields, for example.
Forms
We now have everything we need to create a new MoviesNewForm.jsx
component using the SmartForm utility:
1 | import React, { PropTypes, Component } from 'react'; |
And import it in components.js
:
1 | import '../components/movies/MoviesNewForm.jsx'; |
A few things to note:
- We’ll pass the
MoviesItemFragment
fragment to the form so that it knows what data to return from the server once the mutation is complete. - We only want to show the “New Movie” form when a user actually can submit a new movie, so we’ll make use of the document level permissions we defined earlier to check if a user can create a movie using the
canCreate
helper on the collection. - We need to access the current user to perform this check, so we’ll use the
withCurrentUser
higher-order component.
Let’s add the form component to movies.jsx
:
1 | import React, { PropTypes, Component } from 'react'; |
Now fill out the form and submit it. The query will be updated and the new movie will appear right there in our list!
Editing Movies
We’re almost done! All we need now is to add a way to edit movies. First, create a new MoviesEditForm.jsx
component:
1 | import React, { PropTypes, Component } from 'react'; |
And in components.js
:
1 | import '../components/movies/MoviesEditForm.jsx'; |
Because we’re passing a specific documentId
property to the SmartForm
component, this form will be an edit document form. We’re also passing showRemove
to show a “delete document” option, and a successCallback
function to close the modal popup inside which the form will be displayed.
But note that to edit a document, we first need to know what that document is. In other words, we need to load the document on the client. SmartForms can take care of this for us, but we do need to write a new resolver. After all, the one we have displays a list of document, not a single document.
Go back to resolvers.js
and add a single
resolver:
1 | const resolvers = { |
Now let’s go back to MoviesItem
and make use of our form:
1 | import React, { PropTypes, Component } from 'react'; |
This time we’re using the <Components.ModalTrigger />
Vulcan component to show our form inside a popup (assuming the current user can perform an edit, of course).
Finally, we’ll also need to hook up our update and delete mutations in mutations.js
:
1 | /* |
For the check
function to work properly we need to update the permissions.js
file.
1 | import Users from 'meteor/vulcan:users'; |
All done! You should now be able to edit and remove movies.
Sorting
As it stands, our movie list isn’t really sorted. What if we wanted to sort it by movie year? In Vulcan, document lists receive their options via a terms
object. And whenever that object is processed (whether on the client or server), it goes through a serie of successive callbacks. So if we want to set a default sort, we can do so through one of these callbacks.
Create a parameters.js
file (learn more about terms and parameters here):
1 | import { addCallback } from 'meteor/vulcan:core'; |
And add it to movies/collection.js
:
1 | import { createCollection } from 'meteor/vulcan:core'; |
Note that the list automatically re-sorts as you edit individual documents. This might seem like a simple feature, but it’s one more thing you’d have to implement yourself if you weren’t using Vulcan!
Going Further
You can try to add a page displaying a single movie, fetching the data with the withSingle
higher-order component. Look into the starter package example-movies for an implementation of this.
This is probably a good place to stop, but you can go further simply by going through the code of the example-instagram
package. In it, you’ll see how to create a resolver for single documents so you can load more data for a specific movie, and use permission checks to enforce fine-grained security throughout your app.
And this is just the start. You can do a lot more with Vulcan, Apollo, and React, as you’ll soon see!