How to use a component for new/edit template

I still can’t figure out how to implement and correctly use a component in case where you need the same form to create or edit a Post, for example? The most difficult part is still calling the right action depending on if it is a new Post (should create) or an existing one (should update). I found it to be so basic and frequent in use that it would be grateful it to be a part of the docs.

Here is Posts new template:

#templates/posts/new.hbs
{{#post-form post=model postAction=(action "savePost")}}
  <button type="submit" class="btn btn-success">Save</button>
{{/post-form}}

So here is a component template:

#templates/components/post-form.hbs

<form>
  <div class="form-row">
    <div class="form-group col-sm-6">
      <label for="title">Title</label>
      {{input type="text" class="form-control" value=post.title}}
    </div>
  </div>
  <div class="form-row">
    <div class="form-group col-sm-6">
      <label for="body">Text</label>
      {{textarea type="text"  class="form-control" value=post.body}}
    </div>
  </div>
  <div class="form-row">
    <div class="form-group col-sm-6">
      {{#power-select-multiple
        options=tags
        selected=selectedTags
        searchField="label"
        placeholder="Select some tags..."
        onchange=(action "selectTag")
      as |tag|
      }}
        {{tag.label}}
      {{/power-select-multiple}}
    </div>
  </div>
  <div class="form-row">
    <div class="form-group form-check col-sm-6">
      {{input type="checkbox" name="archived" checked=post.archived}}
      <label class="form-check-label" for="published">Archived</label>
    </div>
  </div>

  {{yield}}
</form>

Post form component JS file:

#components/post-form.js
import Component from '@ember/component';

export default Component.extend({
  tagName: '',

  submit(event) {
    event.preventDefault();
    this.postAction(this.get('post'));
  }
});

Here is posts/new route:

# routes/posts/new.js
import Route from '@ember/routing/route';

export default Route.extend({
  model() {
    return this.store.createRecord('post')
  },

  actions: {
    async savePost() {
      var route = this;
      let controller = this.get('controller');

      let post = route.modelFor(route.routeName);
      let selectedTags = controller.get('selectedTags');
      post.set('tag_ids', selectedTags.mapBy('id'));

      await post.save();
      this.transitionTo('posts');
    }
  }
});

And finally, posts/new controller:

#controllers/posts/new.js
import Controller from '@ember/controller';
import { A } from '@ember/array';
import EmberObject from '@ember/object';

export default Controller.extend({
  init() {
    this._super(...arguments);
    this.selectedTags = [];
    this.tags = this._dummyTags();
  },

  actions: {
    selectTag(tags) {
      this.set('selectedTags', A(tags));
    },

    savePost() {

    }
  },

  _dummyTags() {
    let foot = EmberObject.create({
      id: 11,
      label: 'Football'
    });
    let voley = EmberObject.create({
      id: 12,
      label: 'Voleyball'
    });
    let handball = EmberObject.create({
      id: 13,
      label: 'Handball'
    });

    let tags = [foot, voley, handball]

    return tags;
  }
});

It DOES not work, compilation errors, etc. What is wrong with all that ? What I’d like is just to reuse the same component for creating or editing a Post.

Move your savePost action from the route to the controller.

2 Likes

In the code, when you submit the form, you want to call call post.save(), regardless of whether it’s a new or existing record. So IMO, the component submit action should look like this:

#components/post-form.js
import Component from '@ember/component';

export default Component.extend({
  tagName: '',
  onSave() {},
  onError() {}, # callers can pass onSave/onError actions to customize behavior
  submit(event) {
    event.preventDefault();
    this.get('post').save().then(this.onSave).catch((response) => {
      # TODO: show errors
      this.onError(response);
    });
  }
});

(An example of a “smarter” component, which has some behavior of its own instead of relying on callers to define all actions.)

Thank you guys ! I moved save, update actions from the routes to corresponding controllers and it works. The only issue I have is that the list of created items is not refreshed after calling transitionToRoute. I have to hit CMD-R to see a newly added item.

Here is the code from the controller that saves a new item:

#controllers/country-events/new.js

actions: {
    async save() {
      let event = this.get('model.event');

      let selectedSports = event.get('selectedSports');
      event.set('sport_ids', selectedSports.mapBy('id'));
      event.set('modifiedBy', this.get('currentUser.user').get('username'));

      await event.save();
      this.get('flashMessages').success(this.get('i18n').t('flash.event.added'));
      await this.transitionToRoute('country-events');
    }
  }

And here is the route:

# routes/country-events.js

model() {
    return this.get('currentShop.shop.country').then(country => {
      return this.store.query('event', { country_id: country.get('id')});
    });
  }

What am I missing ? Thank you

@belgoros this is probably because you are using store.query which returns a static array of results. You’ll need to either refresh the route when you transition to it from country-events/new OR kick off the query but then actually return store.peekAll('event') (which live updates with new records) and then filter that list in a computed property.

@dknutsen I don’t see how it is possible to refresh a route fro the controller. What is the more correct way to do that? - modify my model hook, something else ? Thank you.

I found the help on Discord help channel (thanks to @locks)

I had to move the content routes/country-events.js route to routes/country-events/index.js and remove it completely.

1 Like