Issue with createRecord in form component


#1

Continuing the discussion from Question on autofocus for form field:

I am trying to do a createRecord based on form fields. As soon as I type in the form field I get errors in the console that state:

“Assertion Failed: Cannot call set with ‘srdescription’ on an undefined object.”

And when I submit the form I get the following error:

‘Cannot read property ‘createRecord’ of undefined’

I am guessing the component doesn’t know about the model maybe? If so how would I fix that?

Here is the create-form.hbs

 <form class="form-material m-t-40" onsubmit={{action "saveRequest"}}>
  <div class="form-group">
      <label>Description of Problem</label>
      {{textarea name='srdescription' value=model.srdescription class="form-control form-control-line" autofocus="true"}}
  </div>
    <div class="form-group">
        <label>Requester Name:</label>
        {{input type="text" class="form-control form-control-line" value=model.requester_name}}
   </div>
   <div class="form-group">
       <label>Requester Phone:</label>
       {{input type="text" class="form-control form-control-line" value=model.requester_phone}}
  </div>
 </form>

Here is the create-form.js

import Component from '@ember/component';

export default Component.extend({
  didInsertElement() {
    this._super(...arguments);
    this.$('[autofocus]').focus();
  },
  actions: {
    saveRequest(ev) {
      ev.preventDefault();
      let servicerequest = this.store.createRecord('servicerequest', this.model);
      servicerequest.save()
        .then (() => {
        this.transitionToRoute('servicerequest');
      });

    }
  }

});

In servicerequests.create hbs I call the component like this {{create-form}}

Any thoughts or suggestions are appreciated.


#2

Components only have access to properties that you pass them when you call them or those they specify themselves. model has no special meaning (and is not defined) in components, so it makes sense that your textarea cannot bind to model.description.

I suggest to use another name and pass it in:

{{create-form user=user ...}}


#3

Based on the original post link info I changed the component to inject the service. Here is what I have now.

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

    export default Component.extend({
      store: service(),
      didInsertElement() {
        this._super(...arguments);
        this.$('[autofocus]').focus();
      },
      actions: {
        saveRequest(event) {
          event.preventDefault();
          let servicerequest = this.get('store').createRecord('servicerequest', this.model);
          servicerequest.save()
            .then (() => {
            this.transitionToRoute('servicerequest');
          });

        }
      }

    });

I am getting an error in the console that the json post returned a 415 empty payload. Looking in the rails console all of the form fields are reading nil. Any ideas why that is happening?


#4

What is this.model in your component? Is it already an ember-data model? If so, then you would just call this.model.save(), not store.createRecord.


#5

Yes it is a model. If I try to do it with this.model.save I get the following error:

‘Cannot read property ‘save’ of undefined’

Here is the model:

      import DS from 'ember-data';

      export default DS.Model.extend({
      requester_name: DS.attr('string'),
      requester_phone: DS.attr('string'),
      location: DS.attr('string'),
      controller: DS.attr('string'),
      unitnumber: DS.attr('number'),
      building: DS.attr('string'),
      srdescription: DS.attr('string'),
      priority: DS.attr('string'),
      status: DS.attr('string'),
      created_at: DS.attr('date'),
      current_user_id: DS.attr(),
      assignedto_user_id: DS.attr()
    });

#6

So there are two basic ways you could approach this:

  1. Create a record outside the component and pass it into the component
  2. Let the component do the createRecord by injecting the store

It really depends on your use case, but I think in general the “better design” would be to create the record outside the component, pass the record into the component, and then pass an action out of the component, where the “owner” of the record does the save (DDAU). If the component is the only thing that ever has a concept of the record, maybe it makes more sense to let the component inject the store, create the record, save the record, and never reference “model” at all.

It seems that by both doing a createRecord in the action, but also referencing “model” in your template, you’re trying to do both. But if you’re not passing a model into the component, it doesn’t know what that means.

In the latter case, where the component manages the record create and save, you could do it one of two ways:

  1. create the record when the component is inserted, reference the created record in your template so the form modifies the record directly, and then just do a “save” in the action.
  2. let the template reference properties on the component, and then in the action do a create record, set the props, and then do a save.

EDIT: I see that you’re also calling transitionToRoute which is not defined on a component (only a controller). This may be another sign that you’d want to use the DDAU approach where you let the route (or controller I guess) create the record, pass the record into the component, and then the component passes the save action back out to the controller, which saves it and does the transition. If you really wanted to do the transition in the component you’d have to inject the new routing service and use that.


#7

