Merge branch 'erikdakoda5' of https://github.com/ErikDakoda/Vulcan into ErikDakoda-erikdakoda5

# Conflicts:
#	packages/vulcan-forms/lib/components/FormComponent.jsx
This commit is contained in:
SachaG 2018-05-10 09:44:50 +09:00
commit d60b16ea0d
12 changed files with 506 additions and 234 deletions

View file

@ -22,7 +22,10 @@ This component expects:
*/
import { registerComponent, Components, runCallbacks, getCollection, getErrors } from 'meteor/vulcan:core';
import {
registerComponent, Components, runCallbacks, getCollection,
getErrors, registerSetting, getSetting, Utils
} from 'meteor/vulcan:core';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n';
@ -34,8 +37,14 @@ import unset from 'lodash/unset';
import compact from 'lodash/compact';
import update from 'lodash/update';
import merge from 'lodash/merge';
import find from 'lodash/find';
import isEqualWith from 'lodash/isEqualWith';
import { convertSchema, formProperties } from '../modules/schema_utils';
registerSetting('forms.warnUnsavedChanges', false,
'Warn user about unsaved changes before leaving route', true);
// unsetCompact
const unsetCompact = (object, path) => {
const parentPath = path.slice(0, path.lastIndexOf('.'));
@ -68,7 +77,7 @@ const computeStateFromProps = (nextProps) => {
*/
class Form extends Component {
constructor(props) {
constructor (props) {
super(props);
this.state = {
@ -119,7 +128,18 @@ class Form extends Component {
*/
getDocument = () => {
const document = merge({}, this.state.initialDocument, this.defaultValues, this.state.currentValues);
const deletedValues = {};
this.state.deletedValues.forEach(path => {
set(deletedValues, path, null);
});
const document = merge(
{},
this.state.initialDocument,
this.defaultValues,
this.state.currentValues,
deletedValues
);
return document;
};
@ -223,9 +243,10 @@ class Form extends Component {
// remove all hidden fields
if (excludeHiddenFields) {
const document = this.getDocument();
relevantFields = _.reject(relevantFields, fieldName => {
const hidden = schema[fieldName].hidden;
return typeof hidden === 'function' ? hidden(this.props) : hidden;
return typeof hidden === 'function' ? hidden({ ...this.props, document }) : hidden;
});
}
@ -404,7 +425,11 @@ class Form extends Component {
return {
throwError: this.throwError,
clearForm: this.clearForm,
submitForm: this.submitFormContext, //Change in name because we already have a function called submitForm, but no reason for the user to know about that
refetchForm: this.refetchForm,
isChanged: this.isChanged,
submitForm: this.submitFormContext, //Change in name because we already have a function
// called submitForm, but no reason for the user to know
// about that
addToDeletedValues: this.addToDeletedValues,
updateCurrentValues: this.updateCurrentValues,
getDocument: this.getDocument,
@ -428,7 +453,9 @@ class Form extends Component {
}
/*
Manually update the current values of one or more fields(i.e. on change or blur).
*/
updateCurrentValues = newValues => {
// keep the previous ones and extend (with possible replacement) with new ones
@ -452,20 +479,91 @@ class Form extends Component {
};
/*
Warn the user if there are unsaved changes
*/
handleRouteLeave = () => {
if (this.isChanged()) {
const message = this.context.intl.formatMessage({
id: 'forms.confirm_discard',
defaultMessage: 'Are you sure you want to discard your changes?'
});
return message;
}
};
/*
Install a route leave hook to warn the user if there are unsaved changes
*/
componentDidMount = () => {
let warnUnsavedChanges = getSetting('forms.warnUnsavedChanges');
if (typeof this.props.warnUnsavedChanges === 'boolean') {
warnUnsavedChanges = this.props.warnUnsavedChanges;
}
if (warnUnsavedChanges) {
const routes = this.props.router.routes;
const currentRoute = routes[routes.length - 1];
this.props.router.setRouteLeaveHook(currentRoute, this.handleRouteLeave);
}
};
/*
Returns true if there are any differences between the initial document and the current one
*/
isChanged = () => {
const initialDocument = this.state.initialDocument;
const changedDocument = this.getDocument();
const changedValue = find(changedDocument, (value, key, collection) => {
return !isEqualWith(value, initialDocument[key], (objValue, othValue) => {
if (!objValue && !othValue) return true;
});
});
return typeof changedValue !== 'undefined';
};
/*
Refetch the document from the database (in case it was updated by another process or to reset the form)
*/
refetchForm = () => {
if (this.props.data && this.props.data.refetch) {
this.props.data.refetch();
}
};
/*
Clear and reset the form
By default, clear errors and keep current values and deleted values
*/
clearForm = ({ clearErrors = true, clearCurrentValues = false, clearDeletedValues = false }) => {
clearForm = ({
clearErrors = true,
clearCurrentValues = false,
clearDeletedValues = false,
document,
}) => {
document = document ? merge({}, this.props.prefilledProps, document) : null;
this.setState(prevState => ({
errors: clearErrors ? [] : prevState.errors,
currentValues: clearCurrentValues ? {} : prevState.currentValues,
deletedValues: clearDeletedValues ? [] : prevState.deletedValues,
initialDocument: document ? document : prevState.initialDocument,
disabled: false,
}));
};
/*
Key down handler
*/
@ -489,15 +587,11 @@ class Form extends Component {
// for new mutation, run refetch function if it exists
if (mutationType === 'new' && this.props.refetch) this.props.refetch();
// call the clear form method (i.e. trigger setState) only if the form has not been unmounted (we are in an async callback, everything can happen!)
// call the clear form method (i.e. trigger setState) only if the form has not been unmounted
// (we are in an async callback, everything can happen!)
if (typeof this.refs.form !== 'undefined') {
let clearCurrentValues = false;
// reset form if this is a new document form
if (this.getFormType() === 'new') {
this.refs.form.reset();
clearCurrentValues = true;
}
this.clearForm({ clearErrors: true, clearCurrentValues, clearDeletedValues: true });
this.clearForm({ clearErrors: true, clearCurrentValues: true, clearDeletedValues: true, document });
}
// run document through mutation success callbacks
@ -526,6 +620,8 @@ class Form extends Component {
// run error callback if it exists
if (this.props.errorCallback) this.props.errorCallback(document, error);
Utils.scrollIntoView('.flash-message');
};
/*
@ -632,14 +728,14 @@ class Form extends Component {
// ----------------------------- Render -------------------------------- //
// --------------------------------------------------------------------- //
render() {
render () {
const fieldGroups = this.getFieldGroups();
const collectionName = this.getCollection()._name;
return (
<div className={'document-' + this.getFormType()}>
<Formsy.Form onSubmit={this.submitForm} onKeyDown={this.formKeyDown} disabled={this.state.disabled} ref="form">
<Components.FormErrors errors={this.state.errors} />
<Components.FormErrors errors={this.state.errors}/>
{fieldGroups.map(group => (
<Components.FormGroup
@ -662,7 +758,9 @@ class Form extends Component {
<Components.FormSubmit
submitLabel={this.props.submitLabel}
cancelLabel={this.props.cancelLabel}
revertLabel={this.props.revertLabel}
cancelCallback={this.props.cancelCallback}
revertCallback={this.props.revertCallback}
document={this.getDocument()}
deleteDocument={(this.getFormType() === 'edit' && this.props.showRemove && this.deleteDocument) || null}
collectionName={collectionName}
@ -700,7 +798,9 @@ Form.propTypes = {
showRemove: PropTypes.bool,
submitLabel: PropTypes.string,
cancelLabel: PropTypes.string,
revertLabel: PropTypes.string,
repeatErrors: PropTypes.bool,
warnUnsavedChanges: PropTypes.bool,
// callbacks
submitCallback: PropTypes.func,
@ -708,6 +808,7 @@ Form.propTypes = {
removeSuccessCallback: PropTypes.func,
errorCallback: PropTypes.func,
cancelCallback: PropTypes.func,
revertCallback: PropTypes.func,
currentUser: PropTypes.object,
client: PropTypes.object,
@ -734,6 +835,8 @@ Form.childContextTypes = {
setFormState: PropTypes.func,
throwError: PropTypes.func,
clearForm: PropTypes.func,
refetchForm: PropTypes.func,
isChanged: PropTypes.func,
initialDocument: PropTypes.object,
getDocument: PropTypes.func,
submitForm: PropTypes.func,

View file

@ -1,46 +1,60 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n';
import classNames from 'classnames';
import { Components } from 'meteor/vulcan:core';
import { registerComponent } from 'meteor/vulcan:core';
import debounce from 'lodash.debounce';
import get from 'lodash/get';
import merge from 'lodash/merge';
import find from 'lodash/find';
import isObjectLike from 'lodash/isObjectLike';
import isEqual from 'lodash/isEqual';
import { isEmptyValue } from '../modules/utils.js';
class FormComponent extends Component {
constructor(props) {
constructor (props) {
super(props);
const value = this.getValue(props);
this.state = {};
}
if (this.showCharsRemaining(props)) {
const characterCount = value ? value.length : 0;
this.state = {
charsRemaining: props.max - characterCount,
};
componentWillMount () {
if (this.showCharsRemaining()) {
const value = this.getValue();
this.updateCharacterCount(value);
}
}
// shouldComponentUpdate(nextProps, nextState) {
// const { currentValues, deletedValues, errors } = nextProps;
// const { path } = this.props;
// const hasChanged = currentValues[path] && currentValues[path] !== this.props.currentValues[path];
// const hasError = !!errors[path];
// const hasBeenDeleted = deletedValues.includes(path) && !this.props.deletedValues.includes(path)
// return hasChanged || hasError || hasBeenDeleted;
// }
shouldComponentUpdate (nextProps, nextState) {
// allow custom controls to determine if they should update
if (!['nested', 'number', 'url', 'email', 'textarea', 'checkbox',
'checkboxgroup', 'radiogroup', 'select', 'selectmultiple', 'datetime',
'date', 'time', 'text'].includes(this.getType(nextProps))) {
return true;
}
const { currentValues, deletedValues, errors } = nextProps;
const { path } = this.props;
const valueChanged = currentValues[path] !== this.props.currentValues[path];
const errorChanged = !isEqual(this.getErrors(errors), this.getErrors());
const deleteChanged = deletedValues.includes(path) !== this.props.deletedValues.includes(path);
const charsChanged = nextState.charsRemaining !== this.state.charsRemaining;
return valueChanged || errorChanged || deleteChanged || charsChanged;
}
/*
Function passed to form controls (always controlled) to update their value
*/
handleChange = (name, value) => {
// if value is an empty string, delete the field
if (value === '') {
value = null;
}
// if this is a number field, convert value before sending it up to Form
if (this.getType() === 'number') {
if (this.getType() === 'number' && value != null) {
value = Number(value);
}
this.props.updateCurrentValues({ [this.props.path]: value });
@ -53,16 +67,14 @@ class FormComponent extends Component {
/*
Note: not currently used because when function is debounced
some changes might not register if the user submits form too soon
Updates the state of charsCount and charsRemaining as the users types
*/
handleChangeDebounced = debounce(this.handleChange, 500, { leading: true });
updateCharacterCount = value => {
const characterCount = value ? value.length : 0;
this.setState({
charsRemaining: this.props.max - characterCount,
charsCount: characterCount,
});
};
@ -90,7 +102,15 @@ class FormComponent extends Component {
value = merge({}, documentValue, currentValue);
} else {
// note: value has to default to '' to make component controlled
value = currentValue || documentValue || '';
//value = currentValue || documentValue || '';
// note: the previous line does not work when a checkbox is 'false' or a number is '0'
value = currentValue;
if (typeof value === 'undefined' || value === null) {
value = documentValue;
}
if (typeof value === 'undefined' || value === null) {
value = '';
}
}
// replace empty value, which has not been prefilled, by the default value from the schema
if (isEmptyValue(value)) {
@ -116,8 +136,9 @@ class FormComponent extends Component {
Get errors from Form state through context
*/
getErrors = () => {
const fieldErrors = this.props.errors.filter(error => error.path === this.props.path);
getErrors = (errors) => {
errors = errors || this.props.errors;
const fieldErrors = errors.filter(error => error.path === this.props.path);
return fieldErrors;
};
@ -131,194 +152,49 @@ class FormComponent extends Component {
const p = props || this.props;
const fieldType = p.datatype && p.datatype[0].type;
const autoType =
fieldType === Number ? 'number' : fieldType === Boolean ? 'checkbox' : fieldType === Date ? 'date' : 'text';
fieldType === Number ? 'number' :
fieldType === Boolean ? 'checkbox' :
fieldType === Date ?
'date' :
'text';
return p.input || autoType;
};
renderComponent() {
const {
input,
beforeComponent,
afterComponent,
options,
name,
label,
formType,
/*
note: following properties will be passed as part of `...this.props` in `properties`:
Function passed to form controls to clear their contents (set their value to null)
*/
// throwError,
// updateCurrentValues,
// currentValues,
// addToDeletedValues,
// deletedValues,
// clearFieldErrors,
// currentUser,
} = this.props;
const value = this.getValue();
const errors = this.getErrors();
// these properties are whitelisted so that they can be safely passed to the actual form input
// and avoid https://facebook.github.io/react/warnings/unknown-prop.html warnings
const inputProperties = {
name,
options,
label,
onChange: this.handleChange,
value,
...this.props.inputProperties,
};
// note: we also pass value on props directly
const properties = {
...this.props,
value,
errors, // only get errors for the current field
inputProperties,
};
// if input is a React component, use it
if (typeof input === 'function') {
const InputComponent = input;
return <InputComponent {...properties} />;
} else {
// else pick a predefined component
switch (this.getType()) {
case 'nested':
return <Components.FormNested {...properties} />;
case 'number':
return <Components.FormComponentNumber {...properties} />;
case 'url':
return <Components.FormComponentUrl {...properties} />;
case 'email':
return <Components.FormComponentEmail {...properties} />;
case 'textarea':
return <Components.FormComponentTextarea {...properties} />;
case 'checkbox':
// formsy-react-components expects a boolean value for checkbox
// https://github.com/twisty/formsy-react-components/blob/v0.11.1/src/checkbox.js#L20
properties.inputProperties.value = !!properties.inputProperties.value;
return <Components.FormComponentCheckbox {...properties} />;
case 'checkboxgroup':
// formsy-react-components expects an array value
// https://github.com/twisty/formsy-react-components/blob/v0.11.1/src/checkbox-group.js#L42
if (!Array.isArray(properties.inputProperties.value)) {
properties.inputProperties.value = [properties.inputProperties.value];
}
// in case of checkbox groups, check "checked" option to populate value if this is a "new document" form
const checkedValues = _.where(properties.options, { checked: true }).map(option => option.value);
if (checkedValues.length && !properties.inputProperties.value && formType === 'new') {
properties.inputProperties.value = checkedValues;
}
return <Components.FormComponentCheckboxGroup {...properties} />;
case 'radiogroup':
// TODO: remove this?
// formsy-react-compnents RadioGroup expects an onChange callback
// https://github.com/twisty/formsy-react-components/blob/v0.11.1/src/radio-group.js#L33
// properties.onChange = (name, value) => {
// this.context.updateCurrentValues({ [name]: value });
// };
return <Components.FormComponentRadioGroup {...properties} />;
case 'select':
const noneOption = {
label: this.context.intl.formatMessage({ id: 'forms.select_option' }),
value: '',
disabled: true,
};
properties.inputProperties.options = [noneOption, ...properties.inputProperties.options];
return <Components.FormComponentSelect {...properties} />;
case 'selectmultiple':
properties.inputProperties.multiple = true;
return <Components.FormComponentSelect {...properties} />;
case 'datetime':
return <Components.FormComponentDateTime {...properties} />;
case 'date':
return <Components.FormComponentDate {...properties} />;
case 'time':
return <Components.FormComponentTime {...properties} />;
case 'text':
return <Components.FormComponentDefault {...properties} />;
default:
const CustomComponent = Components[input];
return CustomComponent ? (
<CustomComponent {...properties} />
) : (
<Components.FormComponentDefault {...properties} />
);
}
}
}
showClear = () => {
return ['datetime', 'time', 'select', 'radiogroup'].includes(this.props.input);
};
clearField = e => {
e.preventDefault();
clearField = event => {
event.preventDefault();
event.stopPropagation();
this.props.updateCurrentValues({ [this.props.path]: null });
if (this.showCharsRemaining()) {
this.updateCharacterCount(null);
}
};
renderClear() {
render () {
return (
<a
href="javascript:void(0)"
className="form-component-clear"
title={this.context.intl.formatMessage({ id: 'forms.clear_field' })}
onClick={this.clearField}
>
<span></span>
</a>
<Components.FormComponentInner
{...this.props}
{...this.state}
inputType={this.getType()}
value={this.getValue()}
errors={this.getErrors()}
document={this.context.getDocument()}
showCharsRemaining={!!this.showCharsRemaining()}
onChange={this.handleChange}
clearField={this.clearField}
/>
);
}
render() {
const { beforeComponent, afterComponent, name, input } = this.props;
const hasErrors = this.getErrors() && this.getErrors().length;
const inputName = typeof input === 'function' ? input.name : input;
const inputClass = classNames('form-input', `input-${name}`, `form-component-${inputName || 'default'}`, {
'input-error': hasErrors,
});
return (
<div className={inputClass}>
{beforeComponent ? beforeComponent : null}
{this.renderComponent()}
{hasErrors ? <Components.FieldErrors errors={this.getErrors()} /> : null}
{this.showClear() ? this.renderClear() : null}
{this.showCharsRemaining() && (
<div className={classNames('form-control-limit', { danger: this.state.charsRemaining < 10 })}>
{this.state.charsRemaining}
</div>
)}
{afterComponent ? afterComponent : null}
</div>
);
}
}
FormComponent.propTypes = {
document: PropTypes.object,
name: PropTypes.string,
name: PropTypes.string.isRequired,
label: PropTypes.string,
value: PropTypes.any,
placeholder: PropTypes.string,
@ -326,19 +202,21 @@ FormComponent.propTypes = {
options: PropTypes.any,
input: PropTypes.any,
datatype: PropTypes.any,
path: PropTypes.string,
path: PropTypes.string.isRequired,
disabled: PropTypes.bool,
nestedSchema: PropTypes.object,
currentValues: PropTypes.object,
deletedValues: PropTypes.array,
updateCurrentValues: PropTypes.func,
errors: PropTypes.array,
currentValues: PropTypes.object.isRequired,
deletedValues: PropTypes.array.isRequired,
throwError: PropTypes.func.isRequired,
updateCurrentValues: PropTypes.func.isRequired,
errors: PropTypes.array.isRequired,
addToDeletedValues: PropTypes.func,
clearFieldErrors: PropTypes.func.isRequired,
currentUser: PropTypes.object,
};
FormComponent.contextTypes = {
intl: intlShape,
getDocument: PropTypes.func,
getDocument: PropTypes.func.isRequired,
};
registerComponent('FormComponent', FormComponent);

View file

@ -67,8 +67,16 @@ FormGroup.propTypes = {
name: PropTypes.string,
label: PropTypes.string,
order: PropTypes.number,
fields: PropTypes.array,
updateCurrentValues: PropTypes.func,
fields: PropTypes.array.isRequired,
errors: PropTypes.array.isRequired,
throwError: PropTypes.func.isRequired,
currentValues: PropTypes.object.isRequired,
updateCurrentValues: PropTypes.func.isRequired,
deletedValues: PropTypes.array.isRequired,
addToDeletedValues: PropTypes.func.isRequired,
clearFieldErrors: PropTypes.func.isRequired,
formType: PropTypes.string.isRequired,
currentUser: PropTypes.object,
};
registerComponent('FormGroup', FormGroup);

View file

@ -8,10 +8,15 @@ const FormSubmit = ({
submitLabel,
cancelLabel,
cancelCallback,
revertLabel,
revertCallback,
document,
deleteDocument,
collectionName,
classes,
}, {
isChanged,
clearForm,
}) => (
<div className="form-submit">
<Components.Button type="submit" variant="primary">
@ -30,6 +35,19 @@ const FormSubmit = ({
</a>
) : null}
{revertCallback ? (
<a
className="form-cancel"
onClick={e => {
e.preventDefault();
clearForm({ clearErrors: true, clearCurrentValues: true, clearDeletedValues: true });
revertCallback(document);
}}
>
{revertLabel ? revertLabel : <FormattedMessage id="forms.revert"/>}
</a>
) : null}
{deleteDocument ? (
<div>
<hr />
@ -45,10 +63,18 @@ FormSubmit.propTypes = {
submitLabel: PropTypes.string,
cancelLabel: PropTypes.string,
cancelCallback: PropTypes.func,
revertLabel: PropTypes.string,
revertCallback: PropTypes.func,
document: PropTypes.object,
deleteDocument: PropTypes.func,
collectionName: PropTypes.string,
classes: PropTypes.object,
};
FormSubmit.contextTypes = {
isChanged: PropTypes.func,
clearForm: PropTypes.func,
};
registerComponent('FormSubmit', FormSubmit);

View file

@ -27,6 +27,7 @@ component is also added to wait for withDocument's loading prop to be false)
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n';
import { withRouter } from 'react-router'
import { withApollo, compose } from 'react-apollo';
import {
Components,
@ -265,7 +266,13 @@ FormWrapper.propTypes = {
prefilledProps: PropTypes.object,
layout: PropTypes.string,
fields: PropTypes.arrayOf(PropTypes.string),
hideFields: PropTypes.arrayOf(PropTypes.string),
showRemove: PropTypes.bool,
submitLabel: PropTypes.string,
cancelLabel: PropTypes.string,
revertLabel: PropTypes.string,
repeatErrors: PropTypes.bool,
warnUnsavedChanges: PropTypes.bool,
// callbacks
submitCallback: PropTypes.func,
@ -273,6 +280,7 @@ FormWrapper.propTypes = {
removeSuccessCallback: PropTypes.func,
errorCallback: PropTypes.func,
cancelCallback: PropTypes.func,
revertCallback: PropTypes.func,
currentUser: PropTypes.object,
client: PropTypes.object,
@ -287,4 +295,4 @@ FormWrapper.contextTypes = {
intl: intlShape
}
registerComponent('SmartForm', FormWrapper, withCurrentUser, withApollo);
registerComponent('SmartForm', FormWrapper, withCurrentUser, withApollo, withRouter);

View file

@ -43,6 +43,8 @@ addStrings('en', {
"forms.select_option": "-- select option --",
"forms.delete": "Delete",
"forms.delete_confirm": "Delete document?",
"forms.revert": "Revert",
"forms.confirm_discard": "Discard changes?",
"users.profile": "Profile",
"users.complete_profile": "Complete your Profile",

View file

@ -43,6 +43,8 @@ addStrings('es', {
"forms.select_option": "-- seleccionar opción --",
"forms.delete": "Eliminar",
"forms.delete_confirm": "¿Eliminar documento?",
"forms.revert": "Revert", // TODO: translate
"forms.confirm_discard": "Discard changes?", // TODO: translate
"users.profile": "Perfil",
"users.complete_profile": "Complete su perfil",

View file

@ -45,6 +45,8 @@ addStrings('fr', {
"forms.delete_confirm": "Supprimer le document?",
"forms.next": "Suivant",
"forms.previous": "Précédent",
"forms.revert": "Revert", // TODO: translate
"forms.confirm_discard": "Discard changes?", // TODO: translate
"users.profile": "Profil",
"users.complete_profile": "Complétez votre profil",

View file

@ -1,4 +1,5 @@
import { compose } from 'react-apollo'; // note: at the moment, compose@react-apollo === compose@redux ; see https://github.com/apollostack/react-apollo/blob/master/src/index.ts#L4-L7
import React from 'react';
export const Components = {}; // will be populated on startup (see vulcan:routing)
export const ComponentsTable = {} // storage for infos about components
@ -112,3 +113,27 @@ export const populateComponentsApp = () => {
export const copyHoCs = (sourceComponent, targetComponent) => {
return compose(...sourceComponent.hocs)(targetComponent);
}
/**
* Returns an instance of the given component name of function
* @param {string|function} component A component or registered component name
* @param {Object} [props] Optional properties to pass to the component
*/
//eslint-disable-next-line react/display-name
export const instantiateComponent = (component, props) => {
if (!component) {
return null;
} else if (typeof component === 'string') {
const Component = getComponent(component);
return <Component {...props}/>
} else if (typeof component === 'function' && component.prototype && component.prototype.isReactComponent) {
const Component = component;
return <Component {...props}/>
} else if (typeof component === 'function') {
return component(props);
} else {
return component;
}
};

View file

@ -112,6 +112,15 @@ Utils.scrollPageTo = function(selector) {
$('body').scrollTop($(selector).offset().top);
};
Utils.scrollIntoView = function (selector) {
if (!document) return;
const element = document.querySelector(selector);
if (element) {
element.scrollIntoView();
}
};
Utils.getDateRange = function(pageNumber) {
var now = moment(new Date());
var dayToDisplay=now.subtract(pageNumber-1, 'days');

View file

@ -0,0 +1,208 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'meteor/vulcan:i18n';
import { Components, registerComponent, instantiateComponent } from 'meteor/vulcan:core';
import classNames from 'classnames';
class FormComponentInner extends PureComponent {
renderClear = () => {
if (['datetime', 'time', 'select', 'radiogroup'].includes(this.props.input)) {
return (
<a
href="javascript:void(0)"
className="form-component-clear"
title={this.context.intl.formatMessage({ id: 'forms.clear_field' })}
onClick={this.props.clearField}
>
<span></span>
</a>
);
}
};
getProperties = () => {
const { name, options, label, onChange, value } = this.props;
// these properties are whitelisted so that they can be safely passed to the actual form input
// and avoid https://facebook.github.io/react/warnings/unknown-prop.html warnings
const inputProperties = {
name,
options,
label,
onChange,
value,
...this.props.inputProperties,
};
return {
...this.props,
inputProperties,
};
};
renderExtraComponent = (extraComponent) => {
if (!extraComponent) return null;
const properties = this.getProperties();
return instantiateComponent(extraComponent, properties);
};
renderComponent = () => {
const { input, inputType, formType } = this.props;
const properties = this.getProperties();
// if input is a React component, use it
if (typeof input === 'function') {
const InputComponent = input;
return <InputComponent {...properties} />;
} else {
// else pick a predefined component
switch (inputType) {
case 'nested':
return <Components.FormNested {...properties} />;
case 'number':
return <Components.FormComponentNumber {...properties} />;
case 'url':
return <Components.FormComponentUrl {...properties} />;
case 'email':
return <Components.FormComponentEmail {...properties} />;
case 'textarea':
return <Components.FormComponentTextarea {...properties} />;
case 'checkbox':
// formsy-react-components expects a boolean value for checkbox
// https://github.com/twisty/formsy-react-components/blob/v0.11.1/src/checkbox.js#L20
properties.inputProperties.value = !!properties.inputProperties.value;
return <Components.FormComponentCheckbox {...properties} />;
case 'checkboxgroup':
// formsy-react-components expects an array value
// https://github.com/twisty/formsy-react-components/blob/v0.11.1/src/checkbox-group.js#L42
if (!Array.isArray(properties.inputProperties.value)) {
properties.inputProperties.value = [properties.inputProperties.value];
}
// in case of checkbox groups, check "checked" option to populate value if this is a "new
// document" form
const checkedValues = _.where(properties.options, { checked: true })
.map(option => option.value);
if (checkedValues.length && !properties.inputProperties.value && formType === 'new') {
properties.inputProperties.value = checkedValues;
}
return <Components.FormComponentCheckboxGroup {...properties} />;
case 'radiogroup':
// TODO: remove this?
// formsy-react-compnents RadioGroup expects an onChange callback
// https://github.com/twisty/formsy-react-components/blob/v0.11.1/src/radio-group.js#L33
// properties.onChange = (name, value) => {
// this.context.updateCurrentValues({ [name]: value });
// };
return <Components.FormComponentRadioGroup {...properties} />;
case 'select':
const noneOption = {
label: this.context.intl.formatMessage({ id: 'forms.select_option' }),
value: '',
disabled: true,
};
properties.inputProperties.options = [noneOption, ...properties.inputProperties.options];
return <Components.FormComponentSelect {...properties} />;
case 'selectmultiple':
properties.inputProperties.multiple = true;
return <Components.FormComponentSelect {...properties} />;
case 'datetime':
return <Components.FormComponentDateTime {...properties} />;
case 'date':
return <Components.FormComponentDate {...properties} />;
case 'time':
return <Components.FormComponentTime {...properties} />;
case 'text':
return <Components.FormComponentDefault {...properties} />;
default:
const CustomComponent = Components[input];
return CustomComponent ? (
<CustomComponent {...properties} />
) : (
<Components.FormComponentDefault {...properties} />
);
}
}
};
render () {
const {
inputClassName,
name,
input,
beforeComponent,
afterComponent,
errors,
showCharsRemaining,
charsRemaining,
} = this.props;
const hasErrors = errors && errors.length;
const inputName = typeof input === 'function' ? input.name : input;
const inputClass = classNames('form-input', inputClassName, `input-${name}`,
`form-component-${inputName || 'default'}`, { 'input-error': hasErrors, });
return (
<div className={inputClass}>
{this.renderExtraComponent(beforeComponent)}
{this.renderComponent()}
{hasErrors ? <Components.FieldErrors errors={errors}/> : null}
{this.renderClear()}
{showCharsRemaining &&
<div className={classNames('form-control-limit', { danger: charsRemaining < 10 })}>
{charsRemaining}
</div>
}
{this.renderExtraComponent(afterComponent)}
</div>
);
}
}
FormComponentInner.propTypes = {
inputClassName: PropTypes.string,
name: PropTypes.string.isRequired,
input: PropTypes.any,
beforeComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
afterComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
clearField: PropTypes.func.isRequired,
errors: PropTypes.array.isRequired,
help: PropTypes.node,
onChange: PropTypes.func.isRequired,
showCharsRemaining: PropTypes.bool.isRequired,
charsRemaining: PropTypes.number,
charsCount: PropTypes.number,
charsMax: PropTypes.number,
};
FormComponentInner.contextTypes = {
intl: intlShape,
};
FormComponentInner.displayName = 'FormComponentInner';
registerComponent('FormComponentInner', FormComponentInner);

View file

@ -10,6 +10,7 @@ import '../components/forms/Textarea.jsx';
import '../components/forms/Time.jsx';
import '../components/forms/Date.jsx';
import '../components/forms/Url.jsx';
import '../components/forms/FormComponentInner.jsx';
import '../components/forms/FormControl.jsx'; // note: only used by old accounts package, remove soon?
import '../components/ui/Button.jsx';