I have two product
controllers in my elixir/phoenix backend. First - API endpoint (pipe_through :api
) and second controller piping through :browser
:
# router.ex
scope "/api", SecretApp.Api, as: :api do
pipe_through :api
resources "products", ProductController, only: [:create, :index]
end
scope "/", SecretApp do
pipe_through :browser # Use the default browser stack
resources "products", ProductController, only: [:new, :create, :index]
end
ProductController
handles requests from form generated by elixir form helpers and accepts some file attachments. Everything is fine with it. Here is create action and params processed by this action:
def create(conn, %{"product" => product_params}) do
changeset = Product.changeset(%Product{}, product_params)
case Repo.insert(changeset) do
{:ok, _product} ->
conn
|> put_flash(:info, "Product created successfully.")
|> redirect(to: product_path(conn, :index))
{:error, changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
params from log (I am using arc for handling image uploads in elixir code)
[debug] Processing by SecretApp.ProductController.create/2
Parameters: %{"_csrf_token" => "Zl81JgdhIQ8GG2c+ei0WCQ9hTjI+AAAA0fwto+HMdQ7S7OCsLQ9Trg==", "_utf8" => "✓",
"product" => %{"description" => "description_name",
"image" => %Plug.Upload{content_type: "image/png",
filename: "wallpaper-466648.png",
path: "/tmp/plug-1460/multipart-754282-298907-1"},
"name" => "product_name", "price" => "100"}}
Pipelines: [:browser]
Api.ProductController
handles requests from redux-from. Here is action, view and params, which are processed by this action:
# action in controller
def create(conn, %{"product" => product_params}) do
changeset = Product.changeset(%Product{}, product_params)
case Repo.insert(changeset) do
{:ok, _product} ->
conn
|> render("index.json", status: :ok)
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render("error.json", changeset: changeset)
end
end
# product_view.ex
def render("index.json", resp=%{status: status}) do
%{status: status}
end
def render("error.json", %{changeset: changeset}) do
errors = Enum.into(changeset.errors, %{})
%{
errors: errors
}
end
[info] POST /api/products/
[debug] Processing by SecretApp.Api.ProductController.create/2
Parameters: %{"product" => %{"description" => "product_description", "image" => "wallpaper-466648.png", "name" => "product_name", "price" => "100"}}
Pipelines: [:api]
[info] Sent 422 in 167ms
Create action fails with 422 status, because image can't be saved with these params. My problem that I can't access image from backend code, I only have it in my JS code as FileList object. I don't understand how to pass image to backend code. Here is how this attachment represented in my JS code (FileList, containing information about uploaded image).
value:FileList
0: File
lastModified: 1381593256801
lastModifiedDate: Sat Oct 12 2013 18:54:16 GMT+0300
name: "wallpaper-466648.png"
size: 1787293
type: "image/png"
webkitRelativePath: ""
I only have WebkitRelativePath (In case with first controller I have path to image: "/tmp/plug-1460/multipart-754282-298907-1") and I don't know what can I do with this JS object and how to access real image represented by this JS object (here is a redux-form reference about file uploads).
Could you help me? How to explain to elixir how to find an image? I just would like to submit file attachments to my backend using JS code (because there a lot of interesting features for async validation etc).
Here is a link to a full app if it could be helpful
Finally I've managed to solve this problem. The solution is in correct serialization of redux-form submitted params.
Here is my redux form, starting point of the request:
// product_form.js
import React, { PropTypes } from 'react';
import {reduxForm} from 'redux-form';
class ProductForm extends React.Component {
static propTypes = {
fields: PropTypes.object.isRequired,
handleSubmit: PropTypes.func.isRequired,
error: PropTypes.string,
resetForm: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired
};
render() {
const {fields: {name, description, price, image}, handleSubmit, resetForm, submitting, error} = this.props;
return (
<div className="product_form">
<div className="inner">
<form onSubmit={handleSubmit} encType="multipart/form-data">
<div className="form-group">
<label className="control-label"> Name </label>
<input type="text" className="form-control" {...name} />
{name.touched && name.error && <div className="col-xs-3 help-block">{name.error}</div>}
</div>
<div className="form-group">
<label className="control-label"> Description </label>
<input type="textarea" className="form-control" {...description} />
{description.touched && description.error && <div className="col-xs-3 help-block">{description.error}</div>}
</div>
<div className="form-group">
<label className="control-label"> Price </label>
<input type="number" step="any" className="form-control" {...price} />
{price.touched && price.error && <div className="col-xs-3 help-block">{price.error}</div>}
</div>
<div className="form-group">
<label className="control-label"> Image </label>
<input type="file" className="form-control" {...image} value={ null } />
{image.touched && image.error && <div className="col-xs-3 help-block">{image.error}</div>}
</div>
<div className="form-group">
<button type="submit" className="btn btn-primary" >Submit</button>
</div>
</form>
</div>
</div>
);
}
}
ProductForm = reduxForm({
form: 'new_product_form',
fields: ['name', 'description', 'price', 'image']
})(ProductForm);
export default ProductForm;
This form passes the following params to the function handleSubmit
after user presses the button "Submit"
# values variable
Object {name: "1", description: "2", price: "3", image: FileList}
# where image value is
value:FileList
0: File
lastModified: 1381593256801
lastModifiedDate: Sat Oct 12 2013 18:54:16 GMT+0300
name: "wallpaper-466648.png"
size: 1787293
type: "image/png"
webkitRelativePath: ""
To pass these params to backend I am using the FormData Web API and the file-upload request using isomorphic-fetch npm module
Here is the code did the trick:
// product_form_container.js (where form submit processed, see _handleSubmit function)
import React from 'react';
import ProductForm from '../components/product_form';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import Actions from '../actions/products';
import * as form_actions from 'redux-form';
import {httpGet, httpPost, httpPostForm} from '../utils';
class ProductFormContainer extends React.Component {
_handleSubmit(values) {
return new Promise((resolve, reject) => {
let form_data = new FormData();
Object.keys(values).forEach((key) => {
if (values[key] instanceof FileList) {
form_data.append(`product[${key}]`, values[key][0], values[key][0].name);
} else {
form_data.append(`product[${key}]`, values[key]);
}
});
httpPostForm(`/api/products/`, form_data)
.then((response) => {
resolve();
})
.catch((error) => {
error.response.json()
.then((json) => {
let responce = {};
Object.keys(json.errors).map((key) => {
Object.assign(responce, {[key] : json.errors[key]});
});
if (json.errors) {
reject({...responce, _error: 'Login failed!'});
} else {
reject({_error: 'Something went wrong!'});
};
});
});
});
}
render() {
const { products } = this.props;
return (
<div>
<h2> New product </h2>
<ProductForm title="Add product" onSubmit={::this._handleSubmit} />
<Link to='/admin/products'> Back </Link>
</div>
);
}
}
export default connect()(ProductFormContainer);
where httpPostForm
is a wrapper around fetch:
export function httpPostForm(url, data) {
return fetch(url, {
method: 'post',
headers: {
'Accept': 'application/json'
},
body: data,
})
.then(checkStatus)
.then(parseJSON);
}
And that's it. There was nothing to fix in my elixir code, Api.ProductController
remains the same (see initial post). But now it receives request with the following params:
[info] POST /api/products/
[debug] Processing by SecretApp.Api.ProductController.create/2
Parameters: %{"product" => %{
"description" => "2",
"image" => %Plug.Upload{
content_type: "image/jpeg",
filename: "monkey_in_jungle-t3.jpg",
path: "/tmp/plug-1461/multipart-853391-603088-1"
},
"name" => "1",
"price" => "3"}}
Pipelines: [:api]
Many thanks for everyone trying to help me. Hope this could help someone struggling with similar serialization issues.