24
Controllers

Controllers are the last piece of the MVC pattern. As you learned in Chapter 19, controllers hold application logic, retrieve model instances and give them to views, and contain handler functions that make changes to model instances.

The controllers you will build in this chapter will not be particularly large pieces of code. That is the idea behind MVC: distributing the complexity of an application to the places it belongs. Managing the data is the job of the models, and handling the UI is the province of the views. The controller only needs to, well, control the models and views.

Without your knowledge, Ember has been adding controller objects to your application when it is running. Controllers are a proxy between the route object and the template, passing the model through. When you do not add a controller object, Ember knows that the model data is sufficient to pass to the template, and it does that for you.

Creating a controller in Ember allows you to define the events or actions to listen for when the route is active. It also allows you to define decorator properties to augment model data you want to display without persisting it.

One of the goals of the Tracker application is for the user to be able to create new sightings. For this goal, you will need to create a route, a controller, controller properties, and controller actions.

You have already created the new sighting route, app/routes/sightings/new.js. For this page, you are you going to create a new sighting record and load the collections of cryptids and witnesses. Each new sighting will need the relationships of belonging to a cryptid and having one or many witnesses. The form you will create will look like Figure 24.1.

Figure 24.1  Tracker’s New Sighting form

Tracker’s New Sighting form

When you have all of that set up, you will create a controller to manage events from the new sighting form. You will also expand on your work to allow existing sightings to be edited and deleted.

New Sightings

The SightingsRoute’s model that you have set up returns all the sightings. For the new sightings model, you will return the result of creating a single new, empty sighting. Also, you will return a set of Promises for the cryptids and witnesses. To do this you will return Ember.RSVP.hash({}).

Let’s get started. Open app/routes/sightings/new.js and add a model hook to return a collection of Promises as an Ember.RSVP.hash:

...
export default Ember.Route.extend({
  model() {
    return Ember.RSVP.hash({
      sighting: this.store.createRecord('sighting')
    });
  }
});

When this route is active, a new record of a sighting is returned. If you were to return to the sightings, you would see a blank entry, because you created a new record. You will handle the dirty records (model data that has been changed but not saved to the persisted source) toward the end of this chapter. For now, know that createRecord has added a new sighting to the local collection.

When creating a new sighting, you will need the list of cryptids and witnesses. Here, Ember.RSVP.hash({}) is used to say you are returning a hash of Promises. The only key is sighting, which means that your model reference in the template will need to do a look-up on model.sighting to reference the sighting record you created.

Add the retrieval methods for cryptids and witnesses to this hash (do not neglect the comma after this.store.createRecord('sighting')).

