enable $unset of empty properties in edit form; bring in CheckboxGroup code to work around refs bug

This commit is contained in:
Sacha Greif 2016-03-31 09:11:47 +09:00
parent b23b7eea73
commit deb48b5a97
6 changed files with 385 additions and 13 deletions

View file

@ -137,7 +137,6 @@ tap:i18n@1.7.0
templating@1.1.7 templating@1.1.7
templating-tools@1.0.2 templating-tools@1.0.2
tmeasday:check-npm-versions@0.1.1 tmeasday:check-npm-versions@0.1.1
tmeasday:healthcheck-handler@0.0.2
tmeasday:publish-counts@0.7.3 tmeasday:publish-counts@0.7.3
tracker@1.0.11 tracker@1.0.11
twitter@1.1.7 twitter@1.1.7

View file

@ -17,17 +17,25 @@ const EditDocument = React.createClass({
labelFunction: React.PropTypes.func labelFunction: React.PropTypes.func
}, },
getFields() {
const collection = this.props.collection;
const fields = collection.getInsertableFields(this.props.currentUser);
return fields;
},
submitForm(data) { submitForm(data) {
console.log(data) const fields = this.getFields();
const document = this.props.document; const document = this.props.document;
const modifier = {$set: _.compactObject(Utils.flatten(data))}; // put all keys with data on $set
const set = _.compactObject(Utils.flatten(data));
// put all keys without data on $unset
const unsetKeys = _.difference(fields, _.keys(set));
const unset = _.object(unsetKeys, unsetKeys.map(()=>true));
const modifier = {$set: set, $unset: unset};
const collection = this.props.collection; const collection = this.props.collection;
const methodName = this.props.methodName ? this.props.methodName : collection._name+'.edit'; const methodName = this.props.methodName ? this.props.methodName : collection._name+'.edit';
console.log(modifier)
Meteor.call(methodName, document._id, modifier, (error, document) => { Meteor.call(methodName, document._id, modifier, (error, document) => {
if (error) { if (error) {
console.log(error) console.log(error)
@ -49,7 +57,7 @@ const EditDocument = React.createClass({
const document = this.props.document; const document = this.props.document;
const collection = this.props.collection; const collection = this.props.collection;
const fields = collection.getInsertableFields(this.props.currentUser); const fields = this.getFields();
const style = { const style = {
maxWidth: "800px", maxWidth: "800px",
@ -59,7 +67,11 @@ const EditDocument = React.createClass({
return ( return (
<div className="edit-document" style={style}> <div className="edit-document" style={style}>
<Formsy.Form onSubmit={this.submitForm}> <Formsy.Form onSubmit={this.submitForm}>
{fields.map(fieldName => <div key={fieldName} className={"input-"+fieldName}>{SmartForms.getComponent(fieldName, collection.simpleSchema()._schema[fieldName], this.props.labelFunction, document)}</div>)} {fields.map(fieldName =>
<div key={fieldName} className={"input-"+fieldName}>
{SmartForms.getComponent(fieldName, collection.simpleSchema()._schema[fieldName], this.props.labelFunction, document)}
</div>
)}
<Button type="submit" bsStyle="primary">Submit</Button> <Button type="submit" bsStyle="primary">Submit</Button>
</Formsy.Form> </Formsy.Form>
</div> </div>

View file

@ -0,0 +1,159 @@
'use strict';
var React = require('react');
module.exports = {
propTypes: {
layout: React.PropTypes.string,
validatePristine: React.PropTypes.bool,
rowClassName: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.array,
React.PropTypes.object
]),
labelClassName: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.array,
React.PropTypes.object
]),
elementWrapperClassName: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.array,
React.PropTypes.object
])
},
contextTypes: {
layout: React.PropTypes.string,
validatePristine: React.PropTypes.bool,
rowClassName: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.array,
React.PropTypes.object
]),
labelClassName: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.array,
React.PropTypes.object
]),
elementWrapperClassName: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.array,
React.PropTypes.object
])
},
getDefaultProps: function() {
return {
disabled: false,
validatePristine: false,
onChange: function() {},
onFocus: function() {},
onBlur: function() {}
};
},
/**
* Accessors for "special" properties.
*
* The following methods are used to merge master default properties that
* are optionally set on the parent form. This to to allow customising these
* properties 'as a whole' for the form, while retaining the ability to
* override the properties on a component basis.
*
* Also see the parent-context mixin.
*/
getLayout: function() {
var defaultProperty = this.context.layout || 'horizontal';
return this.props.layout ? this.props.layout : defaultProperty;
},
getValidatePristine: function() {
var defaultProperty = this.context.validatePristine || false;
return this.props.validatePristine ? this.props.validatePristine : defaultProperty;
},
getRowClassName: function() {
return [this.context.rowClassName, this.props.rowClassName];
},
getLabelClassName: function() {
return [this.context.labelClassName, this.props.labelClassName];
},
getElementWrapperClassName: function() {
return [this.context.elementWrapperClassName, this.props.elementWrapperClassName];
},
getRowProperties: function() {
return {
label: this.props.label,
rowClassName: this.getRowClassName(),
labelClassName: this.getLabelClassName(),
elementWrapperClassName: this.getElementWrapperClassName(),
layout: this.getLayout(),
required: this.isRequired(),
hasErrors: this.showErrors()
};
},
hashString: function(string) {
var hash = 0;
for (var i = 0; i < string.length; i++) {
hash = (((hash << 5) - hash) + string.charCodeAt(i)) & 0xFFFFFFFF;
}
return hash;
},
/**
* getId
*
* The ID is used as an attribute on the form control, and is used to allow
* associating the label element with the form control.
*
* If we don't explicitly pass an `id` prop, we generate one based on the
* `name` and `label` properties.
*/
getId: function() {
if (this.props.id) {
return this.props.id;
}
var label = (typeof this.props.label === 'undefined' ? '' : this.props.label);
return [
'frc',
this.props.name.split('[').join('_').replace(']', ''),
this.hashString(JSON.stringify(label))
].join('-');
},
renderHelp: function() {
if (!this.props.help) {
return '';
}
return (
<span className="help-block">{this.props.help}</span>
);
},
renderErrorMessage: function() {
if (!this.showErrors()) {
return '';
}
var errorMessages = this.getErrorMessages() || [];
return errorMessages.map((message, key) => {
return (
<span key={key} className="help-block validation-message">{message}</span>
);
});
},
showErrors: function() {
if (this.isPristine() === true) {
if (this.getValidatePristine() === false) {
return false;
}
}
return (this.isValid() === false);
}
};

View file

@ -0,0 +1,126 @@
/*jshint node:true */
'use strict';
var React = require('react');
var classNames = require('classnames/dedupe');
var Row = React.createClass({
propTypes: {
label: React.PropTypes.node,
rowClassName: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.array,
React.PropTypes.object
]),
labelClassName: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.array,
React.PropTypes.object
]),
elementWrapperClassName: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.array,
React.PropTypes.object
]),
required: React.PropTypes.bool,
hasErrors: React.PropTypes.bool,
fakeLabel: React.PropTypes.bool,
layout: React.PropTypes.oneOf(['horizontal', 'vertical', 'elementOnly']),
htmlFor: React.PropTypes.string
},
getDefaultProps: function() {
return {
label: '',
rowClassName: '',
labelClassName: '',
elementWrapperClassName: '',
required: false,
hasErrors: false,
fakeLabel: false
};
},
renderLabel: function() {
if (this.props.layout === 'elementOnly') {
return '';
}
var labelClassNames = [];
labelClassNames.push('control-label');
if (this.props.layout === 'horizontal') {
labelClassNames.push('col-sm-3');
}
labelClassNames.push(this.props.labelClassName);
if (this.props.fakeLabel) {
return (
<div className={classNames(labelClassNames)}>
<strong>
{this.props.label}
{this.props.required ? ' *' : null}
</strong>
</div>
);
}
return (
<label className={classNames(labelClassNames)} htmlFor={this.props.htmlFor}>
{this.props.label}
{this.props.required ? ' *' : null}
</label>
);
},
render: function() {
if (this.props.layout === 'elementOnly') {
return (
<span>
{this.props.children}
</span>
);
}
var cssClasses = {
row: ['form-group'],
elementWrapper: []
};
if (this.props.hasErrors) {
cssClasses.row.push('has-error');
cssClasses.row.push('has-feedback');
}
var element = this.props.children;
if (this.props.layout === 'horizontal') {
// Horizontal layout needs a 'row' class for Bootstrap 4
cssClasses.row.push('row');
cssClasses.elementWrapper.push('col-sm-9');
cssClasses.elementWrapper.push(this.props.elementWrapperClassName);
element = (
<div className={classNames(cssClasses.elementWrapper)}>
{this.props.children}
</div>
);
}
cssClasses.row.push(this.props.rowClassName);
return (
<div className={classNames(cssClasses.row)}>
{this.renderLabel()}
{element}
</div>
);
}
});
module.exports = Row;

