diff --git a/API.md b/API.md index 9d0450b9c..5fb35d933 100644 --- a/API.md +++ b/API.md @@ -314,6 +314,11 @@ import DateField from 'uniforms-unstyled/DateField'; // Choose your theme packag // bootstrap3 // bootstrap4 wrapClassName="a b c" + + // Display time picker in ampm (12hr) format or 24hr format. + // Available in: + // material + timeFormat="ampm" /> ``` @@ -653,6 +658,7 @@ import NumField from 'uniforms-unstyled/NumField'; // Choose your theme package. // antd // bootstrap3 // bootstrap4 + // material // semantic showInlineError={true} @@ -860,6 +866,7 @@ import TextField from 'uniforms-unstyled/TextField'; // Choose your theme packag // antd // bootstrap3 // bootstrap4 + // material // semantic showInlineError={true} diff --git a/README.md b/README.md index 52e0b636a..dbd4ce3b8 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ # uniforms -In short: uniforms is a set of npm packages, which contains helpers and [React](https://facebook.github.io/react/) components - both unstyled and themed with [AntD](https://ant.design/), [Bootstrap3](http://getbootstrap.com/), [Bootstrap4](http://v4-alpha.getbootstrap.com/) and [Semantic UI](http://semantic-ui.com/) - to easily manage, validate and even generate fully featured forms from your schemas. +In short: uniforms is a set of npm packages, which contains helpers and [React](https://facebook.github.io/react/) components - both unstyled and themed with [AntD](https://ant.design/), [Bootstrap3](http://getbootstrap.com/), [Bootstrap4](http://v4-alpha.getbootstrap.com/), [Material UI](material-ui.com) and [Semantic UI](http://semantic-ui.com/) - to easily manage, validate and even generate fully featured forms from your schemas. Demo: [uniforms.tools](https://uniforms.tools/). @@ -108,6 +108,7 @@ $ meteor npm install graphql $ meteor npm install --save react react-dom uniforms uniforms-antd $ meteor npm install --save react react-dom uniforms uniforms-bootstrap3 $ meteor npm install --save react react-dom uniforms uniforms-bootstrap4 +$ meteor npm install --save react react-dom uniforms uniforms-material $ meteor npm install --save react react-dom uniforms uniforms-semantic $ meteor npm install --save react react-dom uniforms uniforms-unstyled ``` @@ -119,6 +120,7 @@ $ meteor npm install --save react react-dom uniforms uniforms-unstyled $ npm install --save react react-dom uniforms uniforms-antd $ npm install --save react react-dom uniforms uniforms-bootstrap3 $ npm install --save react react-dom uniforms uniforms-bootstrap4 +$ npm install --save react react-dom uniforms uniforms-material $ npm install --save react react-dom uniforms uniforms-semantic $ npm install --save react react-dom uniforms uniforms-unstyled ``` diff --git a/demo/components/Application.js b/demo/components/Application.js index 9051b8719..82fd361ff 100644 --- a/demo/components/Application.js +++ b/demo/components/Application.js @@ -1,5 +1,10 @@ -import React from 'react'; -import {Component} from 'react'; +import React from 'react'; +import getMuiTheme from 'material-ui/styles/getMuiTheme'; +import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme'; +import {Component} from 'react'; +import {PropTypes} from 'react'; + +import {Meteor} from 'meteor/meteor'; import presets from '../lib/presets'; import schema from '../lib/schema'; @@ -18,6 +23,10 @@ class Application extends Component { this.onChange = this.onChange.bind(this); } + getChildContext () { + return {muiTheme: getMuiTheme(lightBaseTheme, {userAgent: Meteor.isServer ? false : undefined})}; + } + onChange (key, value) { if (key === 'preset') { this.setState({props: {...this.state.props, schema: presets[value]}}); @@ -62,4 +71,8 @@ class Application extends Component { } } +Application.childContextTypes = { + muiTheme: PropTypes.object.isRequired +}; + export default Application; diff --git a/demo/components/ApplicationPropsField.js b/demo/components/ApplicationPropsField.js index 34b2009ce..159461b63 100644 --- a/demo/components/ApplicationPropsField.js +++ b/demo/components/ApplicationPropsField.js @@ -9,9 +9,10 @@ import ApplicationField from './ApplicationField'; const ApplicationProps = ({onChange, schema, theme, value}) => { const link = styles[theme]; + const isAntd = theme === 'antd'; const isBootstrap = theme === 'bootstrap3' || theme === 'bootstrap4'; + const isMaterial = theme === 'material'; const isSemantic = theme === 'semantic'; - const isAntd = theme === 'antd'; const AutoForm = themes[theme].AutoForm; const BoolField = themes[theme].BoolField; @@ -25,7 +26,7 @@ const ApplicationProps = ({onChange, schema, theme, value}) => { @@ -36,8 +37,8 @@ const ApplicationProps = ({onChange, schema, theme, value}) => { - - + + diff --git a/demo/lib/themes.js b/demo/lib/themes.js index 29052335c..17bd03d76 100644 --- a/demo/lib/themes.js +++ b/demo/lib/themes.js @@ -1,6 +1,7 @@ import * as antd from 'uniforms-antd'; import * as bootstrap3 from 'uniforms-bootstrap3'; import * as bootstrap4 from 'uniforms-bootstrap4'; +import * as material from 'uniforms-material'; import * as semantic from 'uniforms-semantic'; import * as unstyled from 'uniforms-unstyled'; @@ -8,6 +9,7 @@ const themes = { antd, bootstrap3, bootstrap4, + material, semantic, unstyled }; diff --git a/demo/main.js b/demo/main.js index 187363cf0..ee0fc1113 100644 --- a/demo/main.js +++ b/demo/main.js @@ -1,6 +1,6 @@ import 'babel-polyfill'; - -import {mount} from 'react-mounter'; +import tapEvent from 'react-tap-event-plugin'; +import {mount} from 'react-mounter'; import {Meteor} from 'meteor/meteor'; import {DocHead} from 'meteor/kadira:dochead'; @@ -11,6 +11,8 @@ import Application from '/components/Application'; if (Meteor.isServer) { FlowRouter.setPageCacheTimeout(100000); FlowRouter.setDeferScriptLoading(true); +} else { + tapEvent(); } FlowRouter.route('/', { diff --git a/demo/package.json b/demo/package.json index f5532ef68..7bded7b31 100644 --- a/demo/package.json +++ b/demo/package.json @@ -17,16 +17,19 @@ "eslint-plugin-react": "6.9.0", "eslint-plugin-vazco": "1.0.0", "graphql": "0.9.1", + "material-ui": "0.16.7", "meteor-node-stubs": "0.2.5", - "react": "15.4.1", - "react-dom": "15.4.1", + "react": "15.4.2", + "react-dom": "15.4.2", "react-mounter": "1.2.0", "react-panelgroup": "1.0.2", + "react-tap-event-plugin": "2.0.1", "simpl-schema": "0.1.0", "uniforms": "file:../packages/uniforms", "uniforms-antd": "file:../packages/uniforms-antd", "uniforms-bootstrap3": "file:../packages/uniforms-bootstrap3", "uniforms-bootstrap4": "file:../packages/uniforms-bootstrap4", + "uniforms-material": "file:../packages/uniforms-material", "uniforms-semantic": "file:../packages/uniforms-semantic", "uniforms-unstyled": "file:../packages/uniforms-unstyled" }, diff --git a/package.json b/package.json index 4a7be1b17..7c7a67a1c 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,5 @@ ], "sourceMap": false, "instrument": false - }, - "dependencies": {} + } } diff --git a/packages/uniforms-antd/package.json b/packages/uniforms-antd/package.json index 9a6669184..9777973eb 100644 --- a/packages/uniforms-antd/package.json +++ b/packages/uniforms-antd/package.json @@ -56,9 +56,9 @@ "jsdom": "9.10.0", "mocha": "3.2.0", "nyc": "10.1.2", - "react": "15.4.1", - "react-addons-test-utils": "15.4.1", - "react-dom": "15.4.1", + "react": "15.4.2", + "react-addons-test-utils": "15.4.2", + "react-dom": "15.4.2", "rimraf": "2.5.4", "sinon": "2.0.0-pre.5" }, diff --git a/packages/uniforms-bootstrap3/package.json b/packages/uniforms-bootstrap3/package.json index 682f5e680..1c2fd9657 100644 --- a/packages/uniforms-bootstrap3/package.json +++ b/packages/uniforms-bootstrap3/package.json @@ -55,9 +55,9 @@ "jsdom": "9.10.0", "mocha": "3.2.0", "nyc": "10.1.2", - "react": "15.4.1", - "react-addons-test-utils": "15.4.1", - "react-dom": "15.4.1", + "react": "15.4.2", + "react-addons-test-utils": "15.4.2", + "react-dom": "15.4.2", "rimraf": "2.5.4", "sinon": "2.0.0-pre.5" }, diff --git a/packages/uniforms-bootstrap4/package.json b/packages/uniforms-bootstrap4/package.json index 7f76cbb97..340472fda 100644 --- a/packages/uniforms-bootstrap4/package.json +++ b/packages/uniforms-bootstrap4/package.json @@ -55,9 +55,9 @@ "jsdom": "9.10.0", "mocha": "3.2.0", "nyc": "10.1.2", - "react": "15.4.1", - "react-addons-test-utils": "15.4.1", - "react-dom": "15.4.1", + "react": "15.4.2", + "react-addons-test-utils": "15.4.2", + "react-dom": "15.4.2", "rimraf": "2.5.4", "sinon": "2.0.0-pre.5" }, diff --git a/packages/uniforms-material/README.md b/packages/uniforms-material/README.md new file mode 100644 index 000000000..ea9b3b553 --- /dev/null +++ b/packages/uniforms-material/README.md @@ -0,0 +1,11 @@ +# uniforms-material + +> Material UI components for `uniforms`. + +## Install + +```sh +$ npm install uniforms-material +``` + +For more in depth documentation see: [https://github.com/vazco/uniforms/](https://github.com/vazco/uniforms/). diff --git a/packages/uniforms-material/package.json b/packages/uniforms-material/package.json new file mode 100644 index 000000000..e9245476d --- /dev/null +++ b/packages/uniforms-material/package.json @@ -0,0 +1,109 @@ +{ + "name": "uniforms-material", + "version": "1.9.0", + "main": "index.js", + "jsnext:main": "src/index.js", + "description": "Material UI components for uniforms.", + "repository": "https://github.com/vazco/uniforms/tree/master/packages/uniforms-material", + "homepage": "https://github.com/vazco/uniforms/", + "license": "MIT", + "bugs": { + "url": "https://github.com/vazco/uniforms/issues" + }, + "keyword": [ + "form", + "forms", + "meteor", + "react", + "react-component", + "schema", + "validation" + ], + "files": [ + "*.js", + "src/" + ], + "scripts": { + "build": "babel --out-dir . src", + "clean": "rimraf *.js coverage .nyc_output", + "cover": "cross-env NODE_ENV=cover nyc npm test", + "lint": "eslint src test", + "prepublish": "npm run build", + "pretest": "npm run lint", + "reset": "rimraf node_modules", + "test": "babel-node node_modules/.bin/_mocha --require test/index.js test --reporter list" + }, + "devDependencies": { + "babel-cli": "6.22.2", + "babel-eslint": "7.1.1", + "babel-plugin-istanbul": "3.1.2", + "babel-plugin-transform-object-assign": "6.22.0", + "babel-plugin-transform-react-inline-elements": "6.22.0", + "babel-plugin-transform-runtime": "6.22.0", + "babel-preset-es2015": "6.22.0", + "babel-preset-react": "6.22.0", + "babel-preset-stage-0": "6.22.0", + "babel-runtime": "6.22.0", + "chai": "4.0.0-canary.1", + "cross-env": "3.1.4", + "enzyme": "2.7.1", + "eslint": "3.15.0", + "eslint-config-vazco": "2.2.1", + "eslint-plugin-babel": "4.0.1", + "eslint-plugin-react": "6.9.0", + "eslint-plugin-vazco": "1.0.0", + "jsdom": "9.10.0", + "material-ui": "0.16.7", + "mocha": "3.2.0", + "nyc": "10.1.2", + "react": "15.4.2", + "react-addons-test-utils": "15.4.2", + "react-dom": "15.4.2", + "react-tap-event-plugin": "2.0.1", + "rimraf": "2.5.4", + "sinon": "2.0.0-pre.5" + }, + "peerDependencies": { + "material-ui": "^0.16.7", + "react": "^15.0.0 || ^0.14.0", + "uniforms": "^1.9.0" + }, + "babel": { + "plugins": [ + "transform-object-assign", + "transform-react-inline-elements", + "transform-runtime" + ], + "presets": [ + "es2015", + "react", + "stage-0" + ], + "env": { + "cover": { + "plugins": [ + "istanbul" + ] + } + } + }, + "eslintConfig": { + "root": true, + "extends": [ + "vazco" + ] + }, + "nyc": { + "reporter": [ + "html" + ], + "require": [ + "babel-register" + ], + "sourceMap": false, + "instrument": false + }, + "dependencies": { + "babel-runtime": "^6.20.0" + } +} diff --git a/packages/uniforms-material/src/AutoField.js b/packages/uniforms-material/src/AutoField.js new file mode 100644 index 000000000..645542e5e --- /dev/null +++ b/packages/uniforms-material/src/AutoField.js @@ -0,0 +1,39 @@ +import connectField from 'uniforms/connectField'; +import invariant from 'fbjs/lib/invariant'; +import {createElement} from 'react'; + +import NumField from './NumField'; +import BoolField from './BoolField'; +import DateField from './DateField'; +import ListField from './ListField'; +import NestField from './NestField'; +import TextField from './TextField'; +import RadioField from './RadioField'; +import SelectField from './SelectField'; + +const Auto = ({component, ...props}) => { + if (component === undefined) { + if (props.allowedValues) { + if (props.checkboxes && props.fieldType !== Array) { + component = RadioField; + } else { + component = SelectField; + } + } else { + switch (props.fieldType) { + case Date: component = DateField; break; + case Array: component = ListField; break; + case Number: component = NumField; break; + case Object: component = NestField; break; + case String: component = TextField; break; + case Boolean: component = BoolField; break; + } + + invariant(component, 'Unsupported field type: %s', props.fieldType.toString()); + } + } + + return createElement(component, props); +}; + +export default connectField(Auto, {ensureValue: false, includeInChain: false, initialValue: false}); diff --git a/packages/uniforms-material/src/AutoFields.js b/packages/uniforms-material/src/AutoFields.js new file mode 100644 index 000000000..e1430eab5 --- /dev/null +++ b/packages/uniforms-material/src/AutoFields.js @@ -0,0 +1,32 @@ +import {PropTypes} from 'react'; +import {createElement} from 'react'; + +import AutoField from './AutoField'; + +const AutoFields = ({autoField, element, fields, omitFields, ...props}, {uniforms: {schema}}) => + createElement( + element, + props, + (fields || schema.getSubfields()) + .filter(field => omitFields.indexOf(field) === -1) + .map(field => createElement(autoField, {key: field, name: field})) + ) +; + +AutoFields.contextTypes = AutoField.contextTypes; + +AutoFields.propTypes = { + autoField: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + element: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + + fields: PropTypes.arrayOf(PropTypes.string), + omitFields: PropTypes.arrayOf(PropTypes.string) +}; + +AutoFields.defaultProps = { + autoField: AutoField, + element: 'div', + omitFields: [] +}; + +export default AutoFields; diff --git a/packages/uniforms-material/src/AutoForm.js b/packages/uniforms-material/src/AutoForm.js new file mode 100644 index 000000000..96a995f8c --- /dev/null +++ b/packages/uniforms-material/src/AutoForm.js @@ -0,0 +1,9 @@ +import AutoForm from 'uniforms/AutoForm'; + +import ValidatedQuickForm from './ValidatedQuickForm'; + +const Auto = parent => class extends AutoForm.Auto(parent) { + static Auto = Auto; +}; + +export default Auto(ValidatedQuickForm); diff --git a/packages/uniforms-material/src/BaseForm.js b/packages/uniforms-material/src/BaseForm.js new file mode 100644 index 000000000..eb943a390 --- /dev/null +++ b/packages/uniforms-material/src/BaseForm.js @@ -0,0 +1,9 @@ +import BaseForm from 'uniforms/BaseForm'; + +const Material = parent => class extends parent { + static Material = Material; + + static displayName = `Material${parent.displayName}`; +}; + +export default Material(BaseForm); diff --git a/packages/uniforms-material/src/BoolField.js b/packages/uniforms-material/src/BoolField.js new file mode 100644 index 000000000..6e768ab44 --- /dev/null +++ b/packages/uniforms-material/src/BoolField.js @@ -0,0 +1,28 @@ +import Checkbox from 'material-ui/Checkbox'; +import React from 'react'; +import connectField from 'uniforms/connectField'; +import filterDOMProps from 'uniforms/filterDOMProps'; + +const Bool = ({ + disabled, + id, + inputRef, + label, + name, + onChange, + value, + ...props +}) => + disabled || onChange(value)} + ref={inputRef} + {...filterDOMProps(props)} + /> +; + +export default connectField(Bool); diff --git a/packages/uniforms-material/src/DateField.js b/packages/uniforms-material/src/DateField.js new file mode 100644 index 000000000..3dcacd69a --- /dev/null +++ b/packages/uniforms-material/src/DateField.js @@ -0,0 +1,93 @@ +import DatePicker from 'material-ui/DatePicker'; +import React from 'react'; +import TextField from './TextField'; +import TimePicker from 'material-ui/TimePicker'; +import connectField from 'uniforms/connectField'; +import {Component} from 'react'; + +const noop = () => {}; +const dateFormat = date => date && date.toLocaleString(); + +class Date_ extends Component { + static displayName = 'Date'; + + static defaultProps = { + fullWidth: true + }; + + constructor () { + super(...arguments); + + this._intermediate = null; + + this.onFocus = this.onFocus.bind(this); + this.onChangeDate = this.onChangeDate.bind(this); + this.onChangeTime = this.onChangeTime.bind(this); + } + + onChangeDate (event, date) { + this._intermediate = date; + this.refs.timepicker.openDialog(); + } + + onChangeTime (event, date) { + this._intermediate.setHours(date.getHours()); + this._intermediate.setMinutes(date.getMinutes()); + this._intermediate.setSeconds(date.getSeconds()); + this._intermediate.setMilliseconds(date.getMilliseconds()); + + this.props.onChange(this._intermediate); + + this._intermediate = null; + } + + onFocus () { + setTimeout(() => this.refs.datepicker.openDialog(), 100); + } + + render () { + const { + props: { + id, + max, + min, + timeFormat, + value, + ...props + } + } = this; + + return ( +
+ + + + + +
+ ); + } +} + +export default connectField(Date_, {ensureValue: false, includeInChain: false}); diff --git a/packages/uniforms-material/src/ErrorField.js b/packages/uniforms-material/src/ErrorField.js new file mode 100644 index 000000000..4b3793b3e --- /dev/null +++ b/packages/uniforms-material/src/ErrorField.js @@ -0,0 +1,23 @@ +import ErrorOutline from 'material-ui/svg-icons/alert/error-outline'; +import React from 'react'; +import connectField from 'uniforms/connectField'; +import filterDOMProps from 'uniforms/filterDOMProps'; +import nothing from 'uniforms/nothing'; +import {ListItem} from 'material-ui/List'; + +const Error = ({ + children, + errorMessage, + ...props +}) => + !errorMessage ? nothing : ( + } + primaryText={children || errorMessage} + {...filterDOMProps(props)} + /> + ) +; + +export default connectField(Error, {initialValue: false}); diff --git a/packages/uniforms-material/src/ErrorsField.js b/packages/uniforms-material/src/ErrorsField.js new file mode 100644 index 000000000..a4c43122a --- /dev/null +++ b/packages/uniforms-material/src/ErrorsField.js @@ -0,0 +1,24 @@ +import BaseField from 'uniforms/BaseField'; +import ErrorOutline from 'material-ui/svg-icons/alert/error-outline'; +import React from 'react'; +import filterDOMProps from 'uniforms/filterDOMProps'; +import nothing from 'uniforms/nothing'; +import {ListItem} from 'material-ui/List'; +import {List} from 'material-ui/List'; + +const ErrorsField = ({children, ...props}, {uniforms: {error, schema}}) => + (!error && !children) ? nothing : ( + + {!!children && ( + + )} + {schema.getErrorMessages(error).map((message, index) => + } /> + )} + + ) +; + +ErrorsField.contextTypes = BaseField.contextTypes; + +export default ErrorsField; diff --git a/packages/uniforms-material/src/HiddenField.js b/packages/uniforms-material/src/HiddenField.js new file mode 100644 index 000000000..dfe54180a --- /dev/null +++ b/packages/uniforms-material/src/HiddenField.js @@ -0,0 +1,27 @@ +import BaseField from 'uniforms/BaseField'; +import React from 'react'; +import filterDOMProps from 'uniforms/filterDOMProps'; +import nothing from 'uniforms/nothing'; + +export default class HiddenField extends BaseField { + componentWillReceiveProps ({value: valueDesired}) { + if (valueDesired === undefined) { + return; + } + + const props = this.getFieldProps(undefined, {overrideValue: true}); + if (props.value !== valueDesired) { + props.onChange(valueDesired); + } + } + + render () { + const props = this.getFieldProps(); + + return ( + props.noDOM ? nothing : ( + + ) + ); + } +} diff --git a/packages/uniforms-material/src/ListAddField.js b/packages/uniforms-material/src/ListAddField.js new file mode 100644 index 000000000..cb115a429 --- /dev/null +++ b/packages/uniforms-material/src/ListAddField.js @@ -0,0 +1,29 @@ +import Add from 'material-ui/svg-icons/content/add'; +import RaisedButton from 'material-ui/RaisedButton'; +import React from 'react'; +import connectField from 'uniforms/connectField'; +import filterDOMProps from 'uniforms/filterDOMProps'; + +const ListAdd = ({ + disabled, + parent, + value, + addLabel = 'Add', + icon: Icon = Add, + iconVisible = false, + ...props +}) => { + const limitNotReached = !disabled && !(parent.maxCount <= value.length); + + return ( + || null} + label={addLabel} + onTouchTap={() => limitNotReached && parent.onChange(parent.value.concat([value]))} + {...filterDOMProps(props)} + /> + ); +}; + +export default connectField(ListAdd, {includeParent: true, initialValue: false}); diff --git a/packages/uniforms-material/src/ListDelField.js b/packages/uniforms-material/src/ListDelField.js new file mode 100644 index 000000000..f0b345b17 --- /dev/null +++ b/packages/uniforms-material/src/ListDelField.js @@ -0,0 +1,32 @@ +import connectField from 'uniforms/connectField'; +import filterDOMProps from 'uniforms/filterDOMProps'; +import RaisedButton from 'material-ui/RaisedButton'; +import React from 'react'; +import Remove from 'material-ui/svg-icons/content/remove'; + +const ListDel = ({ + disabled, + name, + parent, + deletationLabel = 'Remove', + icon: Icon = Remove, + iconVisible = false, + ...props +}) => { + const fieldIndex = +name.slice(1 + name.lastIndexOf('.')); + const limitNotReached = !disabled && !(parent.minCount >= parent.value.length); + + return ( + || null} + label={deletationLabel} + onTouchTap={() => limitNotReached && parent.onChange( + [].concat(parent.value.slice(0, fieldIndex)).concat(parent.value.slice(1 + fieldIndex)) + )} + {...filterDOMProps(props)} + /> + ); +}; + +export default connectField(ListDel, {includeParent: true, initialValue: false}); diff --git a/packages/uniforms-material/src/ListField.js b/packages/uniforms-material/src/ListField.js new file mode 100644 index 000000000..124aaac5c --- /dev/null +++ b/packages/uniforms-material/src/ListField.js @@ -0,0 +1,53 @@ +import React from 'react'; +import Subheader from 'material-ui/Subheader'; +import connectField from 'uniforms/connectField'; +import filterDOMProps from 'uniforms/filterDOMProps'; +import joinName from 'uniforms/joinName'; +import {Children} from 'react'; +import {List as ListMaterial} from 'material-ui/List'; + +import ListAddField from './ListAddField'; +import ListItemField from './ListItemField'; + +const styles = { + actions: { + paddingTop: 8, + paddingBottom: 8 + } +}; + +const List = ({ + actionsStyle, + children, + initialCount, + itemProps, + label, + name, + value, + ...props +}) => + + {!!label && } + + {children ? ( + value.map((item, index) => + Children.map(children, child => + React.cloneElement(child, { + key: index, + label: null, + name: joinName(name, child.props.name && child.props.name.replace('$', index)) + }) + ) + ) + ) : ( + value.map((item, index) => + + ) + )} +
+ +
+
+; + +export default connectField(List, {includeInChain: false}); diff --git a/packages/uniforms-material/src/ListItemField.js b/packages/uniforms-material/src/ListItemField.js new file mode 100644 index 000000000..f93129aa2 --- /dev/null +++ b/packages/uniforms-material/src/ListItemField.js @@ -0,0 +1,32 @@ +import connectField from 'uniforms/connectField'; +import joinName from 'uniforms/joinName'; +import React from 'react'; +import {Card, CardActions, CardText} from 'material-ui/Card'; +import {Children} from 'react'; + +import AutoField from './AutoField'; +import ListDelField from './ListDelField'; + +const ListItem = props => + + + {props.children ? ( + Children.map(props.children, child => + React.cloneElement(child, { + name: joinName(props.name, child.props.name), + label: null + }) + ) + ) : ( + + )} + + + + + +; + +export default connectField(ListItem, {includeInChain: false}); + + diff --git a/packages/uniforms-material/src/LongTextField.js b/packages/uniforms-material/src/LongTextField.js new file mode 100644 index 000000000..4f58d89b4 --- /dev/null +++ b/packages/uniforms-material/src/LongTextField.js @@ -0,0 +1,37 @@ +import React from 'react'; +import TextField from 'material-ui/TextField'; +import connectField from 'uniforms/connectField'; +import filterDOMProps from 'uniforms/filterDOMProps'; + +const LongText = ({ + disabled, + id, + errorMessage, + inputRef, + label, + name, + onChange, + placeholder, + value, + ...props +}) => + onChange(value)} + placeholder={placeholder} + ref={inputRef} + value={value} + {...filterDOMProps(props)} + /> +; + +LongText.defaultProps = { + fullWidth: true +}; + +export default connectField(LongText); diff --git a/packages/uniforms-material/src/NestField.js b/packages/uniforms-material/src/NestField.js new file mode 100644 index 000000000..abc3b85e4 --- /dev/null +++ b/packages/uniforms-material/src/NestField.js @@ -0,0 +1,31 @@ +import React from 'react'; +import Subheader from 'material-ui/Subheader'; +import connectField from 'uniforms/connectField'; +import filterDOMProps from 'uniforms/filterDOMProps'; +import injectName from 'uniforms/injectName'; +import joinName from 'uniforms/joinName'; + +import AutoField from './AutoField'; + +const Nest = ({ + children, + fields, + label, + name, + style, + ...props +}) => +
+ {!!label && } + + {children ? ( + injectName(name, children) + ) : ( + fields.map(key => + + ) + )} +
+; + +export default connectField(Nest, {includeInChain: false}); diff --git a/packages/uniforms-material/src/NumField.js b/packages/uniforms-material/src/NumField.js new file mode 100644 index 000000000..10c775b6b --- /dev/null +++ b/packages/uniforms-material/src/NumField.js @@ -0,0 +1,46 @@ +import React from 'react'; +import TextField from 'material-ui/TextField'; +import connectField from 'uniforms/connectField'; +import filterDOMProps from 'uniforms/filterDOMProps'; + +const noneIfNaN = x => isNaN(x) ? undefined : x; + +const Num = ({ + decimal, + disabled, + errorMessage, + id, + inputRef, + label, + max, + min, + name, + onChange, + placeholder, + showInlineError, + value, + ...props +}) => + onChange(noneIfNaN((decimal ? parseFloat : parseInt)(value)))} + ref={inputRef} + step={decimal ? 0.01 : 1} + type="number" + value={value} + {...filterDOMProps(props)} + /> +; + +Num.defaultProps = { + fullWidth: true +}; + +export default connectField(Num); diff --git a/packages/uniforms-material/src/QuickForm.js b/packages/uniforms-material/src/QuickForm.js new file mode 100644 index 000000000..b39dce185 --- /dev/null +++ b/packages/uniforms-material/src/QuickForm.js @@ -0,0 +1,24 @@ +import QuickForm from 'uniforms/QuickForm'; + +import AutoField from './AutoField'; +import BaseForm from './BaseForm'; +import ErrorsField from './ErrorsField'; +import SubmitField from './SubmitField'; + +const Quick = parent => class extends QuickForm.Quick(parent) { + static Quick = Quick; + + getAutoField () { + return AutoField; + } + + getErrorsField () { + return ErrorsField; + } + + getSubmitField () { + return SubmitField; + } +}; + +export default Quick(BaseForm); diff --git a/packages/uniforms-material/src/RadioField.js b/packages/uniforms-material/src/RadioField.js new file mode 100644 index 000000000..e74999347 --- /dev/null +++ b/packages/uniforms-material/src/RadioField.js @@ -0,0 +1,44 @@ +import connectField from 'uniforms/connectField'; +import filterDOMProps from 'uniforms/filterDOMProps'; +import React from 'react'; +import Subheader from 'material-ui/Subheader'; +import {RadioButton, RadioButtonGroup} from 'material-ui/RadioButton'; + +const Radio = ({ + allowedValues, + disabled, + id, + label, + labelPosition, + name, + onChange, + transform, + value, + ...props +}) => +
+ {!!label && } + + onChange(value)} + {...filterDOMProps(props)} + > + {allowedValues.map(item => + + )} + +
+; + +export default connectField(Radio); diff --git a/packages/uniforms-material/src/SelectField.js b/packages/uniforms-material/src/SelectField.js new file mode 100644 index 000000000..ab66cdf4d --- /dev/null +++ b/packages/uniforms-material/src/SelectField.js @@ -0,0 +1,106 @@ +import Checkbox from 'material-ui/Checkbox'; +import connectField from 'uniforms/connectField'; +import filterDOMProps from 'uniforms/filterDOMProps'; +import MenuItem from 'material-ui/MenuItem'; +import RadioButton from 'material-ui/RadioButton'; +import React from 'react'; +import SelectField from 'material-ui/SelectField'; +import Subheader from 'material-ui/Subheader'; +import {List} from 'material-ui/List'; + +const xor = (item, array) => { + const index = array.indexOf(item); + if (index === -1) { + return array.concat([item]); + } + + return array.slice(0, index).concat(array.slice(index + 1)); +}; + +const renderCheckboxes = ({ + allowedValues, + disabled, + fieldType, + id, + label, + labelPosition, + name, + onChange, + transform, + value, + ...props +}) => + + {!!label && } + + {allowedValues.map(item => fieldType === Array ? ( + onChange(xor(item, value))} + /> + ) : ( + onChange(item)} + /> + ))} + +; + +const renderSelect = ({ + allowedValues, + disabled, + errorMessage, + fullWidth = true, + id, + inputRef, + label, + name, + onChange, + placeholder, + transform, + value, + ...props +}) => + onChange(value)} + ref={inputRef} + value={value} + {...filterDOMProps(props)} + > + {allowedValues.map(value => + + )} + +; + +const Select = props => + props.checkboxes || props.fieldType === Array + ? renderCheckboxes(props) + : renderSelect (props) +; + +export default connectField(Select, {ensureValue: false}); diff --git a/packages/uniforms-material/src/SubmitField.js b/packages/uniforms-material/src/SubmitField.js new file mode 100644 index 000000000..b1e127d60 --- /dev/null +++ b/packages/uniforms-material/src/SubmitField.js @@ -0,0 +1,22 @@ +import BaseField from 'uniforms/BaseField'; +import RaisedButton from 'material-ui/RaisedButton'; +import React from 'react'; +import filterDOMProps from 'uniforms/filterDOMProps'; + +const SubmitField = ({inputRef, label, value, ...props}, {uniforms: {error, state: {disabled}}}) => + +; + +SubmitField.contextTypes = BaseField.contextTypes; +SubmitField.defaultProps = { + label: 'Submit' +}; + +export default SubmitField; diff --git a/packages/uniforms-material/src/TextField.js b/packages/uniforms-material/src/TextField.js new file mode 100644 index 000000000..14d150049 --- /dev/null +++ b/packages/uniforms-material/src/TextField.js @@ -0,0 +1,40 @@ +import React from 'react'; +import TextField from 'material-ui/TextField'; +import connectField from 'uniforms/connectField'; +import filterDOMProps from 'uniforms/filterDOMProps'; + +const Text = ({ + disabled, + errorMessage, + id, + inputRef, + label, + name, + onChange, + placeholder, + showInlineError, + type, + value, + ...props +}) => + onChange(value)} + ref={inputRef} + type={type} + value={value} + {...filterDOMProps(props)} + /> +; + +Text.defaultProps = { + fullWidth: true, + type: 'text' +}; + +export default connectField(Text, {ensureValue: true}); diff --git a/packages/uniforms-material/src/ValidatedForm.js b/packages/uniforms-material/src/ValidatedForm.js new file mode 100644 index 000000000..e4415392a --- /dev/null +++ b/packages/uniforms-material/src/ValidatedForm.js @@ -0,0 +1,9 @@ +import ValidatedForm from 'uniforms/ValidatedForm'; + +import BaseForm from './BaseForm'; + +const Validated = parent => class extends ValidatedForm.Validated(parent) { + static Validated = Validated; +}; + +export default Validated(BaseForm); diff --git a/packages/uniforms-material/src/ValidatedQuickForm.js b/packages/uniforms-material/src/ValidatedQuickForm.js new file mode 100644 index 000000000..1036d372f --- /dev/null +++ b/packages/uniforms-material/src/ValidatedQuickForm.js @@ -0,0 +1,5 @@ +import BaseForm from './BaseForm'; +import QuickForm from './QuickForm'; +import ValidatedForm from './ValidatedForm'; + +export default ValidatedForm.Validated(QuickForm.Quick(BaseForm)); diff --git a/packages/uniforms-material/src/index.js b/packages/uniforms-material/src/index.js new file mode 100644 index 000000000..8ad143c96 --- /dev/null +++ b/packages/uniforms-material/src/index.js @@ -0,0 +1,23 @@ +export {default as AutoFields} from './AutoFields'; +export {default as AutoField} from './AutoField'; +export {default as AutoForm} from './AutoForm'; +export {default as BaseForm} from './BaseForm'; +export {default as BoolField} from './BoolField'; +export {default as DateField} from './DateField'; +export {default as ErrorField} from './ErrorField'; +export {default as ErrorsField} from './ErrorsField'; +export {default as HiddenField} from './HiddenField'; +export {default as ListAddField} from './ListAddField'; +export {default as ListDelField} from './ListDelField'; +export {default as ListField} from './ListField'; +export {default as ListItemField} from './ListItemField'; +export {default as LongTextField} from './LongTextField'; +export {default as NestField} from './NestField'; +export {default as NumField} from './NumField'; +export {default as QuickForm} from './QuickForm'; +export {default as RadioField} from './RadioField'; +export {default as SelectField} from './SelectField'; +export {default as SubmitField} from './SubmitField'; +export {default as TextField} from './TextField'; +export {default as ValidatedForm} from './ValidatedForm'; +export {default as ValidatedQuickForm} from './ValidatedQuickForm'; diff --git a/packages/uniforms-material/test/combined.js b/packages/uniforms-material/test/combined.js new file mode 100644 index 000000000..c603d328d --- /dev/null +++ b/packages/uniforms-material/test/combined.js @@ -0,0 +1,484 @@ +import React from 'react'; +import {describe} from 'mocha'; +import {expect} from 'chai'; +import {it} from 'mocha'; +import {mount} from 'enzyme'; +import {spy} from 'sinon'; +import {stub} from 'sinon'; + +import MaterialCheckbox from 'material-ui/Checkbox'; +import MaterialDatePicker from 'material-ui/DatePicker'; +import MaterialRadio from 'material-ui/RadioButton'; +import MaterialRadioGroup from 'material-ui/RadioButton/RadioButtonGroup'; +import MaterialSelectField from 'material-ui/SelectField'; +import MaterialTextField from 'material-ui/TextField'; +import MaterialTimePicker from 'material-ui/TimePicker'; +import getMuiTheme from 'material-ui/styles/getMuiTheme'; +import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme'; + +import AutoFields from 'uniforms-material/AutoFields'; +import AutoForm from 'uniforms-material/AutoForm'; +import DateField from 'uniforms-material/DateField'; +import ErrorField from 'uniforms-material/ErrorField'; +import ErrorsField from 'uniforms-material/ErrorsField'; +import HiddenField from 'uniforms-material/HiddenField'; +import ListAddField from 'uniforms-material/ListAddField'; +import ListDelField from 'uniforms-material/ListDelField'; +import ListField from 'uniforms-material/ListField'; +import ListItemField from 'uniforms-material/ListItemField'; +import LongTextField from 'uniforms-material/LongTextField'; +import NumField from 'uniforms-material/NumField'; +import RadioField from 'uniforms-material/RadioField'; +import SelectField from 'uniforms-material/SelectField'; +import SubmitField from 'uniforms-material/SubmitField'; +import filterDOMProps from 'uniforms/filterDOMProps'; + +filterDOMProps.register( + '__type__', + 'allowedValues', + 'checkboxes', + 'component', + 'maxCount', + 'minCount', + 'subfields' +); + +describe('Everything', () => { + const validator = stub(); + + const onChange = spy(); + const onSubmit = spy(); + + const dateA = new Date(2004, 4, 4); + const dateB = new Date(2005, 5, 5); + + const label = 'label'; + const required = true; + const transform = x => x; + const checkboxes = true; + const allowedValues = ['1', '2', '3']; + const base = {label, required}; + + const schema = { + 'x00': {...base, __type__: Number}, + 'x01': {...base, __type__: String}, + 'x02': {...base, __type__: String, allowedValues}, + 'x03': {...base, __type__: String, allowedValues, checkboxes}, + 'x04': {...base, __type__: Array, allowedValues, checkboxes}, + 'x04.$': {...base, __type__: Number}, + 'x05': {...base, __type__: Date}, + 'x06': {...base, __type__: Boolean}, + 'x08': {...base, __type__: Object, subfields: ['y01', 'y02']}, + 'x08.y01': {...base, __type__: String}, + 'x08.y02': {...base, __type__: Number}, + 'x09': {...base, __type__: Array}, + 'x09.$': {...base, __type__: String}, + 'x15': {...base, __type__: Array, minCount: 3}, + 'x15.$': {...base, __type__: String}, + 'x16': {...base, __type__: Array, maxCount: 3}, + 'x16.$': {...base, __type__: String}, + 'x17': {...base, __type__: String, allowedValues, transform}, + 'x18': {...base, __type__: String, allowedValues, checkboxes, transform}, + 'x19': {...base, __type__: Array, allowedValues, checkboxes, transform}, + 'x22': {...base, __type__: Number, decimal: true}, + 'x23': {...base, __type__: String, type: 'password'}, + 'x24': {...base, __type__: Object, children:

