Dynamic Routes
==============
Up to this point we have basically been dealing with routes that show us specific functionality or a list of all elements. The next step is to define a route that allows us to load a specific data-point from the backend API. To do so, we first need to create a new route to view an individual module:
.. code-block:: console
$ yarn ember generate route modules/view
Previously when we have created a new route, we have kept the default route definition. This time, we need to update it to include a dynamic component that we can then use to load a specific module from the backend. Open the file ``app/router.js``, which will look something like this:
.. code-block:: js
import EmberRouter from '@ember/routing/router';
import config from './config/environment';
const Router = EmberRouter.extend({
location: config.locationType,
rootURL: config.rootURL
});
Router.map(function() {
this.route('login');
this.route('modules', function() {
this.route('view');
});
this.route('users', function() {
this.route('register');
});
});
export default Router;
The core route definition happens in the ``Router.map`` function call and you can see the nesting structure of the routes. What we need to do now is to update the definition of the ``view`` route to include a dynamic segment in the path:
.. code-block:: js
Router.map(function() {
this.route('login');
this.route('modules', function() {
this.route('view', {path: ':mid'});
});
this.route('users', function() {
this.route('register');
});
});
By default the URL path for the route is the same as the name of the route. However, we can override this by providing a configuration object with the explicit ``path``. By using the ``':mid'`` as the path definition, we define that this path segment is to match anything and that whatever it matches is to be stored under the key "mid" (we will see how to access that key in a second).
We can now use this new route definition with the dynamic segment in the ``app/components/modules-list.hbs`` to link each module in the list to an individual module view:
.. code-block:: html+handlebars
{{module.code}}
|
{{module.name}}
|
As you can see, we still use the ``LinkTo`` component, except that now in addition to the name of the route to navigate to, we also provide the ``id`` property of each ``module`` as a model parameter. This will be used in generate URL as the ``':mid'`` segment. If you now click on the link, it will take you to the view for that route, which is currently empty. However, you will see that in the URL, the last part of the path now contains the identifier of the module.
We can now use that to fetch a single record from the backend. This works as with the list of records in the ``modules/index`` route. Update the ``app/routes/modules/view.js`` and update it so that it looks like this:
.. code-block:: js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ModulesViewRoute extends Route {
@service store;
model(params) {
return this.store.findRecord('module', params.mid);
}
}
Accessing the dynamic segment values works similarly to the filtering of the query. We get a ``params`` parameter to our ``model()`` function and this parameter has a property with the same name as the dynamic segment we defined in the router, in this case ``mid``. We simply pass that to the ``findRecord`` function of the ``store`` and it will query the backend to retrieve a single module.
To see that this works, we also need to display the individual fields of the single module. Update the ``app/templates/modules/view.hbs`` to look like this:
.. code-block:: html+handlebars
{{page-title "View"}}
Now we need to create this component by running
.. code-block:: console
$ yarn ember generate component view-module
and then populate it with this content
.. code-block:: html+handlebars
{{@module.code}} {{@module.name}}
- Semester
- {{@module.semester}}
- Contact
- {{@module.teacher.email}}
As with the ``LinkTo`` component, we can also use transition to dynamic routes from within the component. To do this, we will implement the necessary functionality to create a new module.
First we need to create a new route and component with class:
.. code-block:: console
$ yarn ember generate route modules/new
$ yarn ember generate component -gc new-module
Next, we need to update the ``app/components/modules-navbar`` to add a link to the new route:
.. code-block:: html+handlebars
Enroll in Module
Create a new Module
Then add the following into the ``app/templates/modules/new``:
.. code-block:: html+handlebars
{{page-title "New"}}
and update ``app/components/new-model.hbs`` to:
.. code-block:: html+handlebars
Next, add the following code into the ``app/components/new-module.js``:
.. code-block:: js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
export default class NewModuleComponent extends Component {
/**
* Services
*/
@service store;
@service router;
/**
* Tracked Properties
*/
@tracked newCode = '';
@tracked newName = '';
@tracked newSemester = 'WS18/19';
@tracked errorCode = '';
@tracked errorName = '';
@tracked errorSemester = '';
/**
* Untracked Properties
*/
classSpan = 'invalid-feedback';
classLabel = 'text-danger';
classInput = 'is-invalid';
/**
*
* Handle submit action and create new module
* Reset error messages
* @param {*} event used to prevent default behavior
*/
@action
createModule(event) {
event.preventDefault();
let newModule = this.store.createRecord('module', {
code: this.newCode,
name: this.newName,
semester: this.newSemester,
teacher: this.args.model,
});
this.errorCode = '';
this.errorName = '';
this.errorSemester = '';
newModule
.save()
.then((module) => {
this.router.transitionTo('modules.view', module.id);
})
.catch((response) => {
response.errors.forEach((error) => {
if (error.source.pointer == '/data/attributes/code') {
this.errorCode = error.detail;
} else if (error.source.pointer == '/data/attributes/name') {
this.errorName = error.detail;
} else if (error.source.pointer == '/data/attributes/semester') {
this.errorSemester = error.detail;
}
});
});
}
/**
*
* Sets value for semester on change
* @param {String} value semester
*/
@action
setNewSemester(value) {
this.newSemester = value;
}
}
Two things are important in this code. First, as in our module model definition, the module is linked to a user via the ``teacher`` relationship. Thus to create a new module, we need to provide this as well and we use ``this.args.model`` to access it. In order for this to work, we actually need to load a user in the ``app/routes/modules/new.js``:
.. code-block:: js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ModulesNewRoute extends Route {
@service store;
model() {
return this.store.findRecord('user', 1);
}
}
The other difference there is the use of ``this.router.transitionTo('modules.view', module.id)`` to transition to the individual modules view, again passing the ``id`` property of the newly created ``module``.
If you try this now and go back to the list of all modules, you will see that your created module is listed among the others.