ember-frost-bunsen
Version:
Create UI's from JSON configurations.
361 lines (305 loc) • 9.51 kB
JavaScript
import {utils} from 'bunsen-core'
const {
getLabel,
getSubModel
} = utils
import Ember from 'ember'
const {Component, get, isEmpty, typeOf} = Ember
import computed, {readOnly} from 'ember-computed-decorators'
import {HookMixin} from 'ember-hook'
import PropTypeMixin, {PropTypes} from 'ember-prop-types'
import _ from 'lodash'
import layout from 'ember-frost-bunsen/templates/components/frost-bunsen-cell'
import {isCommonAncestor} from 'ember-frost-bunsen/tree-utils'
const {isArray} = Array
/**
* Return path without an index at the end
* i.e. "foo.bar.0" would become "foo.bar"
* @param {String} path - path to remove index from
* @returns {String} path without index
*/
export function removeIndex (path) {
const parts = (path || '').split('.')
const last = parts.length !== 0 ? parts.pop() : ''
return /^\d+$/.test(last) ? parts.join('.') : path || ''
}
/**
* Iterates through each entry in a map
* @param {Iterator} iterator - map iterator
* @param {Function} iteratee - callback that is involed on every etry
*/
export function iterateMap (iterator, iteratee) {
let current
while (true) {
current = iterator.next()
if (current.done || iteratee(current.value) === false) {
break
}
}
}
export default Component.extend(HookMixin, PropTypeMixin, {
// == Component Properties ===================================================
classNameBindings: ['computedClassNames'],
layout,
// == State Properties =======================================================
propTypes: {
bunsenId: PropTypes.string,
bunsenModel: PropTypes.object.isRequired,
bunsenView: PropTypes.object.isRequired,
cellConfig: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
formDisabled: PropTypes.bool,
getRootProps: PropTypes.func,
onChange: PropTypes.func.isRequired,
onError: PropTypes.func.isRequired,
readOnly: PropTypes.bool,
registerForFormValueChanges: PropTypes.func,
renderers: PropTypes.oneOfType([
PropTypes.EmberObject,
PropTypes.object
]),
showAllErrors: PropTypes.bool,
unregisterForFormValueChanges: PropTypes.func,
value: PropTypes.object
},
getDefaultProps () {
return {
readOnly: false,
propagatedValue: {},
propagatedValueChangeSet: null
}
},
/* eslint-disable complexity */
didReceiveAttrs () {
const valueChangeSet = this.get('valueChangeSet')
const oldCellConfig = this.get('_oldCellConfig')
const newCellConfig = this.get('cellConfig')
const oldValueChangeSet = this.get('_oldValueChangeSet')
let isDirty = false
if (valueChangeSet && oldValueChangeSet !== valueChangeSet) {
iterateMap(valueChangeSet.keys(), (bunsenId) => {
if (isCommonAncestor(newCellConfig.__dependency__, bunsenId)) {
isDirty = true
return false
}
})
}
if (isDirty || !_.isEqual(oldCellConfig, newCellConfig)) {
this.setProperties({
'propagatedValue': this.get('value'),
'propagatedValueChangeSet': this.get('valueChangeSet')
})
}
this.setProperties({
'_oldCellConfig': newCellConfig,
'_oldValueChangeSet': valueChangeSet
})
},
/* eslint-enable complexity */
// == Computed Properties ====================================================
children (bunsenModel, cellDefinitions, cellConfig, renderId) {
let children
if (cellConfig.extends) {
const extendedDef = cellDefinitions[cellConfig.extends]
children = extendedDef.children || cellConfig.children
} else {
children = cellConfig.children
}
if (!isArray(children)) return null
return children
.map((child) => {
let subModel = bunsenModel
let subId = renderId
if (child.model) {
subModel = getSubModel(bunsenModel, child.model, child.dependsOn)
subId = subId ? `${subId}.${child.model}` : child.model
}
return {
cellConfig: child,
bunsenModel: subModel,
bunsenId: subId,
cellKey: child.__cellKey__
}
})
.filter((child) => child.bunsenModel !== undefined)
},
renderValue (value, cellConfig) {
const bunsenId = this.get('renderId')
if (typeOf(value) !== 'object') {
return undefined
}
return get(value, bunsenId)
},
errorMessage (errors) {
const bunsenId = this.get('renderId')
if (typeOf(errors) !== 'object' || isEmpty(errors[bunsenId])) {
return null
}
return Ember.String.htmlSafe(errors[bunsenId].join('<br>'))
},
/**
* Get bunsen ID for cell's input
* @param {String} bunsenId - bunsen ID
* @param {String} model - bunsen model path
* @returns {String} bunsen ID of input
*/
renderId (bunsenId, model) {
return bunsenId || model || ''
},
/**
* Get bunsen ID for array
* @param {String} renderId - render identifier
* @returns {Number} bunsen ID for array
*/
nonIndexId (renderId) {
return removeIndex(renderId)
},
/**
* Get index for single array item
* @param {String} renderId - render identifier
* @returns {Number} index
*/
index (renderId) {
return parseInt(renderId.split('.').pop(), 10)
},
/**
* Detemrine if we are rendering a single array item
* @param {Number|NaN} index - index of array item or NaN
* @returns {Boolean} whether or not we are rendering a single array item
*/
isArrayItem (index) {
return !isNaN(index)
},
/**
* Determine if sub model is of type "array"
* @param {String} renderer - custom renderer
* @param {String} type - type of sub model
* @returns {Boolean} whether or not sub model is "array"
*/
isSubModelArray (renderer, type) {
return type === 'array' && !renderer
},
/**
* Determine if sub model is of type "object"
* @param {String} renderer - custom renderer
* @param {String} type - type of sub model
* @returns {Boolean} whether or not sub model is "object"
*/
isSubModelObject (renderer, type) {
return type === 'object' && !renderer
},
/**
* Whether or not input's dependency is met
* @param {BunsenCell} cellConfig - cell configuration for input
* @param {String} bunsenId - bunsen ID for input
* @param {Object} value - current value of form
* @returns {Boolean} whether or not dependency is met
*/
isDependencyMet (cellConfig, bunsenId, value) {
if (!cellConfig || cellConfig.dependsOn === undefined) {
return null
}
const dependencyId = bunsenId.replace(cellConfig.model, cellConfig.dependsOn)
const dependencyValue = get(value || {}, dependencyId)
return dependencyValue !== undefined
},
clearable (cellConfig) {
return cellConfig.clearable || false
},
compact (cellConfig) {
return get(cellConfig, 'arrayOptions.compact') === true
},
/* eslint-disable complexity */
showSection (cellConfig, type) {
const isArrayCell = type === 'array' && cellConfig.renderer === undefined
return (
cellConfig.collapsible ||
(cellConfig.label && cellConfig.children) ||
(isArrayCell && !cellConfig.hideLabel)
)
},
/* eslint-enable complexity */
isLeafNode (model, children) {
return !children
},
renderLabel (cellConfig, nonIndexId, bunsenModel) {
if (cellConfig.hideLabel) {
return null
}
let label = get(cellConfig, 'label')
if (label) {
label = label.charAt(0).toUpperCase() + label.slice(1)
}
return getLabel(label, bunsenModel, nonIndexId)
},
computedClassNames (cellConfig) {
const viewDefinedClass = get(cellConfig, 'classNames.cell')
const classes = this.get('classNames').toString().split(' ')
classes.push('frost-bunsen-cell')
if (this.get('compact')) {
classes.push('frost-bunsen-compact')
}
if (viewDefinedClass) {
classes.push(viewDefinedClass)
}
return classes.join(' ').trim()
},
// == Functions ==============================================================
_clearChildren (cell) {
if (!cell.children) {
return
}
cell.children
.forEach((child) => {
if (child.model) {
this.onChange(child.model, null)
return
}
this._clearChildren(child)
})
},
// == Actions ================================================================
actions: {
clear () {
const bunsenId = this.get('bunsenId')
if (bunsenId) {
this.onChange(bunsenId, null)
} else {
const cell = this.get('cellConfig')
this._clearChildren(cell)
}
}
}
})