Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Meteor error


May 10, 2021 Meteor


Table of contents


Error

Using only the alert() conversation window to warn users that their submission is wrong is a little unsatisfactory and clearly not a good user experience. We can do better.

Instead, let's create a more flexible error reporting mechanism to better tell the user what's going on without interrupting the process.

We're going to implement a simple system that displays a new error message in the upper right corner of the window, similar to the popular Mac OS application Growl.

Describes the local collection

At first, we need to create a collection to store our errors. S ince the error is only relevant to the current session and does not need to last in any way, we're going to do something new here and create a local collection. This means that the Errors collection will only exist in the browser and will not attempt to synchronize back to the server.

To do this, we client in the client folder (to make sure that the collection exists only on the client side), and we name its MongoDB collection null the collection's data will not be saved in the server-side database):

// 本地(仅客户端)集合
Errors = new Mongo.Collection(null);

At first, we should build a collection that can store errors. B etween errors is just for the current session, we will take a timeliness set. This means that the error collection exists only in the current browser and is not synchronized with the service side.

Now that the collection has been established, we can create throwError function to add a new error. We don't allow worry deny and deny or any other security considerations, because this collection is "local" to the current user.

throwError = function(message) {
  Errors.insert({message: message});
};

The advantage of using local collections to store errors is that, like all collections, it is responsive ———— which means that we can display errors in the same way as any other collection data.

The error is displayed

We'll insert an error message at the top of the main layout:

<template name="layout">
  <div class="container">
    {{> header}}
    {{> errors}}
    <div id="main" class="row-fluid">
      {{> yield}}
    </div>
  </div>
</template>

Let's errors.html and error templates in errors error

<template name="errors">
  <div class="errors">
    {{#each errors}}
      {{> error}}
    {{/each}}
  </div>
</template>

<template name="error">
  <div class="alert alert-danger" role="alert">
    <button type="button" class="close" data-dismiss="alert">&times;</button>
    {{message}}
  </div>
</template>

Twin template

You may notice that we have two templates in one file. U ntil now we've been following the "one file, one template" standard, but for Meteor, it's the same for Meteor to put all the templates in the same file (but that makes the main.html code very main.html )。

In the current case, because both error templates are small, we make an exception to put them in a file to make our repo code base cleaner.

We just need to add our template helper to make it great!

Template.errors.helpers({
  errors: function() {
    return Errors.find();
  }
});

You can try testing our new error message manually. Open the browser console and enter:

throwError("我就是一个错误!");

Two types of errors

At this point, it is important to distinguish between app-level errors and code-level errors.

Application-level errors are typically triggered by the user, who is able to take action against the disease. T hese include things like validation errors, permission errors, "not found" errors, and so on. This is the kind of error you want to show to users to help them solve any problems they just encounter.

Code-level errors, as another type, are triggered by actual code bugs that are not expected, and you may not want to present the error directly to the user, but instead track the error through, for example, a third-party error tracking service, such as Kadira.

In this chapter, we focus on dealing with the first type of error, rather than catching bugs.

The error was created

We know how to display errors, but we also need to trigger them before we find them. I n fact, we've created a good error situation: repeating the post's warning. Let's simply replace the alert call in postSubmit helper with the new throwError function:

Template.postSubmit.events({
  'submit form': function(e) {
    e.preventDefault();

    var post = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val()
    };

    Meteor.call('postInsert', post, function(error, result) {
      // display the error to the user and abort
      if (error)
        return throwError(error.reason);

      // show this result but route anyway
      if (result.postExists)
        throwError('This link has already been posted');

      Router.go('postPage', {_id: result._id});
    });
  }
});

Now that we're here, postEdit thing for postEdit event helper:

Template.postEdit.events({
  'submit form': function(e) {
    e.preventDefault();

    var currentPostId = this._id;

    var postProperties = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val()
    }

    Posts.update(currentPostId, {$set: postProperties}, function(error) {
      if (error) {
        // display the error to the user
        throwError(error.reason);
      } else {
        Router.go('postPage', {_id: currentPostId});
      }
    });
  },
  //...
});

Try it yourself: Try to create a post and enter a URL http://meteor.com Because this URL already exists, you can see:

Clean up the error

You'll notice that the error message automatically disappears after a few seconds. This is because of the magic of some of the CSS we added to the style sheet at the beginning of this book:

@keyframes fadeOut {
  0% {opacity: 0;}
  10% {opacity: 1;}
  90% {opacity: 1;}
  100% {opacity: 0;}
}

//...

