Skip to content

Commit c8b85f2

Browse files
authored
Merge pull request #9 from tb/parts
Split post body into parts to test advanced nested form
2 parents 951914c + c8dc71a commit c8b85f2

File tree

15 files changed

+481
-112
lines changed

15 files changed

+481
-112
lines changed

app/models/post.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ class Post < ApplicationRecord
33
has_many :comments, dependent: :destroy
44

55
validates :title, presence: true, uniqueness: true
6-
validates :body, presence: true
76

87
scope :title_contains, -> (value) { where('title ILIKE ?', "%#{value.join}%") }
98
end

app/resources/post_resource.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
class PostResource < JSONAPI::Resource
22
extend CustomFilter
33

4-
attributes :title, :body, :created_at
4+
attributes :title, :created_at, :parts
55

66
has_many :comments
77
has_one :category

client/src/api/client.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import {
44
castArray,
55
get,
66
groupBy,
7-
values,
87
keys,
9-
zipObject,
108
pick,
9+
set,
10+
values,
11+
zipObject,
1112
} from 'lodash';
1213

1314
import { denormalize, normalize } from './normalize';
@@ -75,15 +76,16 @@ const normalizeResponse = (response) => {
7576
const normalizeErrors = (response) => {
7677
throw get(response, 'response.data.errors')
7778
.reduce((errors, error) => {
78-
const attribute = /attributes\/(.*)$/.exec(get(error, 'source.pointer'))[1];
79-
errors[attribute] = error.title;
79+
const attribute = /\/data\/[a-z]*\/(.*)$/.exec(get(error, 'source.pointer'))[1];
80+
set(errors, attribute.split('/'), error.title);
8081
return errors;
8182
}, {});
8283
};
8384

8485
export default (requestType, payload, meta) => {
8586
const {
8687
url = `${meta.key}`,
88+
include,
8789
} = meta;
8890

8991
const params = payload;
@@ -104,13 +106,13 @@ export default (requestType, payload, meta) => {
104106
}).then(normalizeResponse).then(res => ({ ...res, params }));
105107
case CREATE:
106108
return client({
107-
url: withParams(url),
109+
url: withParams(url, { include }),
108110
method: 'POST',
109111
data: denormalize(meta.key, payload),
110112
}).then(normalizeResponse).catch(normalizeErrors);
111113
case UPDATE: {
112114
return client({
113-
url: withParams(`${url}/${payload.id}`),
115+
url: withParams(`${url}/${payload.id}`, { include }),
114116
method: 'PUT',
115117
data: denormalize(meta.key, payload),
116118
}).then(normalizeResponse).catch(normalizeErrors);

client/src/api/normalize.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const serializers = {
2121
'title',
2222
'body',
2323
'category',
24+
'parts',
2425
],
2526
category: {
2627
ref: 'id',
Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,65 @@
11
import React, { Component, PropTypes } from 'react';
22
import { isEmpty } from 'lodash';
3-
import { Field, reduxForm } from 'redux-form';
4-
import { Button, Form } from 'reactstrap';
3+
import { connect } from 'react-redux';
4+
import { Field, FieldArray, formValueSelector, reduxForm } from 'redux-form';
5+
import { Button, Form, FormFeedback, Col, Row } from 'reactstrap';
56

67
import { InputField, TextArea, SelectField, required } from '../../forms';
78

8-
class PostForm extends Component {
9+
export const PostFormPart = ({values, part}) => {
10+
switch(values.type) {
11+
case 'image':
12+
return (
13+
<div>
14+
<img src={values.imageUrl} />
15+
<div>{values.description}</div>
16+
<Field
17+
name={`${part}.imageUrl`}
18+
placeholder={'Image url...'}
19+
component={InputField}
20+
/> <Field
21+
name={`${part}.description`}
22+
placeholder={'Image description...'}
23+
component={InputField}
24+
/>
25+
</div>
26+
);
27+
case 'text':
28+
return (
29+
<Field
30+
name={`${part}.text`}
31+
placeholder={'Text...'}
32+
rows={5}
33+
component={TextArea}
34+
/>
35+
);
36+
};
37+
};
38+
39+
export const PostFormParts = ({ parts, fields, meta: { error } }) => (
40+
<div className="my-3">
41+
{fields.map((part, index) =>
42+
<div>
43+
<hr />
44+
<Row key={index}>
45+
<Col xs="10">
46+
<PostFormPart values={parts[index]} part={part} />
47+
</Col>
48+
<Col xs="2">
49+
<Button color="danger" onClick={() => fields.remove(index)}>X</Button>
50+
</Col>
51+
</Row>
52+
</div>
53+
)}
54+
{error && <FormFeedback>{error}</FormFeedback>}
55+
<Button onClick={() => fields.push({ type: 'text' })} className="mr-2">Add Text</Button>
56+
<Button onClick={() => fields.push({ type: 'image' })}>Add Image</Button>
57+
</div>
58+
);
59+
60+
export class PostForm extends Component {
961
render() {
10-
const { categories, handleSubmit, pristine, reset, submitting } = this.props;
62+
const { parts, categories, handleSubmit, pristine, reset, submitting } = this.props;
1163

1264
const categoriesOptions = [{
1365
id: '',
@@ -18,7 +70,7 @@ class PostForm extends Component {
1870
})));
1971

2072
return (
21-
<Form onSubmit={handleSubmit}>
73+
<Form onSubmit={handleSubmit} className="py-3">
2274
<Field
2375
name="title"
2476
label="Title"
@@ -30,13 +82,12 @@ class PostForm extends Component {
3082
component={SelectField}
3183
options={categoriesOptions}
3284
/>
33-
<Field
34-
name="body"
35-
label="Body"
36-
component={TextArea}
37-
rows="10"
85+
<FieldArray
86+
name="parts"
87+
parts={parts}
88+
component={PostFormParts}
3889
/>
39-
<Button disabled={pristine || submitting} color="primary">Submit</Button>
90+
<Button disabled={pristine || submitting} color="primary" className="mr-2">Submit</Button>
4091
<Button disabled={pristine || submitting} onClick={reset}>Undo Changes</Button>
4192
</Form>
4293
);
@@ -47,13 +98,20 @@ const validate = (values) => {
4798
const errors = required(values,
4899
'title',
49100
'category.id',
50-
'body',
51101
);
52102
return errors;
53103
};
54104

105+
const selector = formValueSelector('post');
106+
107+
export const mapStateToProps = (state) => ({
108+
parts: selector(state, 'parts'),
109+
});
110+
55111
export default reduxForm({
56112
enableReinitialize: true,
57113
form: 'post',
58114
validate,
59-
})(PostForm);
115+
})(
116+
connect(mapStateToProps)(PostForm)
117+
);

client/src/forms/fields/InputField.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ class InputField extends Component {
77
};
88

99
render() {
10-
const { input, type, label, meta: { touched, error } } = this.props;
10+
const { input, type, label, placeholder, meta: { touched, error } } = this.props;
1111
const showError = touched && error;
1212

1313
return (
1414
<FormGroup color={showError ? 'danger' : ''}>
1515
{label && <Label>{label}</Label>}
16-
<Input {...input} type={type} />
16+
<Input {...input} type={type} placeholder={placeholder} />
1717
{showError && <FormFeedback>{error}</FormFeedback>}
1818
</FormGroup>
1919
);

client/src/forms/fields/TextArea.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ class TextArea extends Component {
77
};
88

99
render() {
10-
const { input, type, label, rows, meta: { touched, error } } = this.props;
10+
const { input, type, label, rows, placeholder, meta: { touched, error } } = this.props;
1111
const showError = touched && error;
1212

1313
return (
1414
<FormGroup color={showError ? 'danger' : ''}>
1515
{label && <Label>{label}</Label>}
16-
<Input {...input} type={type} rows={rows} />
16+
<Input {...input} type={type} placeholder={placeholder} rows={rows} />
1717
{showError && <FormFeedback>{error}</FormFeedback>}
1818
</FormGroup>
1919
);

client/src/hocs/withResource.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ import {
1616
const withResource = resourceKey => (WrappedComponent) => {
1717
const enhance = compose(
1818
withHandlers({
19-
onSubmit: props => (values) => {
19+
onSubmit: props => (values, meta = {}) => {
2020
const { params, createResource, updateResource, redirectToIndex } = props;
2121
const payload = { id: params.id, ...values };
22-
return (params.id ? updateResource : createResource)(payload)
23-
.then(redirectToIndex)
22+
return (params.id ? updateResource : createResource)(payload, meta)
23+
// .then(redirectToIndex)
2424
.catch((errors) => { throw new SubmissionError(errors); });
2525
},
2626
onDelete: props => (e) => {
@@ -37,10 +37,10 @@ const withResource = resourceKey => (WrappedComponent) => {
3737
});
3838

3939
const mapDispatchToProps = dispatch => ({
40-
fetchResource: payload => dispatch(fetchOne(resourceKey, payload)),
41-
createResource: payload => dispatch(createResource(resourceKey, payload)),
42-
updateResource: payload => dispatch(updateResource(resourceKey, payload)),
43-
deleteResource: payload => dispatch(deleteResource(resourceKey, payload)),
40+
fetchResource: (payload, meta) => dispatch(fetchOne(resourceKey, payload, meta)),
41+
createResource: (payload, meta) => dispatch(createResource(resourceKey, payload, meta)),
42+
updateResource: (payload, meta) => dispatch(updateResource(resourceKey, payload, meta)),
43+
deleteResource: (payload, meta) => dispatch(deleteResource(resourceKey, payload, meta)),
4444
});
4545

4646
return connect(mapStateToProps, mapDispatchToProps)(enhance(WrappedComponent));

client/src/hocs/withResourceList.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
getList,
1414
} from '../store/api';
1515

16-
const withResourceList = resourceKey => (WrappedComponent) => {
16+
const withResourceList = (resourceKey) => (WrappedComponent) => {
1717
const enhance = compose(
1818
withHandlers({
1919
onFilter: props => (filter) => {
@@ -37,9 +37,9 @@ const withResourceList = resourceKey => (WrappedComponent) => {
3737
const { page = {} } = params;
3838
fetchResourceList({ ...params, page: { ...page, number } });
3939
},
40-
onSubmit: props => (values) => {
40+
onSubmit: props => (values, meta = {}) => {
4141
const { createResource, updateResource } = props;
42-
return (values.id ? updateResource : createResource)(values, { list: 'list' })
42+
return (values.id ? updateResource : createResource)(values, { list: 'list', ...meta })
4343
.catch((errors) => { throw new SubmissionError(errors); });
4444
},
4545
onDelete: props => resource => (e) => {
@@ -55,10 +55,10 @@ const withResourceList = resourceKey => (WrappedComponent) => {
5555
});
5656

5757
const mapDispatchToProps = dispatch => ({
58-
fetchResourceList: (params = {}) => dispatch(fetchList(resourceKey, params)),
59-
createResource: payload => dispatch(createResource(resourceKey, payload, { list: 'list' })),
60-
updateResource: payload => dispatch(updateResource(resourceKey, payload)),
61-
deleteResource: payload => dispatch(deleteResource(resourceKey, payload)),
58+
fetchResourceList: (payload, meta) => dispatch(fetchList(resourceKey, payload, meta)),
59+
createResource: (payload, meta) => dispatch(createResource(resourceKey, payload, meta)),
60+
updateResource: (payload, meta) => dispatch(updateResource(resourceKey, payload, meta)),
61+
deleteResource: (payload, meta) => dispatch(deleteResource(resourceKey, payload, meta)),
6262
});
6363

6464
return connect(mapStateToProps, mapDispatchToProps)(enhance(WrappedComponent));

config/application.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,10 @@ class Application < Rails::Application
3333
resource '*', :headers => :any, :methods => [:get, :post, :options]
3434
end
3535
end
36+
37+
# Use SQL instead of Active Record's schema dumper when creating the database.
38+
# This is necessary if your schema can't be completely dumped by the schema dumper,
39+
# like if you have constraints or database-specific column types
40+
config.active_record.schema_format = :sql
3641
end
3742
end

0 commit comments

Comments
 (0)