Updating and Deleting

The last two aspects of implementing the full model life-cycle are updating and deleting models.

Updating

Updating is basically a combination of the display of a single module, together with the form functionality of creating a new module. Because you are already familiar with the majority of concepts, the explanations will be kept to a minimum.

We will first implement updating, so create a new route and component-class:

$ ember generate route modules/edit
$ ember generate component -gc edit-module

As with the individual view route, we need to specify an explicit path for this route, to include the dynamic segment. Update the route definition in app/router.js to the following:

this.route('edit', {path: ':mid/edit'});

Now that we have the dynamic segment, we can use that in the app/routes/modules/edit.js to fetch the model to edit:

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class ModulesEditRoute extends Route {
  @service store;
  model(params) {
    return this.store.findRecord('module', params.mid);
  }
}

Which we can then display in the app/templates/modules/edit.hbs:

{{page-title "Edit"}}
<EditModule @model={{this.model}} />

and in the actual component view app/components/edit-module:

<main class='row justify-content-center'>
  <div class='col-lg-6'>
    <h1>Edit {{@model.name}}</h1>
    <form {{on 'submit' this.updateModule}} {{on 'reset' this.resetModule}}>
      <div class='form-group'>
        <label class={{if this.errorCode this.classLabel}}>Code
          <Input
            type='text'
            class='form-control {{if this.errorCode this.classInput}}'
            @value={{@model.code}}
          />
          {{#if this.errorCode}}
            <span class={{this.classSpan}}>{{this.errorCode}}</span>
          {{/if}}
        </label>
        <label class={{if this.errorName this.classLabel}}>Name
          <Input
            type='text'
            class='form-control {{if this.errorName this.classInput}}'
            @value={{@model.name}}
          />
          {{#if this.errorName}}
            <span class={{this.classSpan}}>{{this.errorName}}</span>
          {{/if}}
        </label>
        <label class={{if this.errorSemester this.classLabel}}>Semester
          <select onchange={{action 'setSemester' value='target.value'}}>
            <option value='WS18/19' selected={{eq @model.semester "WS18/19"}}>Wintersemester 18/19</option>
            <option value='SS18' selected={{eq @model.semester "SS18"}}>Summersemester 18</option>
          </select>
          {{#if this.errorSemester}}
            <span class={{this.classSpan}}>{{this.errorSemester}}</span>
          {{/if}}
        </label>
      </div>
      <div class="btn-container">
        <button type='reset' class='btn btn-secondary'>Don't Update</button>
        <button type="submit" role="button">Update</button>
      </div>
    </form>
  </div>
</main>

As you can see, for each input element, we have directly bound the property of the model to that input field. Thus when the value is changed by typing, it is automatically updated in the model – but not saved to the server.

At this point the code will actually not work, because of this line:

<option value="WS18/19" selected={{eq model.semester "WS18/19"}}>Wintersemester 18/19</option>

As you can see we set the selected attribute by checking whether the semester property of the module we are editing is equal to the string "WS18/19". By default Ember does not provide this kind of functionality, so we need to install an additional package. Stop the Ember build server and then run the following:

$ yarn ember install ember-truth-helpers

Now re-start the build server and update the app/component/edit-module.js to handle the updating.

import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';

export default class EditModuleComponent extends Component {
  /**
  * Services
  */
  @service router;
  @service store;

  /**
  * Untracked Properties
  */
  classSpan = 'invalid-feedback';
  classInput = 'is-invalid';
  classLabel = 'text-danger';

  /**
  * Actions
  */

  /**
  *
  * Handle update of the module, reset error messages
  * @param {*} event prevent default behavior
  */
  @action
  updateModule(event) {
    event.preventDefault();
    this.errorCode = '';
    this.errorName = '';
    this.errorSemester = '';
    this.args.model
      .save()
      .then((model) => {
        this.router.transitionTo('modules.view', model.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;
          }
        });
      });
  }

  /**
  *
  * Handle reset of module to previouse state
  * @param {*} event prevents default behavior
  */
  @action
  resetModule(event) {
    event.preventDefault();
    if (this.args.model.hasDirtyAttributes) {
      this.args.model.rollbackAttributes();
    }
    this.router.transitionTo('modules.view', this.args.model.id);
  }

  /**
  * Sets the semester to the param's value
  * @param {String} semester value of the semester
  */
  @action
  setSemester(semester) {
      this.args.model.semester = semester;
  }
}

As you can see the code is relatively minimal. This is because we directly bound the model properties to the input fields and all we need to do is call save() to save the update model to the server and then re-direct to the individual modules view. Should you wish to discard the changes, there is a second action, called resetModule. This action checks if there are any unsaved changes via the hasDirtyAttributes property. Should there be any, they are discarded by calling the rollbackAttributes function. In any case the user is brought back to the view page of this specific module.

All that remains to be done is to create an edit link in the app/components/view-module.hbs:

<main class='col-lg-9'>
  <div class='row'>
    <div class='col-12'>
      <h1>{{@module.code}} {{@module.name}}</h1>
      <dl>
        <dt>Semester</dt>
        <dd>{{@module.semester}}</dd>
        <dt>Contact</dt>
        <dd>{{@module.teacher.email}}</dd>
      </dl>
      <LinkTo role="button" @route='modules.edit' @model={{@module.id}}>Edit</LinkTo>
    </div>
  </div>
</main>

You can now try out updating a module. Unlike creating new modules, the individual module view is immediately updated after saving. This is because the findRecord call is smart enough to remember which models have been loaded and updating them, when they are changed.

Deleting

The final step is to implement the “delete” action. As a reminder, last week, we added a list of actions to the model in app/models/module.js:

import Model, { attr, belongsTo} from '@ember-data/model';

export default class ModuleModel extends Model {
    @attr code;
    @attr name;
    @attr semester;

    @belongsTo('user') teacher;

    get sections() {
      return {
        dates: {
          title: 'Dates',
          icon: 'mdi mdi-calendar-clock',
        },
        documents: {
          title: 'Documents',
          icon: 'mdi mdi-file-document-box-multiple-outline',
        },
        exercises: {
          title: 'Exercises',
          icon: 'mdi mdi-test-tube',
        },
        students: {
          title: 'TeilnehmerInnen',
          icon: 'mdi mdi-account-multiple',
        },
      };
    }

    get actions() {
        return {
            delete: {
                icon: 'mdi mdi-delete warning',
                title: 'Delete',
                action: 'delete'
            }
        }
    }
}

If you look at the list of modules, you will see that the delete action is shown, but nothing happens if you click on it. Thus update the app/components/modules/modules-list.hbs to call an action in the “actions” area:

<nav aria-label='Actions for {{module.name}}'>
  <ul role='menu' class='horizontal' aria-label='Actions'>
    {{#each-in module.actions as |actions display|}}
      <li><a
          onclick={{action display.action module}}
          role='menuitem'
          aria-label={{display.title}}
          title={{display.title}}
          class={{display.icon}}
          tabindex='0'
        ></a></li>
    {{/each-in}}
  </ul>
</nav>

As you can see, here we call a dynamic action where the name is taken from what we defined in the actions getter. We also pass the module as a parameter to the action, using the action-helper that basically behaves like the on-helper and listens to click events. All we need now is a controller to handle this action:

$ yarn ember generate component-class modules/modules-list

Then add the following code to handle the delete action in app/components/modules/modules-list.js:

import Component from '@glimmer/component';
import { action } from '@ember/object';

export default class ModulesModulesListComponent extends Component {

  @action
  delete(module) {
    module.deleteRecord();
    module.save();
  }
}

Because we provided the module as a parameter to the action, when handing it, we can simply call the deleteRecord() to delete that module. deleteRecord() does not actually delete the record on the server side, for that we need to call save(), it just marks the record for deletion.

You can now try this out to see that we have implemented the full model life-cycle.