.alert {
  animation: fadeOut 2700ms ease-in 0s 1 forwards;
  //...
}

We defined a fadeOut CSS animation with four frame transparency attribute changes (0%, 10%, 90%, and 100% throughout the animation process, respectively) and .alert style.

The animation is 2700 milliseconds long, ease-in with a 0-second delay, runs once, and ends at the last frame when the animation is complete.

Animation vs Animation

You might be wondering why we use CSS-based animations (pre-defined, and outside our app control) instead of Meteor itself.

While Meteor does provide support for inserting animations, we want to focus on errors in this chapter. So now we're using "stupid" CSS animation, and we're going to leave the more glitzy stuff in the animation chapters.

This works, but if you want to trigger multiple errors (for example, by submitting the same connection three times), you'll see the error messages stacked together.

This is .alert the .alert element visually disappears, it remains in the DOM. We need to fix the problem.

This is where Meteor glows. Since Errors collection is responsive, all we have to do is remove the old error from the collection!

We use Meteor.setTimeout specify that a callback function is executed after a certain amount of time (current case, 3000 milliseconds).

Template.errors.helpers({
  errors: function() {
    return Errors.find();
  }
});

Template.error.onRendered(function() {
  var error = this.data;
  Meteor.setTimeout(function () {
    Errors.remove(error._id);
  }, 3000);
});

Once the template is rendered in the browser, onRendered callback function is triggered. Where this this to the current template instance, this.data the data of the currently rendered object (in this case, an error).

Seek validation

So far, we haven't validated the form. A t the very least, we want users to provide URLs and titles for new posts. Then we make sure they do.

Let's do two things: First, we give the parent div of any problematic form field a has-error CSS class. Second, we display a useful error message below the field.

First, we'll postSubmit template to include these new helpers:

<template name="postSubmit">
  <form class="main form">
    <div class="form-group {{errorClass 'url'}}">
      <label class="control-label" for="url">URL</label>
      <div class="controls">
          <input name="url" id="url" type="text" value="" placeholder="Your URL" class="form-control"/>
          <span class="help-block">{{errorMessage 'url'}}</span>
      </div>
    </div>
    <div class="form-group {{errorClass 'title'}}">
      <label class="control-label" for="title">Title</label>
      <div class="controls">
          <input name="title" id="title" type="text" value="" placeholder="Name your post" class="form-control"/>
          <span class="help-block">{{errorMessage 'title'}}</span>
      </div>
    </div>
    <input type="submit" value="Submit" class="btn btn-primary"/>
  </form>
</template>

Notice that we pass parameters url title to each helper. This allows us to reuse the same helper twice and modify its behavior based on parameters.

Now it's the fun part: making these helpers really do something.

We use Session session to store postSubmitErrors When a user uses a form, the object changes, which is to update the form code and content in a responsive manner.

First, when postSubmit template is created, we initialize the object. This ensures that the user does not see the old error message left over from the last time the page was visited.

Then define our two template helpers, keeping an eye on the field Session.get('postSubmitErrors') field url or title on how we call helper).

errorMessage the message itself, and errorClass checks to see if the message exists and, if true, has-error

Template.postSubmit.onCreated(function() {
  Session.set('postSubmitErrors', {});
});

Template.postSubmit.helpers({
  errorMessage: function(field) {
    return Session.get('postSubmitErrors')[field];
  },
  errorClass: function (field) {
    return !!Session.get('postSubmitErrors')[field] ? 'has-error' : '';
  }
});

//...

You can test whether the helper is working properly, open the browser console, and enter the following code:

Session.set('postSubmitErrors', {title: 'Warning! Intruder detected. Now releasing robo-dogs.'});

Next, tie postSubmitErrors Session session object to the form.

Before we begin, we add a new validatePost function to posts.js to post object and return an error object that contains any errors (that is, whether the title or url field is not filled in):

//...

validatePost = function (post) {
  var errors = {};

  if (!post.title)
    errors.title = "请填写标题";

  if (!post.url)
    errors.url =  "请填写 URL";

  return errors;
}

//...

Let's call this function through the postSubmit event helper:

Template.postSubmit.events({
  'submit form': function(e) {
    e.preventDefault();

    var post = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val()
    };

    var errors = validatePost(post);
    if (errors.title || errors.url)
      return Session.set('postSubmitErrors', errors);

    Meteor.call('postInsert', post, function(error, result) {
      // 向用户显示错误信息并终止
      if (error)
        return throwError(error.reason);

      // 显示这个结果且继续跳转
      if (result.postExists)
        throwError('This link has already been posted');

      Router.go('postPage', {_id: result._id});
    });
  }
});

