The time has come to use the generators. We will follow the same flow as the previous chapter. We will develop the create travel section first and then the listing, followed by the search and finally the reservation.
Creating travel
Run the iron g:scaffold createTravel command and you will see the following files created and updated:
created app/lib/collections/create_travel.jscreated app/client/templates/create_travelcreated app/client/templates/create_travel/create_travel.htmlcreated app/client/templates/create_travel/create_travel.jscreated app/client/templates/create_travel/create_travel.csscreated app/lib/controllers/create_travel_controller.jsupdated ../BookMyTravel2/app/lib/routes.jsupdated ../BookMyTravel2/app/server/publish.js
The generator has created a collection (create_travel.js) and a publication (in publish.js), which are not needed. We can ignore them. However, we will use the other components generated. Along with these files, we need a different layout for the creation screen. Let's create one by adding the create_travel directory inside the layouts directory. Add create_travel_layout.html inside the directory and add the following code to it:
We will add this layout to the CreateTravelController in create_travel_controller.js. Add the following piece of code as a property along with the subscriptions:
layoutTemplate: 'CreateTravelLayout',
Now, go to http://localhost:3001/create_travel and you will find that the layout reflects in the screen. Let's create the collection and then the form to create the documents in the collection. Run the following generator command to generate the busservice collection:
iron g:collection busservice
This command will create a busservice.js file in app/lib/collections. Go visit this file and you will find the busservice collection creation code as follows:
Busservice = new Mongo.Collection('busservice');
Followed by this line, there will be allow and deny methods that give us the ability to perform Role-Based Access (RBA) checks. Say, for example, if you want to deny a guest user from creating a bus service, you can do the check in the deny method's insert part and return true if positive. Doing so will deny the document insertion in the collection. You can use these methods to do extensive RBAC for your application. The skeleton is self-explanatory to explain the parameters we will get inside each operation. These insert, update, and remove operation pre-handlers will be executed before doing the respective operations via the collection instance (in our case Busservice). Different applications need different kinds of RBA conditions and so I leave this portion to you. Do not forget to make use of this in-built feature of MeteorJS as it is one of the important security features. We can perform authorization checks extensively using these handlers.
It is time to define the schema. If we cannot keep track of schema in a well-structured format, then it is going to be a big pain for maintenance. MeteorJS doesn't have built-in ways to create or define the schema. However, we have packages that can help us do this. We are going to use the aldeed: collection2 package, which will help us to attach a schema and do validation if required. The Git repo https://github.com/aldeed has some important packages that are very useful for development. Add the package by running the following command:
iron add aldeed:collection2
The schema for the busservice collection is as follows:
Add the preceding code after the busservice collection instantiation. The code is self-explanatory. For more details on the meteor-collection2 package, visit https://github.com/aldeed/meteor-collection2. You will find a lot of useful information and methods that can help you to do a better job in schema definition and validation. We can define many kinds of validations right in the schema definition itself, and the error messages can be defined along with the schema. If you go through the schema definition, you can figure out the minimum and maximum validations for some fields and custom validation for the arrival date and time and the departure date and time, which compares the dates. Similarly, one can define any custom validation and can access the document data from the form inside the custom validation methods.
The best part of defining a schema is the form creation. The aldeed repository has another package called AutoForm, which can read schema and create forms without much effort. Let's install the AutoForm package by running the following command:
iron add aldeed:autoform
Visit the AutoForm package documentation at https://github.com/aldeed/meteor-autoform. There is a lot of information to craft your forms carefully with plenty of options.
Inside the create_travel.html file in client/templates/create_travel, add the following template code:
Visit the browser and you will find that the form is created with less effort. In the preceding code, we have just specified the collection name, which is the instance we created (not the mongo collection name). Along with that, we have specified id, type. There is an attribute omitFields that facilitates to omit fields that we don't want to show in the form. The buttonContent attribute value will appear in the submit button of the form. A single line has created a form for us. Pretty timesaving, isn't it?
The styles are missing. Add the twbs:bootstrap package to the application. This will add styles to some extent. The forms generated by AutoForm are bootstrap complaints with bootstrap-related classes in the markups. We will add the rest of the styles from the previous chapter. Copy the custom styles we had created in the previous chapter to the main.css file of this application. Now, the form is usable, but there are some more details we need to understand.
The schema that we had defined has the Date type for the fields for arrival time, departure time, createdAt, and updatedAt, which you might have noticed. They are Date and not DateTime. There is no DateTime type and so the AutoForm package will also generate forms with the appropriate fields having the Date type and not Datetime. In our case, the arrival time and departure time must be Datetime and not just Date. So, we cannot use the quickform component as it is. To solve this problem, when we need customization in the form, the AutoForm package has given us ways to define the customized form in compliance with the schema. Replace {{quickform ...}} with the following code:
Instead of using quickForm, we are using the autoForm and afQuickField components to create our form. You can learn the syntax and other possible attribute options from the AutoForm package documentation. What we have done here is a minimal usage. However, a notable thing is type and meteormethod. We have specified the type of the form to be method and when we use method as type, it is mandatory to mention the method name in the meteormethod attribute. What this means is, on submit, the meteor method createBusService will be called by passing all the field values. The afQuickField component takes the names that must be the keys defined in the schema and other form field attributes. We needed a datetime input field and thus we are using the type="datetime-local" attribute in the appropriate fields.
Another important feature of the AutoForm package is validation and error display. Based on the schema definition, the form values are validated and an appropriate error message is displayed right below the fields. Try wrong inputs in the fields and submit the form. You will see the error messages below the fields. We don't need to wire the error handling and the error message display. They are taken care by the package itself. Also, AutoForm provides hooks to perform any pre or post-submission operations, if necessary.
All the client-related work is done and we have to define the server method to insert the form. In our case, we just want to validate and insert the document. Create the createBusService method in app/server/methods.js as follows:
Meteor.methods({
createBusService: function(busService) {
busService.createdAt = new Date();
busService.available_seats = parseInt(busService.seats, 10);
check(busService, BusServiceSchema); //validates the form data against the schema in the server side
Busservice.insert(busService);
}
});
If all goes well, you will be able to save the data to the collection. Check in the database to confirm.
Listing and search
Next is the listing part. Visit http://localhost:3000 and you will find the text Find me in app/client/templates/home/home. Our home page is going to be the listing and search page. Let's start adding the layout, then the list template, and wire the data. Things start from the route. Our route for home page points to HomeController and action method. Check the HomeController and you will find that the layout is MasterLayout and the action method calls the render of the Home template.
In master_layout.html, add the following code that builds the two columns layout for our home page:
This will create the two column layout. For maintainability purpose, instead of using the Home template from home.html, we will create the BusServiceList template using the generator and call it in the Home template. Run the following command in the root directory of the application:
iron g:template BusServiceList
The command will create the bus_service_list directory inside app/client/templates. We will use the bus_service_list.html file to define our listing template. Add the following code inside the template tag:
We have used some helper methods inside the template. Let's define them inside the bus_service_list.js file. Replace the helper's skeleton with the following code:
One last thing to do is call the BusServiceList template in the Home template. Go to Home in home.html and replace the content with the following code:
An empty list will be visible in the browser by this time. We have to wire the data. The busservice collection has to be published first. Let us use the generator itself to create the publication. Run the following command in the terminal:
iron g:publish busservice
This will add the publication code to publish.js in app/server. However, we need a slight modification here. Our publication should publish data, by default, in sorted order. So, let us change the return statement of the publication as follows:
We have a proper place to subscribe the data. In HomeController, we have the subscriptions method where we can subscribe the data by name. Add the following subscription line of code to the subscriptions method:
this.subscribe("busservice", {});
Along with this, we will do the search as well. To give a small recap, because we are going to filter the same collection in the list template using the values from search template, we have used reactive variables. Whenever there is a search value, we filter the collection and update the reactive variable, which will update the list as per the search.
We will generate search-related templates and helpers using the generator. Run the following command:
iron g:template search
This will generate the search related files in the search directory under app/client/templates. In the Search template inside search.html, replace the existing content with the following code:
The template is ready. To render this search template to the search region in MasterLayout, add the following code to the action method in HomeController:
this.render('Search', {to: 'search'});
This will render the Search template to the search region. Visit the browser and you will see the search form.
We have to introduce the reactive variable. To use reactive variables, we have to install the reactive-var package. Run the following command to add the package:
iron add reactive-var
Add the following initialization code to the beginning of the action method:
this.ReactiveBusServices = new ReactiveVar([]);
The controller supports the data method such as the subscriptions method. In the data method, we can prepare and format data that will be passed to the template to render. Add the following data preparation code to the HomeController:
From the code, you can figure out that we set the Busservice collection to the reactive variable we had created in the preceding snippet and then return the reactive variable. Visit the browser and you will find the list of services you had created earlier.
It is time to put the search in place. In search.js, inside the app/client/templates/search directory, replace the events skeleton with the following code:
Template.Search.events({
"keyup input": _.throttle(function(e) {
var source = $("[name='source']").val().trim(),
destination = $("[name='destination']").val().trim(),
date = $("[name='startDateTime']").val().trim(),
fare = $("[name='fare']").val().trim(),
search = {};
if(source) search.source = {$regex: new RegExp(source), $options: "i"};
if(destination) search.destination = {$regex: new RegExp(destination), $options: "i"};
if(date) {
var userDate = new Date(date);
search.startDateTime = {
$gte: userDate,
$lte: new Date(moment(userDate).add(1, "day").unix()*1000)
}
}
if(fare) search.fare = {$lte: parseInt(fare, 10)};
if(Template.instance()) {
Template.instance().data.set( Busservice.find(search, {sort: {createdAt: -1}}));
}
}, 200),
"submit": function(e) {
e.preventDefault();
}
});
In the input field's keyup handler, we collect the form data, prepare them to be a proper search query, and filter the collection. Then, we set the filtered collection into the reactive variable. If you notice, Template.instance().data is the reactive variable we had returned from the data method of the HomeController. Perform a search and you will find that things are working as expected. Finally, listing and search is done.
Reservation
The last part of the application is blocking and reserving seats. We need the reservations and blocked_seats collections to store the information. Let's use the generator to generate the collection. Run the following commands to generate the collections:
iron g:collection reservationsiron g:collection blockedSeats
These commands will create two files, reservations.js and blocked_seats.js under app/lib/collections. Each file has its own instantiation to the collections, respectively. We will define the schema to each of these collections as we did for the busservice collection.
Add the following schema definition to blocked_seats.js:
Again, we can define validations if needed. I will leave that to you. Similarly, we will add the schema definition for reservations collection as well. Add the following code to reservations.js:
Now, we have to define the route to reach the reservation part. We will use the generator to create the route. Run the following command to generate the route:
iron g:route book
The generator adds a route for us in the routes.js file and creates a BookController, templates, and helpers. The new route generated is not what we needed. Let's modify it to the way we want it to be. Change the route path from book to book/:_id. The route clearly says we need _id, which we will get from the listing. Also, we need the bus service information of the concerned bus, reservation information of the bus, and the blocked seats information of the bus. In the listing, we already have the link to the reservation page. Let's wire the proper data and create the templates to show the seating information and other required information.
We have to register the required publications first. As we did for the busservice collection, we will use generators to generate the publications for the reservations and blocked_seats collections. Run the following commands one after the other to generate them:
iron g:publish reservationsiron g:publish blocked_seats
To the generated publications, we need to do small modifications to pull the data of only the concerned bus service. Modify the code to look like the code as follows:
Meteor.publish('busservice', function (query) {
query = query || {};
return Busservice.find(query, {sort: {createdAt: -1}});
});
Meteor.publish('reservations', function (query) {
return Reservations.find(query);
});
Meteor.publish('blocked_seats', function (query) {
return BlockedSeats.find(query);
});
Note that we have also modified the busservice publication to accommodate the query parameter. Similarly, other publications will also get an object, based on which the data is published.
We will also use CreateTravelLayout for the reservation page. Add the layoutTemplate property to the controller and put the value as CreateTravelLayout.
The next step is subscribing to these data. In BookController, add the following piece of code to subscribe the data in the subscriptions method:
This is the data that is available in the Book template.
Let us create the UI to show the seating arrangement. Replace the content of the Book template in book.html under app/client/templates/book, with the following piece of code:
I don't have to explain this code as you will be familiar with it from the previous chapter. Now, you will be able to see the seating arrangement of the bus in the browser. The only leftover portion is server-side handling.
Add the server methods to methods.js in app/server. We need three server methods that are called from the template handlers. Add the following code to Meteor.methods:
Now, you can block and reserve seats from the application. This not much, but what we have learned so far is pretty interesting, isn't it?
We have recreated the same old application in a much more maintainable way like a pro-developer. We have used advanced scaffolding techniques to build the application and have increases maintainability and predictability. Also, we have learned to secure database operations with the help of allow and deny methods available in the collection instances.
Is that all for this chapter? No. To become a real pro-developer, two more things are essential. One is debugging and another one is testing. We are going to cover both of them in this chapter.