x24

}, + 'x25': {...base, __type__: String, component: LongTextField}, + 'x26': {...base, __type__: Array, initialCount: 1, children:

x27

}, + 'x26.$': {...base, __type__: String}, + 'x27': {...base, __type__: Array, minCount: 1, initialCount: 1, maxCount: 2}, + 'x27.$': {...base, __type__: String}, + 'x28': {...base, __type__: String, component: ErrorField}, + 'x31': {...base, __type__: String, allowedValues, checkboxes, component: SelectField}, + 'x32': {...base, __type__: String, component: HiddenField}, + 'x33': {...base, __type__: String, component: HiddenField, value: undefined}, + 'x34': {...base, __type__: Number, step: 4} + }; + + const bridgeName = name => name.replace(/\.\d+/g, '.$'); + const bridge = { + getError: (name, error) => error ? {noop: 0} : undefined, + getErrorMessage: (name, error) => error ? 'message' : undefined, + + getErrorMessages: error => error + ? Object.keys(schema) + : [], + + getField: name => schema[bridgeName(name)], + getType: name => schema[bridgeName(name)].__type__, + getProps: name => ({...schema[bridgeName(name)], __type__: null}), + + getInitialValue: name => schema[bridgeName(name)].__type__ === Date + ? dateA + : schema[bridgeName(name)].allowedValues && schema[bridgeName(name)].__type__ !== Array + ? schema[bridgeName(name)].allowedValues[0] + : schema[bridgeName(name)].__type__(), + + getSubfields: name => name + ? schema[bridgeName(name)].subfields || [] + : Object.keys(schema).filter(field => field.indexOf('.') === -1), + + getValidator: () => validator + }; + + const wrapper = mount( + , + { + childContextTypes: {muiTheme: React.PropTypes.object.isRequired}, + context: {muiTheme: getMuiTheme(lightBaseTheme, {userAgent: false})} + } + ); + + it('works (AutoFields, ErrorsField, SubmitField)', async function _ () { + this.timeout(30000); + + const children = ( +
+ + + +
+ ); + + wrapper.setProps({children}); + wrapper.update(); + + await new Promise(resolve => setTimeout(resolve, 5)); + + wrapper.setProps({children: null}); + wrapper.update(); + }); + + it('works (NumField)', async () => { + const find = () => wrapper.find(MaterialTextField).filterWhere(x => x.props().name === 'x00'); + + expect(find().props()).to.have.property('value', 0); + expect(find().props().onChange({}, 0)).to.equal(undefined); + expect(find().props()).to.have.property('value', 0); + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x00', 0)).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x00: 0})).to.be.ok; + }); + + it('works (NumField, invalid)', async () => { + const find = () => wrapper.find(MaterialTextField).filterWhere(x => x.props().name === 'x00'); + + expect(find().props()).to.have.property('value', 0); + expect(find().props().onChange({}, NaN)).to.equal(undefined); + expect(find().props()).to.have.property('value', ''); + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x00', undefined)).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x00: undefined})).to.be.ok; + }); + + it('works (NumField, step)', async () => { + const find = () => wrapper.find(MaterialTextField).filterWhere(x => x.props().name === 'x34'); + + expect(find().props()).to.have.property('step', 4); + }); + + it('works (TextField)', async () => { + const find = () => wrapper.find(MaterialTextField).filterWhere(x => x.props().name === 'x01'); + + expect(find().props()).to.have.property('value', ''); + expect(find().props().onChange({}, 'x01')).to.equal(undefined); + expect(find().props()).to.have.property('value', 'x01'); + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x01', 'x01')).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x01: 'x01'})).to.be.ok; + }); + + it('works (SelectField)', async () => { + const find = () => wrapper.find(MaterialSelectField).filterWhere(x => x.props().name === 'x02'); + + expect(find().props()).to.have.property('value', '1'); + expect(find().props().onChange({}, 1, '2')).to.equal(undefined); + expect(find().props()).to.have.property('value', '2'); + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x02', '2')).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x02: '2'})).to.be.ok; + }); + + it('works (RadioField, on)', async () => { + const find = () => wrapper.find(RadioField).filterWhere(x => x.props().name === 'x03'); + + expect(find().find(MaterialRadio).at(0).prop('checked')).to.be.true; + expect(find().find(MaterialRadio).at(1).prop('checked')).to.be.false; + expect(find().find(MaterialRadio).at(2).prop('checked')).to.be.false; + expect(find().find(MaterialRadioGroup).props().onChange({}, '2')).to.equal(undefined); + expect(find().find(MaterialRadio).at(0).prop('checked')).to.be.false; + expect(find().find(MaterialRadio).at(1).prop('checked')).to.be.true; + expect(find().find(MaterialRadio).at(2).prop('checked')).to.be.false; + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x03', '2')).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x03: '2'})).to.be.ok; + }); + + it('works (RadioField, off)', async () => { + const find = () => wrapper.find(RadioField).filterWhere(x => x.props().name === 'x03'); + + expect(find().find(MaterialRadio).at(0).prop('checked')).to.be.false; + expect(find().find(MaterialRadio).at(1).prop('checked')).to.be.true; + expect(find().find(MaterialRadio).at(2).prop('checked')).to.be.false; + expect(find().find(MaterialRadioGroup).props().onChange({}, '1')).to.equal(undefined); + expect(find().find(MaterialRadio).at(0).prop('checked')).to.be.true; + expect(find().find(MaterialRadio).at(1).prop('checked')).to.be.false; + expect(find().find(MaterialRadio).at(2).prop('checked')).to.be.false; + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x03', '1')).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x03: '1'})).to.be.ok; + }); + + it('works (SelectField, checkboxes, multiple, on)', async () => { + const find = () => wrapper.find(SelectField).filterWhere(x => x.props().name === 'x04'); + + expect(find().props()).to.have.property('value').that.is.deep.equal([]); + expect(find().find(MaterialCheckbox).at(1).props().onCheck()).to.equal(undefined); + expect(find().props()).to.have.property('value').that.is.deep.equal(['2']); + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x04', ['2'])).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x04: ['2']})).to.be.ok; + }); + + it('works (SelectField, checkboxes, multiple, off)', async () => { + const find = () => wrapper.find(SelectField).filterWhere(x => x.props().name === 'x04'); + + expect(find().props()).to.have.property('value').that.is.deep.equal(['2']); + expect(find().find(MaterialCheckbox).at(1).props().onCheck()).to.equal(undefined); + expect(find().props()).to.have.property('value').that.is.deep.equal([]); + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x04', [])).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x04: []})).to.be.ok; + }); + + it('works (DateField)', async () => { + const find = () => wrapper.find(DateField).filterWhere(x => x.props().name === 'x05'); + const input = find().find(MaterialTextField).filterWhere(x => x.props().name === 'x05').last(); + + expect(find().props()).to.have.property('value').that.is.deep.equal(dateA); + expect(find().find(MaterialTextField).first().simulate('focus')).to.be.ok; + expect(input.props().onFocus()).to.equal(undefined); + expect(find().find(MaterialDatePicker).props().onChange({}, dateB)).to.equal(undefined); + expect(find().find(MaterialTimePicker).props().onChange({}, dateB)).to.equal(undefined); + expect(find().props()).to.have.property('value').that.is.deep.equal(dateB); + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x05', dateB)).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x05: dateB})).to.be.ok; + }); + + it('works (BoolField)', async () => { + const find = () => wrapper.find(MaterialCheckbox).filterWhere(x => x.props().name === 'x06'); + + expect(find().props()).to.have.property('checked', false); + expect(find().props().onCheck({}, true)).to.equal(undefined); + expect(find().props()).to.have.property('checked', true); + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x06', true)).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x06: true})).to.be.ok; + }); + + it('works (NestField, TextField)', async () => { + const find = () => wrapper.find(MaterialTextField).filterWhere(x => x.props().name === 'x08.y01'); + + expect(find().props()).to.have.property('value', ''); + expect(find().props().onChange({}, 'x08y01')).to.equal(undefined); + expect(find().props()).to.have.property('value', 'x08y01'); + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x08.y01', 'x08y01')).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x08: {y01: 'x08y01'}})).to.be.ok; + }); + + it('works (NestField, NumField)', async () => { + const find = () => wrapper.find(MaterialTextField).filterWhere(x => x.props().name === 'x08.y02'); + + expect(find().props()).to.have.property('value', 0); + expect(find().props().onChange({}, 2)).to.equal(undefined); + expect(find().props()).to.have.property('value', 2); + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x08.y02', 2)).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x08: {y02: 2}})).to.be.ok; + }); + + it('works (NumField, decimal, nullable)', async () => { + const find = () => wrapper.find(MaterialTextField).filterWhere(x => x.props().name === 'x22'); + + expect(find().props()).to.have.property('value', 0); + expect(find().props().onChange({}, undefined)).to.equal(undefined); + expect(find().props()).to.have.property('value', ''); + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x22', undefined)).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x22: undefined})).to.be.ok; + }); + + it('works (NumField, decimal)', async () => { + const find = () => wrapper.find(MaterialTextField).filterWhere(x => x.props().name === 'x22'); + + expect(find().props()).to.have.property('value', ''); + expect(find().props().onChange({}, NaN)).to.equal(undefined); + expect(find().props()).to.have.property('value', ''); + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x22', undefined)).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x22: undefined})).to.be.ok; + }); + + it('works (LongTextField)', async () => { + const find = () => wrapper.find(MaterialTextField).filterWhere(x => x.props().name === 'x25'); + + expect(find().props()).to.have.property('value', ''); + expect(find().props().onChange({}, 'x25')).to.equal(undefined); + expect(find().props()).to.have.property('value', 'x25'); + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x25', 'x25')).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x25: 'x25'})).to.be.ok; + }); + + it('works (ListAddField, one)', async () => { + expect(wrapper.find(ListAddField).findWhere(x => x.props().onTouchTap).last().simulate('touchTap')).to.be.ok; + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x27.0', '')).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x27: ['']})).to.be.ok; + }); + + it('works (ListAddField, two)', async () => { + expect(wrapper.find(ListAddField).findWhere(x => x.props().onClick).last().simulate('touchTap')).to.be.ok; + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x27.1', '')).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x27: ['', '']})).to.be.ok; + }); + + it('works (ListDelField)', async () => { + expect(wrapper.find(ListDelField).at(0).children().simulate('touchTap')).to.be.ok; + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x27', [''])).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x27: ['']})).to.be.ok; + }); + + it('works (SelectField, checkboxes, multiple, on)', async () => { + const find = () => wrapper.find(SelectField).filterWhere(x => x.props().name === 'x31'); + + expect(find().find(MaterialRadio).at(0).prop('checked')).to.be.true; + expect(find().find(MaterialRadio).at(1).prop('checked')).to.be.false; + expect(find().find(MaterialRadio).at(2).prop('checked')).to.be.false; + expect(find().find(MaterialRadio).at(1).props().onCheck()).to.equal(undefined); + expect(find().find(MaterialRadio).at(0).prop('checked')).to.be.false; + expect(find().find(MaterialRadio).at(1).prop('checked')).to.be.true; + expect(find().find(MaterialRadio).at(2).prop('checked')).to.be.false; + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x31', '2')).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x31: '2'})).to.be.ok; + }); + + it('works (HiddenField)', async () => { + const wrapperHidden = mount( + , + {context: wrapper.instance().getChildContext()} + ); + + expect(wrapperHidden.find(HiddenField).props()).to.have.property('value', ''); + + wrapperHidden.setProps({value: 'x32'}); + + expect(wrapperHidden.find(HiddenField).props()).to.have.property('value', 'x32'); + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x32', 'x32')).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x32: 'x32'})).to.be.ok; + }); + + it('works (HiddenField, noDOM)', async () => { + const wrapperHidden = mount( + , + {context: wrapper.instance().getChildContext()} + ); + + expect(wrapperHidden.find(HiddenField).props()).to.have.property('value', ''); + + wrapperHidden.setProps({value: 'x32'}); + + expect(wrapperHidden.find(HiddenField).props()).to.have.property('value', 'x32'); + + await new Promise(resolve => setTimeout(resolve, 5)); + + expect(onChange.lastCall.calledWith('x32', 'x32')).to.be.ok; + expect(onSubmit.lastCall.calledWithMatch({x32: 'x32'})).to.be.ok; + }); + + it('works (ListField, custom children)', async function _ () { + this.timeout(30000); + + const children = ( + + + + + + ); + + wrapper.setProps({children}); + wrapper.update(); + + await new Promise(resolve => setTimeout(resolve, 5)); + + wrapper.setProps({children: null}); + wrapper.update(); + }); + + it('works (remount)', () => { + wrapper.unmount(); + wrapper.mount(); + }); + + it('works (rest)', () => { + wrapper.setProps({error: {}}); + wrapper.setProps({model: {...wrapper.state('model'), x09: ['', '', '']}}); + + schema.x = {__type__: () => {}}; + + expect(() => wrapper.update()).to.throw(/Unsupported field type/); + }); +}); diff --git a/packages/uniforms-material/test/index.js b/packages/uniforms-material/test/index.js new file mode 100644 index 000000000..369a7d2a7 --- /dev/null +++ b/packages/uniforms-material/test/index.js @@ -0,0 +1,28 @@ +// DOM for React +import {jsdom} from 'jsdom'; + +global.document = jsdom(''); +global.window = document.defaultView; +global.navigator = window.navigator; +Object.keys(window).forEach(property => { + if (typeof global[property] === 'undefined') { + global[property] = window[property]; + } +}); + +// Mocks +const Module = require('module'); +const loader = Module._load; +Module._load = function _load (request, parent) { + return loader( + request + .replace(/^uniforms-material/, '../src') + .replace(/^uniforms/, '../../uniforms/src'), + parent + ); +}; + +// MaterialUI +import injectTapEventPlugin from 'react-tap-event-plugin'; + +injectTapEventPlugin(); diff --git a/packages/uniforms-semantic/package.json b/packages/uniforms-semantic/package.json index 03c5eb9b4..84547bed9 100644 --- a/packages/uniforms-semantic/package.json +++ b/packages/uniforms-semantic/package.json @@ -55,9 +55,9 @@ "jsdom": "9.10.0", "mocha": "3.2.0", "nyc": "10.1.2", - "react": "15.4.1", - "react-addons-test-utils": "15.4.1", - "react-dom": "15.4.1", + "react": "15.4.2", + "react-addons-test-utils": "15.4.2", + "react-dom": "15.4.2", "rimraf": "2.5.4", "sinon": "2.0.0-pre.5" }, diff --git a/packages/uniforms-unstyled/package.json b/packages/uniforms-unstyled/package.json index ae5dfa816..3b27373d6 100644 --- a/packages/uniforms-unstyled/package.json +++ b/packages/uniforms-unstyled/package.json @@ -55,9 +55,9 @@ "jsdom": "9.10.0", "mocha": "3.2.0", "nyc": "10.1.2", - "react": "15.4.1", - "react-addons-test-utils": "15.4.1", - "react-dom": "15.4.1", + "react": "15.4.2", + "react-addons-test-utils": "15.4.2", + "react-dom": "15.4.2", "rimraf": "2.5.4", "sinon": "2.0.0-pre.5" }, diff --git a/packages/uniforms/package.json b/packages/uniforms/package.json index c9ee14088..8dc49ce75 100644 --- a/packages/uniforms/package.json +++ b/packages/uniforms/package.json @@ -56,9 +56,9 @@ "jsdom": "9.10.0", "mocha": "3.2.0", "nyc": "10.1.2", - "react": "15.4.1", - "react-addons-test-utils": "15.4.1", - "react-dom": "15.4.1", + "react": "15.4.2", + "react-addons-test-utils": "15.4.2", + "react-dom": "15.4.2", "rimraf": "2.5.4", "simpl-schema": "0.1.0", "sinon": "2.0.0-pre.5"