Note that if any errors occur, we terminate return execution with return instead of we want to actually return this value.

Server-side authentication

We're not done yet. W e verify that the URL and title exist on the client side, but on the server side? After all, someone will still try to manually call the postInsert method by entering an empty post through the browser console.

Even if we don't need to display any error messages on the server side, we still need to take advantage validatePost function. Except this time we call it within the postInsert method, not just in the event helper:

Meteor.methods({
  postInsert: function(postAttributes) {
    check(this.userId, String);
    check(postAttributes, {
      title: String,
      url: String
    });

    var errors = validatePost(postAttributes);
    if (errors.title || errors.url)
      throw new Meteor.Error('invalid-post', "你必须为你的帖子填写标题和 URL");

    var postWithSameLink = Posts.findOne({url: postAttributes.url});
    if (postWithSameLink) {
      return {
        postExists: true,
        _id: postWithSameLink._id
      }
    }

    var user = Meteor.user();
    var post = _.extend(postAttributes, {
      userId: user._id,
      author: user.username,
      submitted: new Date()
    });

    var postId = Posts.insert(post);

    return {
      _id: postId
    };
  }
});

Again, users don't normally have to see the message "You have to fill in the title and URL for your post." This will only appear if the user wants to bypass the user interface we painstakingly created and use the browser directly.

To test, open the browser console and enter a post without a URL:

Meteor.call('postInsert', {url: '', title: 'No URL here!'});

If we do it well, you'll get a bunch of scary codes and a message that says, "You have to fill in the title and URL for your post."

Edit validation

For added refinement, we added the same validation for the Post Edit form. T he code looks very similar. First, the template:

<template name="postEdit">
  <form class="main form">
    <div class="form-group {{errorClass 'url'}}">
      <label class="control-label" for="url">URL</label>
      <div class="controls">
          <input name="url" id="url" type="text" value="{{url}}" placeholder="Your URL" class="form-control"/>
          <span class="help-block">{{errorMessage 'url'}}</span>
      </div>
    </div>
    <div class="form-group {{errorClass 'title'}}">
      <label class="control-label" for="title">Title</label>
      <div class="controls">
          <input name="title" id="title" type="text" value="{{title}}" placeholder="Name your post" class="form-control"/>
          <span class="help-block">{{errorMessage 'title'}}</span>
      </div>
    </div>
    <input type="submit" value="Submit" class="btn btn-primary submit"/>
    <hr/>
    <a class="btn btn-danger delete" href="#">Delete post</a>
  </form>
</template>

Then there's the template helper:

Template.postEdit.onCreated(function() {
  Session.set('postEditErrors', {});
});

Template.postEdit.helpers({
  errorMessage: function(field) {
    return Session.get('postEditErrors')[field];
  },
  errorClass: function (field) {
    return !!Session.get('postEditErrors')[field] ? 'has-error' : '';
  }
});

Template.postEdit.events({
  'submit form': function(e) {
    e.preventDefault();

    var currentPostId = this._id;

    var postProperties = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val()
    }

    var errors = validatePost(postProperties);
    if (errors.title || errors.url)
      return Session.set('postEditErrors', errors);

    Posts.update(currentPostId, {$set: postProperties}, function(error) {
      if (error) {
        // 向用户显示错误消息
        throwError(error.reason);
      } else {
        Router.go('postPage', {_id: currentPostId});
      }
    });
  },

  'click .delete': function(e) {
    e.preventDefault();

    if (confirm("Delete this post?")) {
      var currentPostId = this._id;
      Posts.remove(currentPostId);
      Router.go('postsList');
    }
  }
});

Just like we did for the post submission form, we also want to verify the post on the server side. Keep in mind that we're not editing posts in one way, but are calling directly from update

This means that we have to add deny callback function:

//...

Posts.deny({
  update: function(userId, post, fieldNames, modifier) {
    var errors = validatePost(modifier.$set);
    return errors.title || errors.url;
  }
});

//...

Note that the parameter post to a post that already exists. W e wanted to verify the $set we called validatePost in the $set property of modifier (just like Posts.update({$set: {title: ..., url: ...}}) )。

This works because modifier.$set the post and url properties as the title url object. Of course, this does mean that only partial updates to title or url possible, but in practice there should be no problem.

You may notice that this is our second deny callback. W hen multiple deny are added, if any one of the callbacks true the run fails. In this case, this update title if it faces both the title and url fields, and these fields cannot be empty.