modules-pack
Version:
JavaScript Modules for Modern Frontend & Backend Projects
223 lines (198 loc) • 7.66 kB
JavaScript
import { isRequired } from 'react-ui-pack/inputs/validationRules'
import { definitionSetup, get, hasListValue, LANGUAGE_LEVEL, toList } from 'utils-pack'
import { TYPE } from './definitions'
/**
* FIELD DEFINITIONS ===========================================================
* @usage:
* FIELD.FOR = {
* CONTACT: [
* {id: FIELD.ID.EMAIL, required: true},
* {id: FIELD.ID.PHONE},
* ]
* }
*
* @withForm()
* export default class ContactForm extends PureComponent {
* render () {
* return this.renderInput(FIELD.FOR.CONTACT)
* }
* }
* =============================================================================
*/
export const FIELD = definitionSetup('TYPE', 'RENDER', 'ACTION', 'ID', 'DEF', 'MIN_MAX', 'FOR')
// Field Type Definitions
FIELD.TYPE = {
BUTTON: 'Button',
EXPAND: 'Expand', // Expandable/collapsing content with clickable `title` or `renderLabel` area
EXPAND_LIST: 'ExpandList', // list of Expand components with dynamic `renderLabel` and `renderItem` attributes
CHECKBOX: 'Checkbox', // checkbox with optional label
COL: 'Col', // Vertical column layout
COL2: 'Column', // alias for Column
COL3: 'VerticalLayout', // alias for Column
COL_LIST: 'ColList', // alias for List
COL_LIST3: 'VerticalList', // alias for List
COUNTER: 'Counter', // Animated number changing from `start` to `end` values
DROPDOWN: 'Dropdown', // Semantic UI Dropdown
LABEL: 'Label',
LIST: 'List', // list of Col components with dynamic `renderItem` attributes
PIE_CHART: 'PieChart', // Pie chat (can be in shape of donut) with optional legends
PROGRESS_STEPS: 'ProgressSteps', // Progress Steps, with content like Tabs
ROW: 'Row', // Horizontal row layout
ROW2: 'HorizontalLayout', // alias for Row
ROW_LIST: 'RowList', // alias for List with Row layout
ROW_LIST2: 'HorizontalList', // alias for List with Row layout
SPACE: 'Space', // for adding space between items
TABLE: 'Table',
TABS: 'Tabs',
TAB_LIST: 'TabList',
TEXT: 'Text',
TITLE: 'Title', // A customised `Text` view with certain styling for consistent look and feel
TOOLTIP: 'Tooltip', // A hint components that pops up when element is being hovered
// ...to be populated by modules
}
// Value Renderer Definitions
FIELD.RENDER = {
CURRENCY: 'Currency',
PERCENT: 'Percent',
DOUBLE5: 'Double5',
FLOAT: 'Float',
TITLE_n_INPUT: 'Title+Input',
}
// Action Type Definitions
FIELD.ACTION = {
RESET: 'reset',
SET_STATE: 'setState',
FETCH: 'fetch',
POPUP: 'popup',
WARN: 'warn',
}
// Action Methods by Action Type Definitions
FIELD.FUNC = {
[FIELD.ACTION.WARN]: console.warn,
}
// Field IDs for uniquely identifying field definitions
FIELD.ID = {
// Common Inputs
ID: 'id', // use lower case value so it can be used as input.name by default
ID_HIDDEN: 'id_hidden', // input `name` should be set to `id`, defining with underscore to avoid potential conflict
NAME: 'name',
EMAIL: 'email',
ABOUT: 'about',
ADDRESS: 'address',
LANGUAGE: 'language',
PHONE: 'phone',
WEBSITE: 'website',
// ...to be populated by modules
}
// Field Min/Max Value Definitions by ID (used for extending base definitions from FIELD.DEF)
FIELD.MIN_MAX = {
// Common
[FIELD.ID.LANGUAGE]: [LANGUAGE_LEVEL.BASIC._, LANGUAGE_LEVEL.NATIVE._],
// ...to be populated by modules
}
// Field Definitions by ID
FIELD.DEF = {
// ...to be populated by modules (see form/constants for reference)
}
// List of Field IDs with optional base definition overrides
FIELD.FOR = {
// ...to be populated by modules
}
/**
* Construct Field Definitions based on given list of fields, returning new Objects
*
* @example:
* <form onSubmit={handleSubmit(this.submit)} className='max-width-360 margin-h'>
* {fieldsFrom(FIELD.FOR.USER).map(renderField)}
* </form>
*
* @param {Array<Object>} fields - list of fields to create, requires FIELD.ID, used for extending base definition
* @param {Object} [initialValues] - for redux-form
* @returns {Array<Object>} list - of field definitions ready for rendering
*/
export function fieldsFrom (fields, {initialValues: initValues = {}} = {}) {
// Collect definitions
return fields.map(({id, ...field}) => ({id, ...FIELD.DEF[id], ...field}))
// Process prefixes
.map(({name = '', namePrefix = '', options, items, float, required, disabled, readonly, validate, ...field}) => {
name = `${namePrefix}${name}`
const initialValues = name ? get(initValues, name) : initValues
const validations = []
if (required) validations.push(isRequired)
if (validate) validations.push(...toList(validate, 'clean'))
return {
float,
...field,
...initialValues && {initialValues},
...name && {name},
...disabled != null && {disabled},
...readonly != null && {readonly},
...required != null && {required}, // required may be false for nested field inside required group
...validations.length && {validate: validateMultiple(validations)},
...options && {options: options.items || options}, // options need to fallback in case lang already set
...items && { // nested fields in group
items: fieldsFrom(items.map(i => ({
...required && {required},
...disabled && {disabled},
...readonly && {readonly},
...i,
})), {initialValues})
},
}
})
}
/* Convert multiple validation functions into a single function */
function validateMultiple (validations) {
return function (...args) {
for (const validate of validations) {
const error = validate(...args)
if (error) return error
}
}
}
/**
* Get Object of Required Fields mapping by key path to type, based on definition from FIELD.FOR.LIST
* @example:
* requiredFieldsFrom(FIELD.FOR.USER)
* >>> {'name': String, 'phones.work': Number}
*
* @param {Array|Object} fields - FIELD.FOR.LIST
* @returns {Object} required fields - key/value pairs of key path -> value type
*/
export function requiredFieldsFrom (fields) {
let result = {}
fieldsFrom(fields).forEach(field => {
const {name} = field
if (hasListValue(field.items)) {
result = {...result, ...requiredFieldsFrom(field.items)}
} else if (name && field.required) {
if (field.type === 'number') {
return result[name] = Number
} else if (field.defaultValue != null) {
return result[name] = field.defaultValue.constructor
} else if (field.view === FIELD.TYPE.MULTIPLE) {
if (hasListValue(field.options)) {
// Field might be marked required for all, but only one is actually required
// field.options.forEach(option => {
// const nestedName = get(option, 'value', '')
// result[`${name}.${nestedName}`] = nestedName.constructor
// })
// Returning top level object is good enough logic for use with isRequired()
// since the object must be non-empty to pass validation
return result[name] = Object
}
} else if (hasListValue(field.options)) {
return result[name] = get(field.options[0], 'value', '').constructor
} else if (field.view === FIELD.TYPE.TOGGLE) {
return result[name] = Boolean
} else if (field.view === FIELD.TYPE.UPLOAD_GRID) {
return result[name] = Array
} else if (field.view === FIELD.TYPE.UPLOAD) {
return result[name] = Object
}
return result[name] = String
}
}
)
return result
}