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.
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.
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 Promise
s 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 Promise
s
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 Promise
s. 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).
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).
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.
18.188.131.255