Build a blog with Areto Node.js framework

Article controller

The ArticleController class is responsible for rendering, search, sort and filter articles.

controllers/ArticleController.js

'use strict';
const Base = require('areto/base/Controller');
module.exports = class ArticleController extends Base {
  // place methods here
};
module.exports.init(module);
const ActiveDataProvider = require('areto/data/ActiveDataProvider');
const Article = require('../models/Article');

The following method validates the article identifier.

controllers/ArticleController.js

...
static isValidId (id) {
  return id && /^[a-f0-9]{24}$/.test(id);
}
...

THe getModel method searches for a article model by ID passed in the GET request this.getQueryParam('id'). The optional relations argument contains an array of relations that should be loaded together with the model. User will receive 404 error this.throwNotFound() if an article could not be found.

controllers/ArticleController.js

...
getModel (cb, ...relations) {
  let id = this.getQueryParam('id');
  if (this.constructor.isValidId(id)) {
    Article.findById(id).with(relations).one((err, model)=> {
      err ? this.throwError(err)
          : model ? cb(model) : this.throwNotFound();
    });
  } else {
    this.throwNotFound();
  }  
}
...

The actionIndex method lists of articles. Class areto/data/ActiveDataProvider is used for sorting and pagination. Parameter query contains a query that searches for a list of published articles.

Use the sort option to display a list of items in a different order. Select article's attributes to be sorted in the attrs property. For the default sort order using defaultOrder.

By default, the ActiveDataProvider class uses a pagination (10 items per page). This is required for a eye-candy display and reduce the load of the database. Use pageSize parameter to select a different page size. If you want the entire list, set pagination: null.

controllers/ArticleController.js

...
actionIndex () {
  let provider = new ActiveDataProvider({
    controller: this,
    query: Article.findPublished(),
    sort: {
      attrs: {
        date: true,
        title: true
      },
      defaultOrder: {date: -1}
    },
    pagination: {
      pageSize: 15
    }    
  });
  provider.prepare(err => {
    err ? this.throwError(err) : this.render('index', {provider});
  });
}
...

The actionSearch method lists articles filtered by the search text. This implementation is similar to the actionIndex method except query option.

controllers/ArticleController.js

...
actionSearch () {
  let provider = new ActiveDataProvider({
    controller: this,
    query: Article.findBySearch(this.getQueryParam('text')),
    sort: {
      attrs: {
        date: true,
        title: true
      },
      defaultOrder: {date: -1}
    }
  });
  provider.prepare(err => {
    err ? this.throwError(err) : this.render('index', {provider});
  });
}
...

The actionView method displays a single article. If a request is received in the POST format (isPost), it is treated as a form to create a new comment.

To create a new comment, load the form data (comment.load(this.getBodyParams())) to the Comment class object. Set user's IP and an article that relates with the comment.

After a comment has been successfully saved, the user will receive a message (setFlash). The message is saved in the session and will be once displayed the next time page loads. Finally, reload the page this.redirect to reset the current POST request. If saving fails, the error will be shown on the comment's form.

controllers/ArticleController.js

...
actionView () {
  this.getModel(model => {
    let Comment = require('../models/Comment');
    let comment = new Comment;
    if (this.isPost()) {
      comment.load(this.getBodyParams());
      comment.set('articleId', model.getId());
      comment.set('ip', this.req.ip);
      comment.save(err => {
        if (err) {
          return this.throwError(err);
        }
        if (comment.hasError()) {
          this.view(model, comment);
        } else {
          this.setFlash('comment-done', 'You message has been sent successfully!');
          this.redirect(['view', model]);
        }
      });
    } else {
      this.view(model, comment);
    }
  }, 'mainPhoto', 'photos', 'tags');
}
...

The view method displays an article contained in the model argument. Comments related to the article, are displayed via the ActiveDataProvider provider that generates paged list.

controllers/ArticleController.js

...
view (model, comment) {
  let comments = new ActiveDataProvider({
    controller: this,
    query: model.relComments()
  });
  comments.prepare(err => {
    err ? this.throwError(err) : this.render('view', {model, comments, comment});
  });
}
...

The actionTagged method lists articles filtered by tag.

First, find the model tag by its name. This name is passed in the GET-query parameter tag. Create a new Tag model and set the name attribute by request data. Validate the data. If there are no errors, then create a search query by tag name. If a tag model is found, initialize the ActiveDataProvider provider. As a data source query, select the tag.relArticles() relation that returns articles with this tag.

controllers/ArticleController.js

...
actionTagged () {
  let tagName = this.getQueryParam('tag');  
  let Tag = require('../models/Tag');
  let tag = new Tag;
  tag.set('name', tagName);
  tag.validate(err => {
    if (err) {
      return this.throwError(err);
    }
    if (tag.hasError()) {
      return this.render('tagged', {tagName});
    }
    Tag.find({name: tagName}).one((err, tag)=> {
      if (err) {
        return this.throwError(err);
      }
      if (!tag) {
        return this.render('tagged', {tagName});
      }
      let provider = new ActiveDataProvider({
        controller: this,
        query: tag.relArticles(),
        sort: {
          attrs: {
            date: true,
            title: true
          },
          defaultOrder: {date: -1}
        }
      });
      provider.prepare(err => {
        err ? this.throwError(err) : this.render('tagged', {provider, tagName});
      });
    });
  });
}
...