wux-weapp
Version:
一套组件化、可复用、易扩展的微信小程序 UI 组件库
494 lines (442 loc) • 13.5 kB
JavaScript
import warning from '../libs/warning'
import { set } from '../shared/util'
export function isNullValue(value) {
return value === null || value === undefined
}
/**
* fix: setting data field "xxx" to undefined is invalid.
*
* @param {*} value
* @returns
*/
export function fixNullValue(value) {
return isNullValue(value) ? null : value
}
export function flattenArray(arr) {
return Array.prototype.concat.apply([], arr)
}
export function treeTraverse(path = '', tree, isLeafNode, errorMessage, callback) {
if (isLeafNode(path, tree)) {
callback(path, tree)
} else if (tree === undefined || tree === null) {
// Do nothing
} else if (Array.isArray(tree)) {
tree.forEach((subTree, index) => treeTraverse(
`${path}[${index}]`,
subTree,
isLeafNode,
errorMessage,
callback
))
} else { // It's object and not a leaf node
if (typeof tree !== 'object') {
warning(false, errorMessage)
return
}
Object.keys(tree).forEach(subTreeKey => {
const subTree = tree[subTreeKey]
treeTraverse(
`${path}${path ? '.' : ''}${subTreeKey}`,
subTree,
isLeafNode,
errorMessage,
callback
)
})
}
}
export function flattenFields(maybeNestedFields, isLeafNode, errorMessage) {
const fields = {}
treeTraverse(undefined, maybeNestedFields, isLeafNode, errorMessage, (path, node) => {
fields[path] = node
})
return fields
}
export function normalizeValidateRules(validate, rules, validateTrigger) {
const validateRules = validate.map((item) => {
const newItem = {
...item,
trigger: item.trigger || [],
}
if (typeof newItem.trigger === 'string') {
newItem.trigger = [newItem.trigger]
}
return newItem
})
if (rules) {
validateRules.push({
trigger: validateTrigger ? [].concat(validateTrigger) : [],
rules,
})
}
return validateRules
}
export function getValidateTriggers(validateRules) {
return validateRules
.filter(item => !!item.rules && item.rules.length)
.map(item => item.trigger)
.reduce((pre, curr) => pre.concat(curr), [])
}
export function getParams(ns, opt, cb) {
let names = ns
let options = opt
let callback = cb
if (cb === undefined) {
if (typeof names === 'function') {
callback = names
options = {}
names = undefined
} else if (Array.isArray(names)) {
if (typeof options === 'function') {
callback = options
options = {}
} else {
options = options || {}
}
} else {
callback = options
options = names || {}
names = undefined
}
}
return {
names,
options,
callback,
}
}
export function isEmptyObject(obj) {
return Object.keys(obj).length === 0
}
export function hasRules(validate) {
if (validate) {
return validate.some((item) => {
return item.rules && item.rules.length
})
}
return false
}
export function getErrorStrs(errors) {
if (errors) {
return errors.map((e) => {
if (e && e.message) {
return e.message
}
return e
})
}
return errors
}
export function startsWith(str, prefix) {
return str.lastIndexOf(prefix, 0) === 0
}
export function getValueFromEvent(e) {
const getValue = (target) =>
target.type === 'checkbox' ? target.checked : target.value
// To support custom element
if (!e || !e.detail) {
if (!!e) {
return getValue(e)
}
return e
}
return getValue(e.detail)
}
/**
* ## 转换校验规则,使之支持 async-validator
* 注意:由于小程序 data 不支持传入 RegExp & function,所以在设置 rules 需要传入字符串映射!!!
*
* @example
* ```
* // 手机号验证
* rules="{{ [{ required: true, pattern: '1[3456789]\d{9}' }] }}"
* ```
* @example
* ```
* // 自定义验证
* rules="{{ [{ required: true, validator: 'checkMobile' }] }}"
*
* Page({
* checkMobile(rule, value) {
* if (value) {
* return Promise.resolve()
* }
* return Promise.reject(new Error('手机号不能为空!'))
* }
* })
* ```
* @example
* ```
* // 校验前去除空格
* rules="{{ [{ required: true, transform: 'trim' }] }}"
*
* Page({
* trim(value) {
* return value.trim()
* }
* })
* ```
* @export
* @param {array} rules 校验规则,设置字段的校验逻辑
* @param {object} vm 当前页面的实例对象
* @return {array}
*/
export function transformRules(rules, vm = getCurrentPages()[getCurrentPages().length - 1]) {
return rules.map((rule) => {
const cloneRule = { ...rule }
// 正则表达式校验
if (typeof rule.pattern === 'string') {
cloneRule.pattern = new RegExp(rule.pattern)
}
// 校验前转换字段值
if (typeof rule.transform === 'string' && vm && vm[rule.transform]) {
cloneRule.transform = vm[rule.transform]
}
// 自定义校验,接收 Promise 作为返回值。
if (typeof rule.validator === 'string' && vm && vm[rule.validator]) {
cloneRule.validator = vm[rule.validator]
}
return cloneRule
})
}
/**
* ## Replace with template.
*
* @example
* ```
* `I'm ${name}` + { name: 'bamboo' } = I'm bamboo
* ```
* @export
* @param {*} template
* @param {*} kv
* @return {*}
*/
export function replaceMessage(template, kv) {
return template.replace(/\$\{\w+\}/g, (str) => {
const key = str.slice(2, -1);
return kv[key];
});
}
class Field {
constructor(fields) {
Object.assign(this, fields);
}
}
export function isFormField(obj) {
return obj instanceof Field;
}
export function createFormField(field) {
if (isFormField(field)) {
return field;
}
return new Field(field);
}
export function internalFlattenFields(fields) {
return flattenFields(
fields,
(_, node) => isFormField(node),
'You must wrap field data with `createFormField`.'
)
}
class FieldsStore {
constructor(fields = {}) {
this.fields = internalFlattenFields(fields)
this.fieldsMeta = {}
}
updateFields(fields) {
this.fields = internalFlattenFields(fields)
}
flattenRegisteredFields(fields) {
const validFieldsName = this.getAllFieldsName();
return flattenFields(
fields,
path => validFieldsName.indexOf(path) >= 0,
'You cannot set a form field before rendering a field associated with the value.'
)
}
setFieldsInitialValue = (initialValues) => {
const flattenedInitialValues = this.flattenRegisteredFields(initialValues)
const fieldsMeta = this.fieldsMeta
Object.keys(flattenedInitialValues).forEach(name => {
if (fieldsMeta[name]) {
const fieldMeta = this.getFieldMeta(name)
const initValue = fieldMeta.initialValue
if (isNullValue(initValue)) {
this.setFieldMeta(name, {
...fieldMeta,
initialValue: fixNullValue(flattenedInitialValues[name]),
});
}
}
})
}
setFields(fields) {
const nowFields = {
...this.fields,
...fields,
}
this.fields = nowFields
}
resetFields(ns) {
const { fieldsMeta } = this
const names = ns ?
this.getValidFieldsFullName(ns) :
this.getAllFieldsName()
return names.reduce((acc, name) => {
const fieldMeta = fieldsMeta[name]
if (fieldMeta) {
acc[name] = { value: fixNullValue(fieldMeta.initialValue) }
}
return acc
}, {})
}
setFieldMeta(name, meta) {
this.fieldsMeta[name] = meta
}
setFieldsAsDirty() {
Object.keys(this.fields).forEach((name) => {
const field = this.fields[name]
const fieldMeta = this.fieldsMeta[name]
if (field && fieldMeta && hasRules(fieldMeta.validate)) {
this.fields[name] = {
...field,
dirty: true,
}
}
})
}
getFieldMeta(name) {
this.fieldsMeta[name] = this.fieldsMeta[name] || {}
return this.fieldsMeta[name]
}
getValueFromFields(name, fields) {
const field = fields[name]
if (field && 'value' in field) {
return fixNullValue(field.value)
}
const fieldMeta = this.getFieldMeta(name)
return fieldMeta && fixNullValue(fieldMeta.initialValue)
}
getAllValues = () => {
const { fieldsMeta, fields } = this
return Object.keys(fieldsMeta)
.reduce((acc, name) => set(acc, name, this.getValueFromFields(name, fields)), {})
}
getValidFieldsName() {
const { fieldsMeta } = this
return fieldsMeta ?
Object.keys(fieldsMeta).filter(name => !this.getFieldMeta(name).hidden) :
[]
}
getAllFieldsName() {
const { fieldsMeta } = this
return fieldsMeta ? Object.keys(fieldsMeta) : []
}
getValidFieldsFullName(maybePartialName) {
const maybePartialNames = Array.isArray(maybePartialName) ?
maybePartialName : [maybePartialName]
return this.getValidFieldsName()
.filter(fullName => maybePartialNames.some(partialName => (
fullName === partialName || (
startsWith(fullName, partialName) &&
['.', '['].indexOf(fullName[partialName.length]) >= 0
)
)))
}
getFieldValuePropValue(fieldMeta) {
const { name, valuePropName } = fieldMeta
const field = this.getField(name)
const fieldValue = 'value' in field ? field.value : fieldMeta.initialValue
return {
[valuePropName]: fixNullValue(fieldValue),
}
}
getField(name) {
return {
...this.fields[name],
name,
}
}
getNotCollectedFields() {
const fieldsName = this.getValidFieldsName();
return fieldsName
.filter(name => !this.fields[name])
.map(name => ({
name,
dirty: false,
value: this.getFieldMeta(name).initialValue,
}))
.reduce((acc, field) => set(acc, field.name, createFormField(field)), {});
}
getNestedAllFields() {
return Object.keys(this.fields)
.reduce(
(acc, name) => set(acc, name, createFormField(this.fields[name])),
this.getNotCollectedFields()
);
}
getFieldMember(name, member) {
return this.getField(name)[member]
}
getNestedFields(names, getter) {
const fields = names || this.getValidFieldsName()
return fields.reduce((acc, f) => set(acc, f, getter(f)), {})
}
getNestedField(name, getter) {
const fullNames = this.getValidFieldsFullName(name)
if (
fullNames.length === 0 || // Not registered
(fullNames.length === 1 && fullNames[0] === name) // Name already is full name.
) {
return getter(name)
}
const isArrayValue = fullNames[0][name.length] === '['
const suffixNameStartIndex = isArrayValue ? name.length : name.length + 1
return fullNames
.reduce(
(acc, fullName) => set(
acc,
fullName.slice(suffixNameStartIndex),
getter(fullName)
),
isArrayValue ? [] : {}
)
}
getFieldsValue = (names) => {
return this.getNestedFields(names, this.getFieldValue)
}
getFieldValue = (name) => {
const { fields } = this
return this.getNestedField(name, (fullName) => this.getValueFromFields(fullName, fields))
}
getFieldsError = (names) => {
return this.getNestedFields(names, this.getFieldError)
}
getFieldError = (name) => {
return this.getNestedField(
name,
(fullName) => getErrorStrs(this.getFieldMember(fullName, 'errors'))
)
}
isFieldValidating = (name) => {
return this.getFieldMember(name, 'validating')
}
isFieldsValidating = (ns) => {
const names = ns || this.getValidFieldsName()
return names.some((n) => this.isFieldValidating(n))
}
isFieldTouched = (name) => {
return this.getFieldMember(name, 'touched')
}
isFieldsTouched = (ns) => {
const names = ns || this.getValidFieldsName()
return names.some((n) => this.isFieldTouched(n))
}
clearField(name) {
delete this.fields[name]
delete this.fieldsMeta[name]
}
}
export default function createFieldsStore(fields) {
return new FieldsStore(fields)
}