Thanks for the info. So I’m new to ember so I’m not sure what the best way is to do all this stuff. I basically have a form that I want to fill out, and have it create a record via a rails api. Then redirect to the main servicerequests page to display a refreshed list of servicerequests.


#8

Yeah IMHO this is one of the hardest aspects of Ember and front-end development in general, because it’s more art than science. In general to make a component more flexible and reusable I think it would make sense to pass a model into the component, and pass an action back out. For example in one of our apps we have form components that are reused for both creating new records and editing existing ones. The form doesn’t care where the model comes from or if it’s new or not, it just takes a model and passes a submit action back out. Then the data owner (aka the route/controller) can figure out what to do with it.

In this case it doesn’t sound like you will need to reuse this component so maybe that’s not as important, but I’d probably still go with a DDAU approach personally. I would probably create a record in the route model hook, pass it into the component, let the component mutate the values, pass an action out on submit, and let the controller do the save and transition. Other people might do it differently, and it sorta depends on the use case.


#9

Actually that sounds exactly what I will need to happen. That’s sort of how it works in pure rails. I have a form that is used for both creating new and editing existing records. I haven’t gotten to the edit functions yet, but that is how I was planning (or hoping) to do it.

So how do you do that? Any examples online that you can point me to?


#10

Nothing public unfortunately but I’ll put some snippets here for the general gist. This is pre-3.1 syntax, and this is a little more stripped down (sometimes I use some mixins or other mechanisms to genericize this, and support things like form validation/disabled button/etc. ) so forgive me if there’s anything weird or broken.

In the below example I’m using the concept of a “draft” record, so if you are at, let’s say a user create route, and you type some stuff and navigate away and then navigate back, it will save your work. If you didn’t want that behavior your model hook could simply return a new record.

Component

form component template:

<form class="form-horizontal" {{action 'submitForm' on="submit"}}>
  ...
  {{!-- e.g. {{input value=record.name placeholder="Name" ...}}, etc. --}}
  ...
  <button type="submit" ...>Submit</button>
</form>

component js:

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

export default Component.extend({
  ...
  actions: {
    submitForm: function(){
      // send the action out to the controller, don't necessarily need to send record out
      let record = this.get('record');
      get(this, 'submitForm')(record);
    }
  }
});

New Route

“new” route:

import { hash } from 'rsvp';
import Route from '@ember/routing/route';
import AuthenticatedRouteMixin from "../../mixins/authenticated-route-mixin";

export default Route.extend(AuthenticatedRouteMixin, {
  model: function(/*params ,transition*/){
    // if there's a record with 'isNew' set, it must have been a previous 'draft', so use
    // that, otherwise make a new 'draft' record
    let newRecord = this.store.peekAll('modelType').findBy('isNew', true);
    newRecord = newRecord || this.store.createRecord('modelType');
    return newRecord;
  },
});

“new” controller:

import Controller from '@ember/controller';

export default Controller.extend({
  ...
  actions: {
    submitForm(record) {
      if(record) {
        record.save().then(saved => {
          ... // if success
          this.transitionToRoute(/* route to redirect to */);
        }).catch(error => {
          // optionall do something with error
        });
      }
    }
  }
});

“new” template

...
{{<form-component-name>
  record=model
  submitForm=(action "submitForm")
}}
...

Edit Route

“edit” route:

import { hash } from 'rsvp';
import Route from '@ember/routing/route';
import AuthenticatedRouteMixin from "../../mixins/authenticated-route-mixin";

export default Route.extend(AuthenticatedRouteMixin, {
  model: function(params /*,transition*/){
    return this.store.findRecord('recordType', params.id);
  },
});

“edit” controller:

import Controller from '@ember/controller';

export default Controller.extend({
  ...
  actions: {
    submitForm(record) {
      if(record) {
        record.save().then(saved => {
          ... // if success
        }).catch(error => {
          // optionall do something with error
        });
      }
    }
  }
});

“edit” template

...
{{<form-component-name>
  record=model
  submitForm=(action "submitForm")
}}
...

EDIT: this is, of course, a very stripped down version. You could pass something into the component that tells it whether it’s a new or edit to change the button text or do validation differently or hide a field or two, etc. You can also disable the form submit button until the record is dirty and/or validation is complete. You could also come up with a sweet contextual form component that yields form elements like the one in ember-bootstrap. There are lots of things you could add, this is just a rough skeleton, but the basic mechanisms are pretty simple.


#11

Thanks a lot I appreciate it. I’ll look at this and play around a bit.