I'm having somewhat of a predicament. I'm using Rails 4.2.3 in hand with AngularJS. I have a form that needs to upload nested attributes for an associated data model and ALSO upload an image. As I've come to find out, it isn't an easy task to upload files using AJAX requests, I'm using ng-file-upload to address this, however I'm having some fundamental usage problems.
My Rails model:
class Job < ActiveRecord::Base
has_many :references, dependent: :destroy
accepts_nested_attributes_for :references, limit: 5
end
class Reference < ActiveRecord::Base
belongs_to :job
end
Rails expects this format when submitting the form:
Parameters: {"job"=>{"title"=>"Example Title", "company"=>"Example Company", "description"=>"Example Description", "references_attributes"=>{"0"=>{"reference"=>"Example Reference", "id"=>"1"}}, "image"=> #plus the image data here}
With ng-file-upload, it seems almost impossible to include nested attributes (or perhaps I am missing something, I have read the documentation more than once in an attempt to find anything on this). I've resorted to this somewhat hacky means of including nested attributes in a way that Rails can understand. Form below:
<div class="row">
<div class="col-md-6 col-md-offset-3">
<form ng-submit="save(job)" accept-charset="UTF-8" enctype="multipart/form-data">
<label for="job_title">Title</label>
<input ng-model="job.title"
class="form-control"
id="job_title"
name="job[title]"
type="text"
required />
<div ng-repeat="error in errors.title" class="alert alert-danger">Title {{error}}</div>
<label for="job_company">Company</label>
<input ng-model="job.company"
class="form-control"
id="job_company"
name="job[company]"
type="text"
required />
<div ng-repeat="error in errors.company" class="alert alert-danger">Company {{error}}</div>
<label for="job_years">Years</label>
<input ng-model="job.years"
class="form-control"
id="job_years"
name="job[years]"
type="text" />
<div ng-repeat="error in errors.years" class="alert alert-danger">Years {{error}}</div>
<label for="job_manager">Manager</label>
<input ng-model="job.manager"
class="form-control"
id="job_manager"
name="job[manager]"
type="text" />
<div ng-repeat="error in errors.manager" class="alert alert-danger">Manager {{error}}</div>
<label for="job_contact">Contact</label>
<input ng-model="job.contact"
class="form-control"
id="job_contact"
name="job[contact]"
type="text" />
<div ng-repeat="error in errors.contact" class="alert alert-danger">Contact {{error}}</div>
<label for="job_address">Address</label>
<input ng-model="job.address"
class="form-control"
id="job_address"
name="job[address]"
type="text" />
<div ng-repeat="error in errors.address" class="alert alert-danger">Address {{error}}</div>
<label for="job_description">Description</label>
<textarea ng-model="job.description"
class="form-control"
id="job_description"
name="job[description]"
required>
</textarea>
<div ng-repeat="error in errors.description" class="alert alert-danger">Description {{error}}</div>
<label for="job_skills">Skills</label>
<input ng-model="job.skills"
class="form-control"
id="job_skills"
name="job[skills]"
type="text" />
<div ng-repeat="error in errors.skills" class="alert alert-danger">Skills {{error}}</div>
<label for="job_references">References</label>
<input ng-model="job.references[0]"
class="form-control"
id="job_references_attributes_0_reference"
name="job[references_attributes][0][reference]"
type="text" />
<input ng-model="job.references[1]"
class="form-control"
id="job_references_attributes_1_reference"
name="job[references_attributes][1][reference]"
type="text" />
<input ng-model="job.references[2]"
class="form-control"
id="job_references_attributes_2_reference"
name="job[references_attributes][2][reference]"
type="text" />
<input ng-model="job.references[3]"
class="form-control"
id="job_references_attributes_3_reference"
name="job[references_attributes][3][reference]"
type="text" />
<input ng-model="job.references[4]"
class="form-control"
id="job_references_attributes_4_reference"
name="job[references_attributes][4][reference]"
type="text" />
<label for="job_image">Image</label>
<input ng-model="job.image"
class="width-100"
id="job_image"
name="job[image]"
type="file"
ngf-select
accept="image/*"
ngf-max-size="5MB" />
<div ng-repeat="error in errors.image" class="alert alert-danger">Image {{error}}</div>
<div class="center">
<div class="btn-group">
<input class="btn btn-large btn-primary"
name="commit"
type="submit"
value="Submit" />
</div>
<div class="btn-group">
<a ng-click="back()" href class="btn btn-large btn-default">← Cancel</a>
</div>
</div>
</form>
</div>
</div>
The "hacky" bit I am referring to is where I've got "job.references[0]" going on. Here is how I'm having ng-file-upload work with it to include the nested attributes:
$scope.save = function(job) {
var file = job.image;
Upload.upload({
url: '/api/jobs/' + job.id,
method: 'PUT',
fields: {
'job[title]': job.title,
'job[company]': job.company,
'job[description]': job.description,
'job[years]': job.years,
'job[manager]': job.manager,
'job[contact]': job.contact,
'job[skills]': job.skills,
'job[address]': job.address,
'job[references_attributes][0][reference]': job.references[0],
'job[references_attributes][1][reference]': job.references[1],
'job[references_attributes][2][reference]': job.references[2],
'job[references_attributes][3][reference]': job.references[3],
'job[references_attributes][4][reference]': job.references[4] },
file: file,
fileFormDataName: 'job[image]'
}).progress(function (evt) {
$scope.progress = Math.min(100, parseInt(100.0 * evt.loaded / evt.total));
console.log('Progress: ' + $scope.progress + '% ' + evt.config.file.name);
}).success(function (data, status, headers, config) {
console.log('File ' + config.file.name + 'uploaded. Response: ' + data);
$scope.jobs.push(data);
$state.go('jobs.show', {id: $stateParams.id})
}).error(function (data, status, headers, config) {
console.log('Error status: ' + status);
$scope.errors = data;
});
}
This works well for NEW model entries, but when I EDIT existing entries here is where it goes wrong... This is the JSON data that is being sent to Rails with this setup:
Parameters: {"job"=>{"title"=>"Example Title", "company"=>"Example Company", "description"=>"Example Description", "references_attributes"=>{"0"=>{"reference"=>"Example Reference"}}, "image"=> #plus the image data here}
As you may have noticed, the "id" attribute is no longer being included in the nested attributes. This is causing Rails to duplicate existing nested attributes when I submit any edit. Does anyone have a better idea of how I can include nested attributes with ng-file-upload? Or perhaps even solutions without ng-file-upload?
Thanks in advance!
There has been a recent update to the tool which greatly helps nested attributes. For this using Rails, with nested attributes... In the form, you can use the following format:
<input ng-model="Object.nested_attributes[0]" />
Then in the AngularJS controller:
Upload.upload({
url: 'RailsController#create or update path',
method: 'post or put',
data: {
YourDataModel: {
image: file,
nested_attributes: $scope.Object.nested_attributes
}
}
});