...
export default Ember.Route.extend({
  model() {
    return Ember.RSVP.hash({
      sighting: this.store.createRecord('sighting'),
      cryptids: this.store.findAll('cryptid'),
      witnesses: this.store.findAll('witness')
    });
  }
}

Next, you are going to use <select> tags in your new sightings template to present the lists of cryptids and witnesses to the user. But before you set up your template with the new model data, you will need an Ember CLI plug-in that makes it easy to use <select> tags with bound properties.

From the command line, install emberx-select:

ember install emberx-select

You will use this component, usually called x-select, in your template. This saves you from writing onchange actions for each <select> tag.

Restart ember server before using the x-select component.

With all the model data set up, you can now edit the template, app/templates/sightings/new.hbs, to create the new sighting form:

<h1>New Route</h1>
<h1>New Sighting</h1>
<form>
  <div class="form-group">
    <label for="name">Cryptid</label>
    {{#x-select value=model.sighting.cryptid class="form-control"}}
      {{#x-option}}Select Cryptid{{/x-option}}
      {{#each model.cryptids as |cryptid|}}
        {{#x-option value=cryptid}}{{cryptid.name}}{{/x-option}}
      {{/each}}
    {{/x-select}}
  </div>
  <div class="form-group">
    <label>Witnesses</label>
    {{#x-select value=model.sighting.witnesses multiple=true class="form-control"}}
      {{#x-option}}Select Witnesses{{/x-option}}
      {{#each model.witnesses as |witness|}}
        {{#x-option value=witness}}{{witness.fullName}}{{/x-option}}
      {{/each}}
    {{/x-select}}
  </div>
  <div class="form-group">
    <label for="location">Location</label> {{input value=model.sighting.location
      type="text" class="form-control" name="location" required=true}}
  </div>
</form>

Wow! That has got everything you have been working on – and more. The route is using a new Ember.RSVP method, the template is using helpers, and the new {{x-select}} and {{x-option}} components are used.

The {{x-select}} component is built to use the <select> element to assign a value to a property. It is designed to work just like a <select> element in the Ember data-binding environment. You assign the value to the sightings model’s cryptid property and the component will handle the onchange event when a new option is selected. This works because the cryptid property needs a cryptid model record as its value.

For the witnesses, there is an extra attribute, multiple=true, which will allow your users to select multiple witnesses for a sighting. Multiple selections will translate into a collection of hasMany witnesses.

Before you go any further, you will need to add a link to the sightings route template so you have a way to get to the sightings.new route. Open app/templates/sightings.hbs and take care of that.

<h1>Sightings</h1>
<div class="row">
  <div class="col-xs-6">
    <h1>Sightings</h1>
  </div>
  <div class="col-xs-6 h1">
    {{#link-to "sightings.new" class="pull-right btn btn-primary"}}
      New Sighting
    {{/link-to}}
  </div>
</div>
{{outlet}}

Your link takes advantage of the simplicity of Bootstrap formatting to create a button. Load http://​localhost:4200/​sightings to see it (Figure 24.2).

Figure 24.2  New Sighting button

New Sighting button

Now you have the ability to navigate to the sightings.new route. The new button adds a link to create a new sighting from anywhere in the sightings route tree structure.

Also, notice that when you are on the sightings.new route, the button is active. Ember has thought of it all, giving an active class to a link even when the current route is the link’s route. Having an active state on a button or link signifies the current route in the form of UI cues.

Actions are the key to handling form events – or any other events in your app. The actions property is a hash where you assign functions to keys. The keys will be used in the templates to trigger the callback.

You are ready to create the controller for the sightings.new route, using this terminal command:

ember g controller sightings/new

Ember creates the app/controllers/sightings/new.js file. Open it and add the create and cancel actions you will need to create sightings:

import Ember from 'ember';

export default Ember.Controller.extend({
  actions: {
    create() {
    },
    cancel() {
    }
  }
});

When creating a form element, the action attribute usually has a URL to which you submit the form. With Ember, the form element only needs the name of a function to run.

In app/templates/sightings/new.hbs, have the form element set the action for submit. Also, add Create and Cancel buttons:

<h1>New Sighting</h1>
<form {{action "create" on="submit"}}>
...
  <div class="form-group">
    <label for="location">Location</label> {{input value=model.location
      type="text" class="form-control" name="location" required=true}}
  </div>
  <button type="submit" class="btn btn-primary btn-block">Create</button>
  <button {{action 'cancel'}} class="btn btn-link btn-block">Cancel</button>
</form>

Atom may complain about the syntax of the lines with the {{action}} helpers. You can ignore its complaints. (You can also install Language-Mustache and enable it in Atom’s preferences so that Atom recognizes this syntax. The package can be found at atom.io/​packages/​language-mustache.)

The two {{action}} helpers have string arguments matching the actions you created in app/controllers/sightings/new.js. Actions are bound to event handlers. The {{action}} helper accepts an argument, on, to which the action is assigned. A click event listener is assigned to the action if you do not add the on argument to the helper.

In the new sightings form, you added on="submit" to specify that the create action will be called on submit. You did not give an on argument to the cancel button, on the other hand, so the event bound with the action will be a click.

Your form now works to submit and cancel using controller actions, but those actions need some code. Start with the create action. Update app/controllers/sightings/new.js to complete the sighting, save it, and return to the sightings listing.

...
  actions: {
    create() {
      var self = this;
      this.get('model.sighting').save().then(function() {
        self.transitionToRoute('sightings');
      });
    },
    cancel() {
    }
  }
});

When the form is submitted, create is called. First, you set a reference to the controller with self. Next, you get the sighting model and call save.

The last step is saving to the persistent source. There is a flag on the model for hasDirtyAttributes that is set to false when a model is saved.

Saving a model returns a Promise. You chained that Promise with then, which takes a function to be called when the model has been saved successfully. Finally, you return to the sightings listing with transitionToRoute.

Check out your form at http://​localhost:4200/​sightings/​new (Figure 24.3).

Figure 24.3  New Sighting form

New Sighting form

Fill in the form and click Create. Although you successfully added a new sighting, your sightings list route model is still returning the records created inline. Open app/routes/sightings.js, delete the dummy data, and replace it with a call to the store to retrieve the sightings.

import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    let record1 = this.store.createRecord('sighting', {
      location: 'Atlanta',
      sightedAt: new Date('2016-02-09')
    });
    record1.set('location', 'Paris, France');
    console.log("Record 1 location: " + record1.get('location'));

    let record2 = this.store.createRecord('sighting', {
      location: 'Calloway',
      sightedAt: new Date('2016-03-14')
    });

    let record3 = this.store.createRecord('sighting', {
      location: '',
      sightedAt: new Date('2016-03-21')
    });

    return [record1, record2, record3];
    return this.store.findAll('sighting', {reload: true});
  }
});

Now your app has creation and retrieval.

Notice the second argument of findAll, the object literal with the single key reload. This argument tells the store to request fresh data from the API each time the route model is called. Adding this argument makes it explicit that you always want the freshest data each time you view the list.

Next, the cancel action needs to delete the in-memory, dirty sighting instance. You will use model.deleteRecord, as you did in Chapter 21. Add it to app/controllers/sightings/new.js.

...
  actions: {
    create() {
      var self = this;
      this.get('model.sighting').save().then(function() {
        self.transitionToRoute('sightings');
      });
    },
    cancel() {
      this.get('model.sighting').deleteRecord();
      this.transitionToRoute('sightings');
    }
  }
...

After deleting the record, the user will be returned to the listing. This scenario works when the user clicks the cancel button, but what happens when the top navigation is used to go back to the listing or another route?

If you do not destroy the dirty record, it will stay in memory while the user’s session is active. To destroy the record you will use an action in the route.

In the lifecycle of a route, there are actions called at different states and transitions. You can override these actions to customize callbacks for different stages of your route transition.

Open app/routes/sightings/new.js and override the willTransition action to ensure that dirty records are deleted:

...
  model(){
    return Ember.RSVP.hash({
      sighting: this.store.createRecord('sighting'),
      cryptids: this.store.findAll('cryptid'),
      witnesses: this.store.findAll('witness')
    });
  },
  actions: {
    willTransition() {
      var sighting = this.get('controller.model.sighting');
      if(sighting.get('hasDirtyAttributes')){
        sighting.deleteRecord();
      }
    }
  }
});

The action willTransition will fire when the route changes. Using deleteRecord will destroy the model object, but only when the model property hasDirtyAttributes is true.

You have now covered your bases with a dirty record on creation. You are also set up to save to a persistent data source with minimal changes to the controller.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.131.255