Contents


Mastering MEAN

MEAN and UGLI CRUD with responsive web design

Content series:

This content is part # of # in the series: Mastering MEAN

Stay tuned for additional content in this series.

This content is part of the series:Mastering MEAN

Stay tuned for additional content in this series.

Now that you're familiar with the mechanics of a MEAN application, it's time to start customizing the MEAN.JS app that you created in the first installment of this series and toured in the second. In this third installment, I demonstrate basic CRUD functionality for the app. You'll also learn a bit about responsive web design and Bootstrap.

The application you'll be building for the rest of this series is affectionately dubbed UGLI: the User Group List and Information app. I've been running the HTML5 Denver User Group since 2010 (and the Boulder Java User Group before that, and the Denver Java User Group before that), so it should come as no surprise that I'm a big fan of local user groups. What I am surprised about is the lack of dedicated software for running a user group. (It's the cobbler's kids who are always barefoot, right?) It's time to remedy that problem.

Many user groups have found an online home at Meetup.com. My goal with this MEAN and UGLI application isn't to replace Meetup.com; rather, I'd like to integrate deeply with it. Meetup.com excels at most of the core functionality you need to run a successful user group: registering new users, publishing meeting details, handling RSVPs, and so on. But a few key pieces of functionality are still missing for user group leaders, including managing a list of presenters and linking out to slide decks. UGLI will fill in the gaps. (See Download to get the complete sample code.)

Adjusting the branding

The first task in making your application UGLI is to adjust the app's branding. Some changes are needed on the server side of the application in the config and app directories; others are required on the client side in the public directory.

Start with the metadata stored in config/env/all.js. Change the title to HTML5 Denver (or the user group of your choice) and the description to HTML5 Denver User Group, as shown in Listing 1.

Listing 1. config/env/all.js
'use strict';