View file

@ -5,7 +5,7 @@ import FRC from 'formsy-react-components';
import Utils from './utils.js'; import Utils from './utils.js';
const Checkbox = FRC.Checkbox; const Checkbox = FRC.Checkbox;
const CheckboxGroup = FRC.CheckboxGroup; // const CheckboxGroup = FRC.CheckboxGroup;
const Input = FRC.Input; const Input = FRC.Input;
const RadioGroup = FRC.RadioGroup; const RadioGroup = FRC.RadioGroup;
const Select = FRC.Select; const Select = FRC.Select;
@ -38,7 +38,7 @@ SmartForms.getComponent = (fieldName, field, labelFunction, document) => {
return <Checkbox key={fieldName} name={fieldName} value={value} label={label}/>; return <Checkbox key={fieldName} name={fieldName} value={value} label={label}/>;
// note: checkboxgroup cause React refs error, so use RadioGroup for now // note: checkboxgroup cause React refs error, so use RadioGroup for now
case "checkboxgroup": case "checkboxgroup":
return <RadioGroup key={fieldName} name={fieldName} value={value} label={label} options={options} />; return <CheckboxGroup key={fieldName} name={fieldName} value={value} label={label} options={options} />;
case "radiogroup": case "radiogroup":
return <RadioGroup key={fieldName} name={fieldName} value={value} label={label} options={options} />; return <RadioGroup key={fieldName} name={fieldName} value={value} label={label} options={options} />;
case "select": case "select":
@ -48,4 +48,80 @@ SmartForms.getComponent = (fieldName, field, labelFunction, document) => {
} }
} }
export default SmartForms; export default SmartForms;
import ComponentMixin from './component';
import Row from './row';
const CheckboxGroup = React.createClass({
mixins: [Formsy.Mixin, ComponentMixin],
propTypes: {
name: React.PropTypes.string.isRequired,
options: React.PropTypes.array.isRequired
},
getDefaultProps: function() {
return {
label: '',
help: null
};
},
changeCheckbox: function() {
var value = [];
this.props.options.forEach(function(option, key) {
if (this.refs['element-' + key].checked) {
value.push(option.value);
}
}.bind(this));
this.setValue(value);
this.props.onChange(this.props.name, value);
},
renderElement: function() {
var _this = this;
var controls = this.props.options.map(function(checkbox, key) {
var checked = (_this.getValue().indexOf(checkbox.value) !== -1);
var disabled = _this.isFormDisabled() || checkbox.disabled || _this.props.disabled;
return (
<div className="checkbox" key={key}>
<label>
<input
ref={'element-' + key}
checked={checked}
type="checkbox"
value={checkbox.value}
onChange={_this.changeCheckbox}
disabled={disabled}
/> {checkbox.label}
</label>
</div>
);
});
return controls;
},
render: function() {
if (this.getLayout() === 'elementOnly') {
return (
<div>{this.renderElement()}</div>
);
}
return (
<Row
{...this.getRowProperties()}
fakeLabel={true}
>
{this.renderElement()}
{this.renderHelp()}
{this.renderErrorMessage()}
</Row>
);
}
});

View file

@ -17,7 +17,7 @@
"postInterval": 20, "postInterval": 20,
"RSSLinksPointTo": "link", "RSSLinksPointTo": "link",
"commentInterval": 20, "commentInterval": 20,
"maxPostsPerDay": 10 "maxPostsPerDay": 10,
"startInvitesCount": 5, "startInvitesCount": 5,
"postsPerPage": 10, "postsPerPage": 10,
@ -27,7 +27,7 @@
"facebookPage": "http://facebook.com/foo", "facebookPage": "http://facebook.com/foo",
"googleAnalyticsId":"123foo", "googleAnalyticsId":"123foo",
"embedlyKey":"123foo", "embedlyKey":"123foo"
}, },