├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmignore
├── .nycrc
├── .travis.yml
├── LICENSE
├── README.md
├── docs
└── screenshots
│ └── general.png
├── example
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
│ └── index.html
└── src
│ ├── App.js
│ ├── index.js
│ └── posts.js
├── makefile
├── mocha.opts
├── package-lock.json
├── package.json
├── src
├── index.js
├── lib
│ └── custom-lodash.js
└── mui
│ ├── field
│ ├── EmbeddedArrayField.js
│ ├── EmbeddedArrayField.spec.js
│ └── index.js
│ ├── form
│ ├── EmbeddedArrayInputFormField.js
│ └── isRequired.js
│ ├── index.js
│ └── input
│ ├── EmbeddedArrayInput.js
│ ├── EmbeddedArrayInput.spec.js
│ └── index.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "stage-0",
5 | "react"
6 | ],
7 | "plugins": [
8 | [
9 | "transform-runtime",
10 | {
11 | "polyfill": false,
12 | "regenerator": true
13 | }
14 | ]
15 | ],
16 | "env": {
17 | "test": {
18 | "plugins": [
19 | ["istanbul", { "exclude": "**/*.spec.js" }]
20 | ]
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 | example/static
4 | docs/_site/
5 | docs/js/
6 | docs/css/
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "env": {
4 | "es6": true,
5 | "mocha": true,
6 | "node": true,
7 | "browser": true,
8 | },
9 | "extends": [
10 | "eslint:recommended",
11 | "plugin:import/errors",
12 | "plugin:import/warnings",
13 | "plugin:react/recommended",
14 | "prettier",
15 | "prettier/react"
16 | ],
17 | "plugins": [
18 | "react",
19 | "prettier"
20 | ],
21 | "rules": {
22 | "prettier/prettier": ["error", {
23 | "printWidth": 120,
24 | "singleQuote": true,
25 | "tabWidth": 4,
26 | "trailingComma": "all"
27 | }],
28 | "react/prop-types": ["warn"],
29 | "import/no-extraneous-dependencies": "off",
30 | "no-console": ["error", { "allow": ["warn", "error"] }],
31 | "no-unused-vars": ["error", { "ignoreRestSiblings": true }]
32 | }
33 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /lib
2 | node_modules
3 | .nvmrc
4 | .nyc_output
5 | .coverage-cache
6 | coverage
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | .babelrc
3 | .eslintrc
4 | Makefile
5 |
--------------------------------------------------------------------------------
/.nycrc:
--------------------------------------------------------------------------------
1 | {
2 | "sourceMap": false,
3 | "instrument": false,
4 | "reporter": [
5 | "cobertura",
6 | "lcov",
7 | "text"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '7.8.0'
4 | env:
5 | global:
6 | - NODE_ENV=test
7 | dist: trusty
8 | cache:
9 | directories:
10 | - node_modules
11 | branches:
12 | only:
13 | - master
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017-present, Gildas Garcia, Marmelab
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.com/package/aor-embedded-array)
2 | [](https://www.npmjs.com/package/aor-embedded-array)
3 | [](https://www.npmjs.com/package/aor-embedded-array)
4 | [](https://travis-ci.org/MhdSyrwan/aor-embedded-array)
5 | # aor-embedded-array
6 |
7 | A custom field/input component for [Admin-on-rest](https://github.com/marmelab/admin-on-rest/) that provides the ability to represent embedded arrays.
8 |
9 | 
10 |
11 | ## Installation
12 |
13 | Install with:
14 |
15 | ```sh
16 | npm install --save aor-embedded-array
17 | ```
18 |
19 | or
20 |
21 | ```sh
22 | yarn add aor-embedded-array
23 | ```
24 |
25 | ## Usage
26 |
27 | ### Basic Usage
28 |
29 | Define the `Create` and `Edit` View like this:
30 |
31 | ```jsx
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | ```
41 |
42 | Define the `Show` and `List` View like this:
43 |
44 | ```jsx
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | ```
54 |
55 | For primitive arrays, define the Views the same way but without the source prop for the unique child:
56 |
57 | ```jsx
58 |
59 |
60 |
61 | ```
62 |
63 | ### Using Custom action buttons
64 |
65 | ```jsx
66 | import FlatButton from 'material-ui/FlatButton';
67 | import ActionDeleteIcon from 'material-ui/svg-icons/action/delete';
68 | const CustomDeleteButton = ({items, index}) => (
69 | }
74 | onClick={() => {
75 | // Take custom action
76 | console.log(items, index);
77 | items.remove(index);
78 | }}
79 | />
80 | )
81 | ```
82 |
83 | ```jsx
84 | var style = {
85 | actionsContainerStyle: {
86 | display: "inline-block",
87 | clear: "both",
88 | float: "right",
89 | padding: "2em 0em 0em 0em"
90 | }
91 | }
92 | ]}
95 | >
96 |
97 |
98 |
99 | ```
100 |
101 | ### Customizing Add and Remove buttons' labels
102 | You can make use of the translation system provided by `admin-on-rest` and set the following translation paths:
103 | 1. `aor.input.embedded_array.add` to set Add Button's label.
104 | 2. `aor.input.embedded_array.remove` to set Remove Button's label.
105 |
106 | Also, you can change the translation path's themselves by providing values for props `labelAdd` and `labelRemove`.
107 |
108 | You can learn more about admin-on-rest's translation system from this link: https://marmelab.com/admin-on-rest/Translation.html
109 |
110 | ### Passing props to customize style
111 |
112 | There are four style props you can pass to customize style, those are `actionsContainerStyle`, `innerContainerStyle`, `labelStyle` & `insertDividers`.
113 |
114 | Default values of those are as follows
115 |
116 | ```js
117 | actionsContainerStyle: {
118 | clear: 'both',
119 | margin: '1em',
120 | display: 'block',
121 | textAlign: 'right',
122 | },
123 | ```
124 |
125 | ```js
126 | innerContainerStyle: {
127 | padding: '0 1em 1em 1em',
128 | width: '90%',
129 | display: 'inline-block',
130 | },
131 | ```
132 |
133 | ```js
134 | labelContainerStyle: {
135 | padding: '1.2em 1em 0 0',
136 | width: '90%',
137 | display: 'inline-block',
138 | },
139 | ```
140 |
141 | ```js
142 | labelStyle: {
143 | top: 0,
144 | position: 'relative',
145 | textTransform: 'capitalize',
146 | },
147 | ```
148 |
149 | You can also pass `insertDividers` value as `true` or `false` to get the divider or not. Default value for `insertDividers` is true.
150 |
--------------------------------------------------------------------------------
/docs/screenshots/general.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MhdSyrwan/aor-embedded-array/c5178ffb9762c9b2eb2b44dfd1d33551b9816a17/docs/screenshots/general.png
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | Example usage of aor-embedded-array
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react-scripts": "1.1.1",
7 | "aor-embedded-array": "../"
8 | },
9 | "scripts": {
10 | "start": "react-scripts start",
11 | "build": "react-scripts build",
12 | "test": "react-scripts test --env=jsdom",
13 | "eject": "react-scripts eject"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React App
8 |
9 |
10 |
13 |
14 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/example/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { simpleRestClient, Admin, Resource } from 'admin-on-rest';
3 |
4 | import { PostList, PostEdit, PostCreate, PostIcon } from './posts';
5 |
6 | export default class App extends React.Component {
7 | render() {
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/example/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import App from './App';
4 |
5 | render(, document.getElementById('root'));
6 |
--------------------------------------------------------------------------------
/example/src/posts.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | List,
4 | Datagrid,
5 | Edit,
6 | Create,
7 | SimpleForm,
8 | DateField,
9 | TextField,
10 | EditButton,
11 | DisabledInput,
12 | TextInput,
13 | LongTextInput,
14 | DateInput,
15 | } from 'admin-on-rest';
16 | import { EmbeddedArrayInput, EmbeddedArrayField } from 'aor-embedded-array';
17 | import BookIcon from 'material-ui/svg-icons/action/book';
18 | export const PostIcon = BookIcon;
19 |
20 | export const PostList = props =>
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
;
37 |
38 | const PostTitle = ({ record }) => {
39 | return (
40 |
41 | Post {record ? `"${record.title}"` : ''}
42 |
43 | );
44 | };
45 |
46 | export const PostEdit = props =>
47 | } {...props}>
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | ;
62 |
63 | export const PostCreate = props =>
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | ;
77 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build help
2 |
3 | help:
4 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
5 |
6 | install: package.json ## Install dependencies
7 | @yarn
8 |
9 | clean: ## Clean up the lib folder for building
10 | @rm -rf lib
11 |
12 | build: clean ## Compile ES6 files to JS
13 | @NODE_ENV=production ./node_modules/.bin/babel \
14 | --out-dir=lib \
15 | --stage=0 \
16 | --ignore=*.spec.js \
17 | ./src
18 |
19 | watch: ## Continuously compile ES6 files to JS
20 | @NODE_ENV=production ./node_modules/.bin/babel \
21 | --out-dir=lib \
22 | --stage=0 \
23 | --ignore=*.spec.js \
24 | --watch \
25 | ./src
26 |
27 | test: ## Launch unit tests
28 | @NODE_ENV=test ./node_modules/.bin/nyc \
29 | ./node_modules/.bin/mocha \
30 | --opts ./mocha.opts \
31 | "./src/**/*.spec.js"
32 |
33 | inspect-test: ## Launch unit tests
34 | @NODE_ENV=test ./node_modules/.bin/nyc \
35 | ./node_modules/.bin/mocha \
36 | --opts ./mocha.opts \
37 | --inspect-brk \
38 | "./src/**/*.spec.js"
39 |
40 |
41 | watch-test: ## Launch unit tests and watch for changes
42 | @NODE_ENV=test ./node_modules/.bin/nyc \
43 | ./node_modules/.bin/mocha \
44 | --opts ./mocha.opts \
45 | --watch \
46 | "./src/**/*.spec.js"
47 |
48 | format: ## Format the source code
49 | @./node_modules/.bin/eslint --fix ./src
50 |
--------------------------------------------------------------------------------
/mocha.opts:
--------------------------------------------------------------------------------
1 | --require babel-polyfill
2 | --compilers js:babel-core/register
3 | --colors
4 | --reporter=spec
5 | --timeout=5000
6 | --recursive
7 | --sort
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aor-embedded-array",
3 | "version": "0.1.0",
4 | "description": "Embeded Array Field/Input for Admin On Rest",
5 | "main": "./lib/index.js",
6 | "repository": "https://github.com/mhdsyrwan/aor-embedded-array.git",
7 | "authors": [
8 | "Muhammad Al-Seyrawan "
9 | ],
10 | "license": "MIT",
11 | "scripts": {
12 | "build": "make build",
13 | "clean": "make clean",
14 | "format": "make format",
15 | "precommit": "lint-staged",
16 | "prepublish": "make build",
17 | "test": "make test",
18 | "watch-test": "make watch-test"
19 | },
20 | "lint-staged": {
21 | "*.js": [
22 | "eslint --fix ./src",
23 | "git add"
24 | ]
25 | },
26 | "devDependencies": {
27 | "admin-on-rest": "~1.3.0",
28 | "babel-cli": "~6.24.1",
29 | "babel-core": "~6.25.0",
30 | "babel-eslint": "7.2.3",
31 | "babel-plugin-istanbul": "~4.1.3",
32 | "babel-preset-es2015": "~6.24.1",
33 | "babel-preset-react": "~6.24.1",
34 | "babel-preset-stage-0": "~6.24.1",
35 | "enzyme": "~2.9.1",
36 | "eslint": "~4.2.0",
37 | "eslint-config-prettier": "~2.3.0",
38 | "eslint-plugin-import": "~2.7.0",
39 | "eslint-plugin-jsx-a11y": "~6.0.2",
40 | "eslint-plugin-mocha": "~4.11.0",
41 | "eslint-plugin-prettier": "~2.1.2",
42 | "eslint-plugin-react": "~7.1.0",
43 | "expect": "~1.20.2",
44 | "husky": "~0.14.3",
45 | "istanbul-cobertura-badger": "~1.3.0",
46 | "lint-staged": "~4.0.1",
47 | "mocha": "~3.4.2",
48 | "nyc": "~11.0.3",
49 | "prettier": "~1.5.2",
50 | "react": "~15.6.1",
51 | "react-addons-test-utils": "~15.6.0",
52 | "react-dom": "~15.6.1"
53 | },
54 | "dependencies": {
55 | "babel-plugin-transform-runtime": "~6.23.0",
56 | "lodash.get": "~4.4.2",
57 | "lodash.map": "^4.6.0",
58 | "lodash.merge": "^4.6.1",
59 | "prop-types": "~15.5.10"
60 | },
61 | "peerDependencies": {
62 | "admin-on-rest": "~1.3"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { EmbeddedArrayField, EmbeddedArrayInput } from './mui';
2 |
3 | export { EmbeddedArrayField, EmbeddedArrayInput };
4 |
--------------------------------------------------------------------------------
/src/lib/custom-lodash.js:
--------------------------------------------------------------------------------
1 | import get from 'lodash.get'
2 | import map from 'lodash.map'
3 | import merge from 'lodash.merge'
4 |
5 | export default { get, map, merge }
6 |
--------------------------------------------------------------------------------
/src/mui/field/EmbeddedArrayField.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { SimpleShowLayout } from 'admin-on-rest';
4 | import _ from '../../lib/custom-lodash';
5 |
6 | /**
7 | * A container component that shows embedded array elements as a list of input sets
8 | *
9 | * You must define the fields and pass them as children or only use one field for primitive arrays.
10 | *
11 | * @example Display all the items of an order
12 | * // order = {
13 | * // id: 123,
14 | * // items: [
15 | * // { qty: 1, price: 10 },
16 | * // { qty: 3, price: 15 },
17 | * // ],
18 | * // }
19 | *
20 | *
21 | *
22 | *
23 | * @example Display all the tags of a product
24 | * // product = {
25 | * // id: 123,
26 | * // tags: [
27 | * // 'relaxation',
28 | * // 'chair',
29 | * // ],
30 | * // }
31 | *
32 | *
33 | *
34 | */
35 | export class EmbeddedArrayField extends Component {
36 | render() {
37 | const { resource, children, source, record } = this.props;
38 | const layoutProps = { resource, basePath: '/', record };
39 | const elements = _.get(record, source) || [];
40 | const elementsWithIndex = _.map(elements, (el, k) => _.merge(el, { _index: k }));
41 |
42 | return (
43 |
44 | {_.map(
45 | elementsWithIndex,
46 | (element, i) =>
47 |
48 | {React.Children.map(children, child =>
49 | React.cloneElement(child, {
50 | source: child.props.source
51 | ? `${source}[${i}].${child.props.source}`
52 | : `${source}[${i}]`,
53 | }),
54 | )}
55 | ,
56 | this,
57 | )}
58 |
59 | );
60 | }
61 | }
62 |
63 | EmbeddedArrayField.propTypes = {
64 | addLabel: PropTypes.bool,
65 | basePath: PropTypes.string,
66 | children: PropTypes.node.isRequired,
67 | data: PropTypes.object,
68 | label: PropTypes.string,
69 | record: PropTypes.object,
70 | resource: PropTypes.string,
71 | source: PropTypes.string.isRequired,
72 | };
73 |
74 | EmbeddedArrayField.defaultProps = {
75 | addLabel: true,
76 | };
77 |
78 | export default EmbeddedArrayField;
79 |
--------------------------------------------------------------------------------
/src/mui/field/EmbeddedArrayField.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import assert from 'assert';
3 | import { shallow } from 'enzyme';
4 | import EmbeddedArrayField from './EmbeddedArrayField';
5 | import { TextField, UrlField } from 'admin-on-rest';
6 |
7 | const record = {
8 | links: [
9 | {
10 | url: 'https://www.google.com/',
11 | context: 'Search engine',
12 | },
13 | {
14 | url: 'https://www.bing.com/',
15 | context: 'Search engine',
16 | },
17 | ],
18 | };
19 |
20 | describe('', () => {
21 | it('should display 2 nested SimpleShowLayouts', () => {
22 | const wrapper = shallow(
23 |
24 |
25 |
26 | ,
27 | );
28 | assert.equal(2, wrapper.find('SimpleShowLayout').length);
29 | assert.deepEqual(
30 | [['links[0].url', 'links[0].context'], ['links[1].url', 'links[1].context']],
31 | wrapper.find('SimpleShowLayout').map(s => s.prop('children').map(c => c.props.source)),
32 | );
33 | });
34 | });
35 |
36 | const primitiveRecord = {
37 | links: ['https://www.google.com/', 'https://www.bing.com/'],
38 | };
39 |
40 | describe(' with primitive record', () => {
41 | it('should display 2 nested SimpleShowLayouts', () => {
42 | const wrapper = shallow(
43 |
44 |
45 | ,
46 | );
47 | assert.equal(2, wrapper.find('SimpleShowLayout').length);
48 | assert.deepEqual(
49 | [['links[0]'], ['links[1]']],
50 | wrapper.find('SimpleShowLayout').map(s => s.prop('children').map(c => c.props.source)),
51 | );
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/src/mui/field/index.js:
--------------------------------------------------------------------------------
1 | import EmbeddedArrayField from './EmbeddedArrayField';
2 | export { EmbeddedArrayField };
3 |
--------------------------------------------------------------------------------
/src/mui/form/EmbeddedArrayInputFormField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Field } from 'redux-form';
3 |
4 | import { Labeled } from 'admin-on-rest';
5 | import isRequired from './isRequired';
6 |
7 | /**
8 | * A helper Input component for EmbeddedArrayInput
9 | *
10 | * It's an alternative to FormField that provides the ability to prefix the source/name
11 | * with a string you provide
12 | *
13 | * @example
14 | *
15 | */
16 | const EmbeddedArrayInputFormField = ({ input, prefix, ...rest }) => {
17 | const name = input.props.source ? `${prefix}.${input.props.source}` : prefix;
18 | const source = name;
19 |
20 | if (input.props.addField) {
21 | if (input.props.addLabel) {
22 | return (
23 |
31 | {input}
32 |
33 | );
34 | }
35 | return (
36 |
44 | );
45 | }
46 | if (input.props.addLabel) {
47 | return (
48 |
49 | {input}
50 |
51 | );
52 | }
53 | return typeof input.type === 'string'
54 | ? input
55 | : React.cloneElement(input, {
56 | ...rest,
57 | source: name,
58 | });
59 | };
60 |
61 | export default EmbeddedArrayInputFormField;
62 |
--------------------------------------------------------------------------------
/src/mui/form/isRequired.js:
--------------------------------------------------------------------------------
1 | import { required } from 'admin-on-rest';
2 |
3 | const isRequired = validate => {
4 | if (validate === required) return true;
5 | if (Array.isArray(validate)) {
6 | return validate.includes(required);
7 | }
8 | return false;
9 | };
10 |
11 | export default isRequired;
12 |
--------------------------------------------------------------------------------
/src/mui/index.js:
--------------------------------------------------------------------------------
1 | import { EmbeddedArrayField } from './field';
2 | import { EmbeddedArrayInput } from './input';
3 |
4 | export { EmbeddedArrayField, EmbeddedArrayInput };
5 |
--------------------------------------------------------------------------------
/src/mui/input/EmbeddedArrayInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { FieldArray } from 'redux-form';
4 | import inflection from 'inflection';
5 |
6 | import FlatButton from 'material-ui/FlatButton';
7 | import TextFieldLabel from 'material-ui/TextField/TextFieldLabel';
8 | import ContentAdd from 'material-ui/svg-icons/content/add';
9 | import ActionDeleteIcon from 'material-ui/svg-icons/action/delete';
10 | import Divider from 'material-ui/Divider';
11 |
12 | import { translate } from 'admin-on-rest';
13 |
14 | import EmbeddedArrayInputFormField from '../form/EmbeddedArrayInputFormField';
15 |
16 | /**
17 | * An Input component for generating/editing an embedded array
18 | *
19 | *
20 | * Use it with any set of input components as children, like ``,
21 | * ``, ``, etc.
22 | *
23 | * You must define the targeted field for each child or only use one child for primitive arrays.
24 | *
25 | * @example
26 | * export const CommentEdit = (props) => (
27 | *
28 | *
29 | *
30 | *
31 | *
32 | *
33 | *
34 | *
35 | *
36 | *
37 | *
38 | * );
39 | * @example
40 | * export const CommentEdit = (props) => (
41 | *
42 | *
43 | *
44 | *
45 | *
46 | *
47 | *
48 | * );
49 | */
50 | export class EmbeddedArrayInput extends Component {
51 | static propTypes = {
52 | addLabel: PropTypes.bool.isRequired,
53 | addField: PropTypes.bool.isRequired,
54 | allowEmpty: PropTypes.bool.isRequired,
55 | allowAdd: PropTypes.bool.isRequired,
56 | allowRemove: PropTypes.bool.isRequired,
57 | arrayElStyle: PropTypes.object,
58 | basePath: PropTypes.string,
59 | children: PropTypes.node.isRequired,
60 | disabled: PropTypes.bool,
61 | elStyle: PropTypes.object,
62 | input: PropTypes.object,
63 | label: PropTypes.string,
64 | labelAdd: PropTypes.string.isRequired,
65 | labelRemove: PropTypes.string.isRequired,
66 | meta: PropTypes.object,
67 | onChange: PropTypes.func,
68 | resource: PropTypes.string,
69 | readOnly: PropTypes.bool,
70 | record: PropTypes.object,
71 | source: PropTypes.string,
72 | customButtons: PropTypes.node,
73 | actionsContainerStyle: PropTypes.object,
74 | innerContainerStyle: PropTypes.object,
75 | labelContainerStyle: PropTypes.object,
76 | labelStyle: PropTypes.object,
77 | insertDividers: PropTypes.bool,
78 | };
79 |
80 | static defaultProps = {
81 | addLabel: false,
82 | addField: false,
83 | allowEmpty: true,
84 | allowAdd: true,
85 | allowRemove: true,
86 | labelAdd: 'aor.input.embedded_array.add',
87 | labelRemove: 'aor.input.embedded_array.remove',
88 | insertDividers: true,
89 | actionsContainerStyle: {
90 | clear: 'both',
91 | margin: '1em',
92 | display: 'block',
93 | textAlign: 'right',
94 | },
95 | innerContainerStyle: {
96 | padding: '0 1em 1em 1em',
97 | width: '90%',
98 | display: 'inline-block',
99 | },
100 | labelContainerStyle: {
101 | padding: '1.2em 1em 0 0',
102 | width: '90%',
103 | display: 'inline-block',
104 | },
105 | labelStyle: {
106 | top: 0,
107 | position: 'relative',
108 | textTransform: 'capitalize',
109 | },
110 | };
111 |
112 | static contextTypes = {
113 | muiTheme: PropTypes.object.isRequired,
114 | };
115 |
116 | renderListItem = ({
117 | allowRemove,
118 | items,
119 | inputs,
120 | member,
121 | index,
122 | translate,
123 | labelRemove,
124 | readOnly,
125 | disabled,
126 | customButtons,
127 | actionsContainerStyle,
128 | innerContainerStyle,
129 | }) => {
130 | const removeItem = () => items.remove(index);
131 | const passedProps = {
132 | resource: this.props.resource,
133 | basePath: this.props.basePath,
134 | record: this.props.record,
135 | };
136 |
137 | return (
138 |
139 |
140 | {React.Children.map(
141 | inputs,
142 | input =>
143 | input &&
144 |
149 |
150 |
,
151 | )}
152 |
153 | {(customButtons || (allowRemove && !readOnly && !disabled)) &&
154 |
155 | {allowRemove &&
156 | !readOnly &&
157 | !disabled &&
158 | }
162 | onClick={removeItem}
163 | />}
164 | {customButtons && customButtons.map(button => React.cloneElement(button, { items, index }))}
165 |
}
166 |
167 | );
168 | };
169 |
170 | renderList = ({ fields: items }) => {
171 | const {
172 | children,
173 | style,
174 | translate,
175 | labelRemove,
176 | labelAdd,
177 | allowAdd,
178 | allowRemove,
179 | readOnly,
180 | disabled,
181 | customButtons,
182 | actionsContainerStyle,
183 | innerContainerStyle,
184 | insertDividers,
185 | } = this.props;
186 | const createItem = () => items.push();
187 |
188 | return (
189 |
190 |
191 | {items.map((member, index) =>
192 |
193 | {this.renderListItem({
194 | items,
195 | inputs: children,
196 | member,
197 | index,
198 | translate,
199 | labelRemove,
200 | allowRemove,
201 | readOnly,
202 | disabled,
203 | customButtons,
204 | actionsContainerStyle,
205 | innerContainerStyle,
206 | })}
207 | {insertDividers && index < items.length - 1 &&
}
208 |
,
209 | )}
210 |
211 |
212 | {allowAdd &&
213 | !readOnly &&
214 | !disabled &&
215 |
}
218 | label={translate(labelAdd, { _: 'Add' })}
219 | onClick={createItem}
220 | />}
221 |
222 | );
223 | };
224 |
225 | render() {
226 | const { source, label, addLabel, translate, resource, labelStyle, labelContainerStyle } = this.props;
227 | const labelStyleMuiTheme = Object.assign(labelStyle, {
228 | color: this.context.muiTheme ? this.context.muiTheme.textField.focusColor : '',
229 | });
230 |
231 | const minimizedLabel =
232 | typeof label !== 'undefined'
233 | ? translate(label, { _: label })
234 | : translate(
235 | `resources.${resource}.fields.${source.replace(/\./g, '.fields.').replace(/\[\d+\]/g, '')}.name`,
236 | {
237 | _: inflection.humanize(source.split('.').pop()),
238 | },
239 | );
240 |
241 | const labelElement =
242 | !addLabel &&
243 |
244 |
245 | {minimizedLabel}
246 |
247 |
;
248 |
249 | return (
250 |
251 | {labelElement}
252 |
253 |
254 | );
255 | }
256 | }
257 |
258 | export default translate(EmbeddedArrayInput);
259 |
--------------------------------------------------------------------------------
/src/mui/input/EmbeddedArrayInput.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import assert from 'assert';
3 | import { shallow } from 'enzyme';
4 |
5 | import { TextInput } from 'admin-on-rest';
6 |
7 | import { EmbeddedArrayInput } from './EmbeddedArrayInput';
8 |
9 | import FlatButton from 'material-ui/FlatButton';
10 |
11 | describe('', () => {
12 | const defaultProps = {
13 | source: 'sub_items',
14 | children: [, ],
15 | resource: 'the_items',
16 | translate: x => x,
17 | };
18 |
19 | it('should render FieldArray Element', () => {
20 | const wrapper = shallow();
21 | const fieldArrayElement = wrapper.find('FieldArray');
22 | assert.equal(fieldArrayElement.prop('name'), 'sub_items');
23 | });
24 |
25 | it('should render 4 EmbeddedArrayInputFormField elements', () => {
26 | // instantiating an EmbeddedArrayInput to test its renderList function
27 | const embeddedArrayInput = new EmbeddedArrayInput(defaultProps);
28 |
29 | // mocking redux-form FieldArray items array
30 | const items = ['sub_items[0]', 'sub_items[1]'];
31 |
32 | // shallow rendering the renderList helper to test its contents
33 | const renderList = shallow(embeddedArrayInput.renderList({ fields: items }));
34 |
35 | // It should render the container
36 | assert.equal(renderList.find('.EmbeddedArrayInputContainer').length, 1);
37 |
38 | // It should render a container for each item
39 | assert.equal(renderList.find('.EmbeddedArrayInputItemContainer').length, 2);
40 |
41 | // Totally there should be 4 EmbeddedArrayInputFormField
42 | assert.equal(renderList.find('EmbeddedArrayInputFormField').length, 4);
43 |
44 | // It should render 2 items: sub_items[0] and sub_items[1]
45 | // each with 2 fields: price and qty
46 | assert.deepEqual(
47 | renderList.find('EmbeddedArrayInputFormField').map(el => {
48 | const input = el.prop('input');
49 | return [input.type.name, el.prop('prefix'), input.props.source];
50 | }),
51 | [
52 | ['TextInput', 'sub_items[0]', 'price'],
53 | ['TextInput', 'sub_items[0]', 'qty'],
54 | ['TextInput', 'sub_items[1]', 'price'],
55 | ['TextInput', 'sub_items[1]', 'qty'],
56 | ],
57 | );
58 | });
59 | });
60 |
61 | describe(' with primitive child', () => {
62 | const defaultProps = {
63 | source: 'sub_items',
64 | children: [],
65 | resource: 'the_items',
66 | translate: x => x,
67 | };
68 |
69 | it('should render FieldArray Element', () => {
70 | const wrapper = shallow();
71 | const fieldArrayElement = wrapper.find('FieldArray');
72 | assert.equal(fieldArrayElement.prop('name'), 'sub_items');
73 | });
74 |
75 | it('should render 4 EmbeddedArrayInputFormField elements', () => {
76 | // instantiating an EmbeddedArrayInput to test its renderList function
77 | const embeddedArrayInput = new EmbeddedArrayInput(defaultProps);
78 |
79 | // mocking redux-form FieldArray items array
80 | const items = ['sub_items[0]', 'sub_items[1]'];
81 |
82 | // shallow rendering the renderList helper to test its contents
83 | const renderList = shallow(embeddedArrayInput.renderList({ fields: items }));
84 |
85 | // It should render the container
86 | assert.equal(renderList.find('.EmbeddedArrayInputContainer').length, 1);
87 |
88 | // It should render a container for each item
89 | assert.equal(renderList.find('.EmbeddedArrayInputItemContainer').length, 2);
90 |
91 | // Totally there should be 2 EmbeddedArrayInputFormField
92 | assert.equal(renderList.find('EmbeddedArrayInputFormField').length, 2);
93 |
94 | // It should render 2 items: sub_items[0] and sub_items[1]
95 | assert.deepEqual(
96 | renderList.find('EmbeddedArrayInputFormField').map(el => {
97 | const input = el.prop('input');
98 | return [input.type.name, el.prop('prefix')];
99 | }),
100 | [['TextInput', 'sub_items[0]'], ['TextInput', 'sub_items[1]']],
101 | );
102 | });
103 | });
104 |
105 | describe(' with custom action buttons', () => {
106 | const CustomDeleteButton = ({ items, index }) =>
107 | items.remove(index)} />;
108 |
109 | const defaultProps = {
110 | source: 'sub_items',
111 | children: [],
112 | resource: 'the_items',
113 | translate: x => x,
114 | customButtons: [],
115 | actionsContainerStyle: { padding: '1em' },
116 | allowRemove: false,
117 | };
118 |
119 | it('should render FieldArray Element', () => {
120 | const wrapper = shallow();
121 | const fieldArrayElement = wrapper.find('FieldArray');
122 | assert.equal(fieldArrayElement.prop('name'), 'sub_items');
123 | });
124 |
125 | it('should render 2 CustomDeleteButton elements', () => {
126 | // instantiating an EmbeddedArrayInput to test its renderList function
127 | const embeddedArrayInput = new EmbeddedArrayInput(defaultProps);
128 |
129 | // mocking redux-form FieldArray items array
130 | const items = ['sub_items[0]', 'sub_items[1]'];
131 |
132 | // shallow rendering the renderList helper to test its contents
133 | const renderList = shallow(embeddedArrayInput.renderList({ fields: items }));
134 |
135 | // Totally there should be 2 CustomDeleteButton
136 | assert.equal(renderList.find('CustomDeleteButton').length, 2);
137 | });
138 | });
139 |
--------------------------------------------------------------------------------
/src/mui/input/index.js:
--------------------------------------------------------------------------------
1 | import EmbeddedArrayInput from './EmbeddedArrayInput';
2 | export { EmbeddedArrayInput };
3 |
--------------------------------------------------------------------------------