module.exports = {
    app: {
        title: 'HTML5 Denver',
        description: 'HTML5 Denver User Group',
        keywords: 'MongoDB, Express, AngularJS, Node.js'
    },

The title in config/env/development.js also needs the change, as shown in Listing 2. As you learned last time, development.js and all.js are merged at runtime.

Listing 2. config/env/development.js
'use strict';

module.exports = {
    db: 'mongodb://localhost/test-dev',
    app: {
        title: 'HTML5 Denver'
    },

Next, change the brand displayed in the upper-left corner of the navigation bar. To do so, edit public/modules/core/views/header.client.view.html. Find the anchor tag with the navbar-brand class at around line 9 and change the body to HTML5 Denver, as shown in Listing 3.

Listing 3. public/modules/core/views/header.client.view.html
<div class="container" data-ng-controller="HeaderController">
    <div class="navbar-header">
        <button class="navbar-toggle" type="button" data-ng-click="toggleCollapsibleMenu()">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
        </button>
        <a href="/#!/" class="navbar-brand">HTML5 Denver</a>
    </div>

    <!-- ...snip... -->
</div>

To verify your changes, start MongoDB by typing mongod at the command line, and then start your app by typing grunt. View the web app in a browser to see your brand show up in the menu and in the title bar.

To complete the branding changes, you need to replace the boilerplate text in public/modules/core/views/home.client.view.html that shows up in the body of the home page. Create a copy named home.client.view.html.original so that you can refer back to it (if you need to) at a later time.

This file uses the power of the Bootstrap framework to ensure that your website is mobile-ready from the start. Before you continue, it helps to be familiar with the 12-column grid layout that Bootstrap offers.

Understanding Bootstrap and responsive web design

Look at any hard-copy newspaper or magazine, and you'll see columns in use. Sometimes, an image or headline spans more than one column for some design flair, but a basic columnar layout is the foundation of almost every printed page.

Web pages are no different. For example, visit the TIME website. You see a column-based layout initially. But notice what happens as you reduce the width of your browser window from full screen to extremely narrow. The number of visible columns shrinks as you make the window smaller and grows as you make the window bigger.

This effect is called responsive web design, because the web page responds and adjusts its design to the screen size of the device that requested it. The modern web developer builds websites that seamlessly flow from the smallest handheld device to the largest screen sitting on a desk or hanging on a wall. Actively creating separate, discrete websites for smartphones, tablets, laptops, and so on — and using separate http://m.* and http://www.* URLs — is an obsolete twentieth-century strategy.

Responsive web design isn't a one-size-fits-all strategy; rather, it's a "one website that looks and feels right on all of your devices" strategy. You don't get to choose which type of device your users visit your website with, so it's crucial that your design have the built-in flexibility to adjust itself accordingly.

Many popular websites (including Facebook and Instagram) are visited more often from mobile devices than from traditional computers. Twitter's user base is predominately mobile. Twitter canonicalized its responsive web design strategy and made it open source as Bootstrap. Bootstrap features a 12-column layout that can grow or shrink based on the CSS classes you use to define the columns.

Notice the four columns for MongoDB, Express, AngularJS, and Node.js on the MEAN.JS sample application's home page, shown in Figure 1.

Figure 1. Example of Bootstrap's columnar layout
Screenshot of an example of Bootstrap's columnar layout
Screenshot of an example of Bootstrap's columnar layout

Now look at the source of public/modules/core/views/home.client.view.html, shown in Listing 4, to see Bootstrap's 12-column layout in action.

Listing 4. public/modules/core/views/home.client.view.html
<div class="row">
    <div class="col-md-3">
        <h2><strong>M</strong>ongoDB</h2>
    </div>
    <div class="col-md-3">
        <h2><strong>E</strong>xpress</h2>
    </div>
    <div class="col-md-3">
        <h2><strong>A</strong>ngularJS</h2>
    </div>
    <div class="col-md-3">
        <h2><strong>N</strong>ode.js</h2>
    </div>
</div>

If you add class="row" to a parent div, you can add class="col-xx-N" attributes to the child divs to divide them up into columns. The N value must be a number between 1 and 12, and the xx value depends on which size of device you want to optimize the layout for:

  • xs for extra-small devices (less than 768 pixels wide)
  • sm for small devices (between 768 and 991 pixels)
  • md for medium devices (between 992 and 1,199 pixels)
  • lg for large devices (1,200 pixels or larger)

See the Grid system section of the Bootstrap CSS documentation for more information.

Because each column in Listing 4 is optimized for medium (md) devices, if you visit this page on a device that has a screen width of less than 992 pixels, the columns stack vertically instead of horizontally. Make your browser window narrow enough to trigger this change, as shown in Figure 2.

Figure 2. Example of responsive web design on a mobile device
Screenshot of an example of responsive web design on a mobile device
Screenshot of an example of responsive web design on a mobile device

Now, using what you've learned up to this point, it's time to replace the boilerplate text in home.client.view.html with some UGLI-specific text.

To begin, download the 256-pixel HTML5 logo from the W3C HTML5 logo page and copy it to public/modules/core/img/brand/HTML5_Logo_256.png. Then replace the existing HTML in public/modules/core/views/home.client.view.html with the source in Listing 5.

Listing 5. public/modules/core/views/home.client.view.html
<section data-ng-controller="HomeController">
    <div class="jumbotron text-center">
        <div class="row">
            <div class="col-md-4">
                <img alt="HTML5" class="img-responsive center-block" 
                src="modules/core/img/brand/HTML5_Logo_256.png" />
            </div>
            <div class="col-md-8">
                <h1>The HTML story is still being written.</h1> 
                <h2><em>Come hear the latest chapter at the HTML5 Denver User Group.</em></h2>
            </div>
        </div>
    </div>
</section>

When you view the website in a wide browser window, the HTML5 logo appears next to the text, as shown in Figure 3.

Figure 3. The new UGLI homepage
Screenshot of the new UGLI homepage
Screenshot of the new UGLI homepage

When you make your browser window narrow enough, the logo stacks on top of the text, as shown in Figure 4.

Figure 4. The new UGLI homepage, as it will appear on a mobile device
Screenshot of the new UGLI homepage as it appears on a mobile device
Screenshot of the new UGLI homepage as it appears on a mobile device

See how easy it is to make your website mobile-friendly with Bootstrap? Bootstrap is the foundation of every new website I build for my clients.

Now it's time to tackle CRUD in the MEAN stack.

Basic CRUD

Meetup.com does a great job of helping me manage user group events. But after an event has passed, the temporal aspect of the event is less important than the talks that happened that night.

In other words, one user story for this website is: "What's going to be discussed at the next meeting?" This user story is perfectly satisfied by Meetup.com as-is.

A second user story — "Show me all of the talks related to the MEAN stack, regardless of when they happened" — is the one I aim to solve with the UGLI application. To implement this story, you must create a CRUD infrastructure around a new model object named Talk. Thankfully, a Yeoman generator is available to help put this infrastructure in place.

In the root directory of your application, type yo meanjs:crud-module talks. In response to the prompts:

  1. Select all four supplemental folders (css, img, directives, and filters).
  2. Answer Yes to add the CRUD module links to a menu.
  3. Accept the default (topbar) when the generator asks you which menu you'd like to use.

Listing 6 shows the interactive command-line sequence.

Listing 6. Generating a new CRUD module with the Yeoman generator
$ yo meanjs:crud-module talks
[?] Which supplemental folders would you like to include in your angular module? 
css, img, directives, filters
[?] Would you like to add the CRUD module links to a menu? Yes
[?] What is your menu identifier? topbar
   create app/controllers/talks.server.controller.js
   create app/models/talk.server.model.js
   create app/routes/talks.server.routes.js
   create app/tests/talk.server.model.test.js
   create public/modules/talks/config/talks.client.routes.js
   create public/modules/talks/controllers/talks.client.controller.js
   create public/modules/talks/services/talks.client.service.js
   create public/modules/talks/tests/talks.client.controller.test.js
   create public/modules/talks/config/talks.client.config.js
   create public/modules/talks/views/create-talk.client.view.html
   create public/modules/talks/views/edit-talk.client.view.html
   create public/modules/talks/views/list-talks.client.view.html
   create public/modules/talks/views/view-talk.client.view.html
   create public/modules/talks/talks.client.module.js

Notice in Listing 6 that the generator creates the server-side infrastructure that you need (stored in the app directory): routes, a controller, a model, and a unit test. It also builds out all of the client-side artifacts under the public/modules/talks directory.

You'll add some custom fields to the Talk object in a moment. Before you do, see what you get by default by visiting the website in your browser.

Click the Signin link in the upper-right corner and type in the username and password you created earlier in the series, or click Signup and create a new set of credentials.

After you are logged in, you can see a Talks menu in the upper left. Choose New Talk from the menu to open an HTML form that offers a lonely Name field, as shown in Figure 5.

Figure 5. The New Talk form, before customization
Screenshot of the New Talk form, before customization
Screenshot of the New Talk form, before customization

This is a good start, but you'll need more than a simple text field to capture all of the attributes of a Talk.

Adding new fields for persistence

To add new fields to a Talk, you must edit six files — four for display and two for persistence:

  • app/models/talk.server.model.js
  • public/modules/controllers/talks.client.controller.js
  • public/modules/talks/views/create-talk.client.view.html
  • public/modules/talks/views/edit-talk.client.view.html
  • public/modules/talks/views/view-talk.client.view.html
  • public/modules/talks/views/list-talks.client.view.html

I'll tackle persistence first. Half of the solution is on the server side and half on the client side.

The server-side model (defined in app/models/talk.server.model.js) is your application's source of truth. In it, you'll name fields, provide data types, add validation rules, and so on.

The client-side controller (defined in public/modules/controllers/talks.client.controller.js) gathers the data input from the user and pushes the data up to the server via HTTP requests. The controller also pulls in the JSON data over the wire and hands it over to the views for presentation.

What's interesting about this architecture is that the model object never leaves the server. Instead, the object is materialized from data sent up from the client, and it's serialized to JSON in an HTTP response.

This application has two controllers — one on the server side, the other on the client-side — but only the client-side controller is of interest. The server-side controller simply pours the incoming JSON into a model object, so you don't need to make any adjustments to the server-side controller when you add additional fields to your model. The client-side controller requires a bit of adjustment to accommodate the new fields.

Open app/models/talk.server.model.js to add new fields to the server-side model, as shown in Listing 7. You can see the expected name field (as shown in Figure 5) defined alongside two more metadata fields: created and user.

Listing 7. app/models/talk.server.model.js
/**
 * Talk Schema
 */
var TalkSchema = new Schema({
    name: {
        type: String,
        default: '',
        required: 'Please fill Talk name',
        trim: true
    },
    created: {
        type: Date,
        default: Date.now
    },
    user: {
        type: Schema.ObjectId,
        ref: 'User'
    }
});

The JSON-based schema is fairly self-explanatory. When you define a new field, you can specify a data type, a default value, an error message to display for required fields, and many other refinements. See the Mongoose documentation for more information.

Add new fields for description, presenter, and slidesUrl, as shown in Listing 8. In this case, description and presenter are both required fields. The slidesUrl field is optional.

Listing 8. app/models/talk.server.model.js
/**
 * Talk Schema
 */
var TalkSchema = new Schema({
    name: {

        type: String,
        default: '',
        required: 'Please fill Talk name',
        trim: true
    },
    description: {
        type: String,
        default: '',
        required: 'Please fill Talk description',
        trim: true
    },  
    presenter: {
        type: String,
        default: '',
        required: 'Please fill Talk presenter',
        trim: true
    },
    slidesUrl: {
        type: String,
        default: '',
        trim: true
    },
    created: {
        type: Date,
        default: Date.now
    },
    user: {
        type: Schema.ObjectId,
        ref: 'User'
    }
});

At this point, your server-side back end is ready to accept the new fields. Now you need to tackle your client-side controller. Open public/modules/controllers/talks.client.controller.js and add the new fields, as shown in Listing 9.

Listing 9. public/modules/controllers/talks.client.controller.js
// Create new Talk
$scope.create = function() {
    // Create new Talk object
    var talk = new Talks ({
        name: this.name,
        description: this.description,
        presenter: this.presenter,
        slidesUrl: this.slidesUrl
    });

    // Redirect after save
    talk.$save(function(response) {
        $location.path('talks/' + response._id);
    }, function(errorResponse) {
        $scope.error = errorResponse.data.message;
    });

    // Clear form fields
    this.name = '';
    this.description = '';
    this.presenter = '';
    this.slidesUrl = '';
};

The $scope.create function is where the form fields are aggregated into a JSON object to be sent up to the server for persistence. After you add the corresponding fields from your model to the controller, you've got the persistence part of the story wrapped up.

Now it's time to shift your focus to the presentation tier so that your users can see and interact with the new fields.

Adding new fields for display

Look in public/modules/talks/views/. Four files there relate to the CRUD lifecycle:

  • create-talk.client.view.html
  • edit-talk.client.view.html
  • view-talk.client.view.html
  • list-talks.client.view.html

Open create-talk.client.view.html, shown in Listing 10.

Listing 10. Generated create-talk.client.view.html
<section data-ng-controller="TalksController">
  <div class="page-header">
    <h1>New Talk</h1>
  </div>
  <div class="col-md-12">
    <form class="form-horizontal" data-ng-submit="create()" novalidate>
      <fieldset>
        <div class="form-group">
          <label class="control-label" for="name">Name</label>
          <div class="controls">
            <input type="text" data-ng-model="name" id="name" class="form-control" 
            placeholder="Name" required>
          </div>
        </div>
        <div class="form-group">
          <input type="submit" class="btn btn-default">
        </div>
        <div data-ng-show="error" class="text-danger">
          <strong data-ng-bind="error"></strong>
        </div>
      </fieldset>
    </form>
  </div>
</section>

Copy the block of code related to the Name field three times to support Description, Presenter, and slidesUrl, as shown in Listing 11. Notice that I made the Description field a textarea instead of a simple text field. Also, I removed the required attribute from the slidesUrl field and changed the input type from text to url.

Listing 11. Updated create-talk.client.view.html
<section data-ng-controller="TalksController">
  <div class="page-header">
    <h1>New Talk</h1>
  </div>
  <div class="col-md-12">
    <form class="form-horizontal" data-ng-submit="create()" novalidate>
      <fieldset>
        <div class="form-group">
          <label class="control-label" for="name">Name</label>
          <div class="controls">
            <input type="text" data-ng-model="name" id="name" class="form-control" 
            placeholder="Name" required>
          </div>
        </div>
        <div class="form-group">
          <label class="control-label" for="description">Description</label>
          <div class="controls">
            <textarea data-ng-model="description" id="description" class="form-control" 
            placeholder="Description" required></textarea>
          </div>
        </div>
        <div class="form-group">
          <label class="control-label" for="presenter">Presenter</label>
          <div class="controls">
            <input type="text" data-ng-model="presenter" id="presenter" class="form-control" 
            placeholder="Presenter" required>
          </div>
        </div>
        <div class="form-group">
          <label class="control-label" for="slidesUrl">Slides</label>
          <div class="controls">
            <input type="url" data-ng-model="slidesUrl" id="slidesUrl" class="form-control" 
            placeholder="Slides Url">
          </div>
        </div>                        
        <div class="form-group">
          <input type="submit" class="btn btn-default">
        </div>
        <div data-ng-show="error" class="text-danger">
          <strong data-ng-bind="error"></strong>
        </div>
      </fieldset>
    </form>
  </div>
</section>

In a web browser, your newly modified New Talk page looks like Figure 6.

Figure 6. The New Talk form, after customization
Screenshot of the New Talk form, after customization
Screenshot of the New Talk form, after customization

When you are satisfied with your changes, open edit-talk.client.view.html and make the corresponding changes there, as shown in Listing 12.

Listing 12. edit-talk.client.view.html
<div class="col-md-12">
    <form class="form-horizontal" data-ng-submit="update()" novalidate>
      <fieldset>
        <div class="form-group">
          <label class="control-label" for="name">Name</label>
          <div class="controls">
            <input type="text" data-ng-model="talk.name" id="name" class="form-control" 
            placeholder="Name" required>
          </div>
        </div>
        <div class="form-group">
          <label class="control-label" for="description">Description</label>
          <div class="controls">
            <textarea data-ng-model="talk.description" id="description" class="form-control" 
            placeholder="Description" required></textarea>
          </div>
        </div>
        <div class="form-group">
          <label class="control-label" for="presenter">Presenter</label>
          <div class="controls">
            <input type="text" data-ng-model="talk.presenter" id="name" class="form-control" 
            placeholder="Presenter" required>
          </div>
        </div>
        <div class="form-group">
          <label class="control-label" for="slidesUrl">Slides</label>
          <div class="controls">
            <input type="url" data-ng-model="talk.slidesUrl" id="name" class="form-control" 
            placeholder="Slides Url">
          </div>
        </div>
        <div class="form-group">
          <input type="submit" value="Update" class="btn btn-default">
        </div>
        <div data-ng-show="error" class="text-danger">
          <strong data-ng-bind="error"></strong>
        </div>
      </fieldset>
    </form>
</div>

Notice that the HTML for editing is slightly different from the creation form that you modified earlier. When editing, you already have a Talk object, so the data-ng-model attributes refer to fields in a fully qualified manner, such as talk.name instead of name. View your changes in a web browser, as shown in Figure 7.

Figure 7. The Edit Talk form, after customization
Screenshot of the Edit Talk form, after customization
Screenshot of the Edit Talk form, after customization

The view-talk.client.view.html page is the read-only view of the object. This view is where users land after they save a new Talk, update an existing Talk, or select the Talk from the list page. Make the changes shown in Listing 13.

Listing 13. edit-talk.client.view.html
<div class="page-header">
  <h1 data-ng-bind="talk.name"></h1>
  <h2><em>by {{talk.presenter}} 
    <span ng-if="talk.slidesUrl !== '' ">[<a href="{{talk.slidesUrl}}">slides</a>]</span></em></h2>
  <p>{{talk.description}}</p>              
</div>

Recall that the slidesUrl field is optional. In the view page, you are using the ng-if directive to display the field conditionally if it is populated. Verify this behavior by viewing the page in your browser, as shown in Figure 8.

Figure 8. The View Talk form, after customization
Screenshot of the View Talk form, after customization
Screenshot of the View Talk form, after customization

The last view you need to tweak is the list view. Open list-talks.client.view.html and make the adjustments shown in Listing 14.

Listing 14. list-talks.client.view.html
<div class="list-group">
    <a data-ng-repeat="talk in talks" data-ng-href="#!/talks/{{talk._id}}" class="list-group-item">
    <h4 class="list-group-item-heading" data-ng-bind="talk.name"></h4>
        <p><em>by {{talk.presenter}}</em></p>
    </a>
</div>

Notice that the data-ng-repeat directive is used to display each talk in the list of talks returned from the server. View the results in your browser, as shown in Figure 9.

Figure 9. The List Talks form, after customization
Screenshot of the List Talks form, after customization
Screenshot of the List Talks form, after customization

Conclusion

At this point, you have a good grasp on how the various pieces of the MEAN stack interact. You're using the responsive web design capabilities of Bootstrap to ensure that your website looks good on all devices, not just the traditional ones that have 101 keys and a mouse. And you've experienced the power and convenience of using a Yeoman generator to add a new CRUD module to your application. The generator puts all of the raw artifacts in the correct directories, leaving you the modest task of customizing them.

In the next installment, you'll see how easy it is to incorporate data from remote sources into your application. Specifically, you'll begin pulling in event data from Meetup.com directly via Meetup's RESTful API. Until then, have fun mastering MEAN.


Downloadable resources


Related topics

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development, Open source
ArticleID=983387
ArticleTitle=Mastering MEAN: MEAN and UGLI CRUD with responsive web design
publish-date=09162014