Reuse a component for display and create actions

I have a component that I call from a for loop in a template and use just to display a model values:

# templates/holiday_hours.hbs

<form {{action "updateHolidayHours" on="submit"}}>
      {{#each model as |holidayHour|}}
        {{holiday-row holiday=holidayHour}}
      {{/each}}
...

Like this, I display several rows with values to update. I’d like to make it possible to a create a new row and reuse the same component template.

Here is the template code

<form {{action "updateHolidayHours" on="submit"}}>
      {{#each model as |holidayHour|}}
        {{holiday-row holiday=holidayHour}}
      {{/each}}

      {{#if isAddingHoliday}}
        <form {{action "createHoliday" on="submit"}}>
          {{#holiday-row holiday=model}}
            <div class="form-check sm-2">
              <button type="button" type="submit" class="btn btn-success btn-sm">
                <span class="oi oi-plus"></span>
                {{t 'buttons.add'}}
              </button>
            </div>

            <div class="form-check ml-sm-2">
              <button type="button" class="btn btn-danger btn-sm" onclick={{action "cancelAddHoliday"}}>
                <span class="oi oi-x"></span>
                {{t 'buttons.cancel'}}
              </button>
            </div>
          {{/holiday-row}}
        </form>
      {{else}}
        <button type="button" class="btn btn-success btn-sm mb-2" onclick={{action "addHoliday"}}>
          <span class="oi oi-plus"></span>
          {{t 'buttons.new.language'}}
        </button>
      {{/if}}
      <div class="float-right">
        <button type="submit" class="btn btn-success">{{t 'buttons.save'}}</button>
      </div>
    </form>

isAddingHoliday is a just a flag to flip to indicate if I should display a new row with empty fields or not. Here is how it is triggered in the corresponding controller:

# controllers/holiday-hours.js

 import Controller from '@ember/controller';
import { inject as service } from '@ember/service';

export default Controller.extend({
  currentShop: service('current-shop'),
  isAddingHoliday: false,

  actions: {
    addHoliday() {
      this.set('isAddingHoliday', true);
      this.set('model', this.store.createRecord('holiday-hour'));
    },

    cancelAddHoliday() {
      this.set('isAddingHoliday', false);
    }
  }
});

I tried to call

 this.set('model', this.store.createRecord('holiday-hour'));

But it erased my previously loaded model values. Here is how it looks before clicking on New holiday button:

Here is what it looks like after clicking on New holiday button:

Here is what it looks like after clicking on Cancel button:

The model hook in the route handler is very basic:

#routes/holiday-hours.js
...
model() {
    return this.store.query('holiday-hour', { shop_id: this.get('currentShop.shop').get('id')});
  },

What is the right way to do that ? Thank you.

You are replacing the query result (from your routes model hook) with a single newly created record inside addHoliday. This is why the previous items are being replaced.

One option would be to stash the temporary holiday-hour record on some other property (e.g. don’t clobber model)…

Thank you for your response. That’s what I was almost sure in. I wonder what is the right way to reuse (if possible) a component in such a case ? I’ve never heard about

stash the temporary holiday-hour record on some other property

Or may be one of the right solutions would be to create a separate nested route holiday-hours/new ? …

I tried to assign this.store.createRecord('holiday-hour')); to a new variable in the init method of the controller:

controllers/holiday-hours.js

export default Controller.extend({
  currentShop: service('current-shop'),
  isAddingHoliday: false,
  newHoliday: null,

  actions: {
    addHoliday() {
      this.set('isAddingHoliday', true);
      this.set('newHoliday', this.store.createRecord('holiday-hour'));
    },

    cancelAddHoliday() {
      this.set('isAddingHoliday', false);
      this.set('newHoliday', null);
    }
  }
});

Then in the template, I just pass in newHoliday to the component:

#templates/holiday-hours.hbs

{{#if isAddingHoliday}}
        {{#holiday-row holiday=newHoliday}}
          <div class="form-group row">
            <div class="col-sm-2">
              <button type="button" type="submit" class="btn btn-success btn-sm" {{action 'createHoliday' holiday}}>
                <span class="oi oi-plus"></span>
                {{t 'buttons.add'}}
              </button>
            </div>

            <div class="col-sm-2">
              <button type="button" class="btn btn-danger btn-sm" onclick={{action "cancelAddHoliday"}}>
                <span class="oi oi-x"></span>
                {{t 'buttons.cancel'}}
              </button>
            </div>
          </div>
        {{/holiday-row}}

      {{else}}
        <button type="button" class="btn btn-info btn-sm mb-2" onclick={{action "addHoliday"}}>
          <span class="oi oi-plus"></span>
          {{t 'buttons.new.holiday'}}
        </button>
      {{/if}}

But it does not work because all the values of the submitted form are null. :frowning:

#routes/holiday-hours.js
...
actions: {

    async createHoliday(holiday) {
      var route = this;
      var controller = this.get('controller');

      let newHoliday = this.store.createRecord('holiday-hour', {
        shop: this.get('currentShop.shop')
      });

      console.log('newHoliday: ' + newHoliday);

      try {
        await newHoliday.save();
        route.get('flashMessages').success(route.get('i18n').t('flash.holiday.added'));
        route.refresh();
      } catch(response) {
        let error = response.errors[0];
        if (error.status !== "422") {
          route.get('flashMessages').danger(error.message);
        }
      }
    },

I’getting closer to get it work. I managed to have all the vales submitted but one, - state. Here is how the states drop-down list is implemented in the template:

# templates/components/holiday-row.hbs

<select class="form-control" onchange={{action "selectState" value="target.value"}}>
      {{#each states as |state|}}
        <option value={{state.id}} selected={{eq state.id holiday.state}}>{{state.name}}</option>
      {{/each}}
 </select>

where states come from constants service:

# services/constants.js
import Service from '@ember/service';
import { inject as service } from '@ember/service';

export default Service.extend({
    i18n: service('i18n'),

    init() {
      this._super(...arguments);
      this.set('states',
        [
          {id: 'open', name: this.get('i18n').t('states.open')},
          {id: 'closed', name: this.get('i18n').t('states.closed')},
          {id: 'divided', name: this.get('i18n').t('states.divided')}
        ]
      );
...

I guess that it is because of state value is not bound to the model that I don’t get its value :frowning: So how is it possible to:

  • display all states values available as constants
  • bind a state value to the state of the holiday.

I figured out how to achieve that. I just set the state in init action of the holiday-hours.js controller:

# controllers/holiday-hours.js

actions: {
    addHoliday() {
      this.set('isAddingHoliday', true);
      this.set('newHoliday', this.store.createRecord('holiday-hour', {
        state: 'open'
      }));
    },
1 Like