useFieldForm.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. import warning from '../libs/warning'
  2. import { set } from '../shared/util'
  3. export function isNullValue(value) {
  4. return value === null || value === undefined
  5. }
  6. /**
  7. * fix: setting data field "xxx" to undefined is invalid.
  8. *
  9. * @param {*} value
  10. * @returns
  11. */
  12. export function fixNullValue(value) {
  13. return isNullValue(value) ? null : value
  14. }
  15. export function flattenArray(arr) {
  16. return Array.prototype.concat.apply([], arr)
  17. }
  18. export function treeTraverse(path = '', tree, isLeafNode, errorMessage, callback) {
  19. if (isLeafNode(path, tree)) {
  20. callback(path, tree)
  21. } else if (tree === undefined || tree === null) {
  22. // Do nothing
  23. } else if (Array.isArray(tree)) {
  24. tree.forEach((subTree, index) => treeTraverse(
  25. `${path}[${index}]`,
  26. subTree,
  27. isLeafNode,
  28. errorMessage,
  29. callback
  30. ))
  31. } else { // It's object and not a leaf node
  32. if (typeof tree !== 'object') {
  33. warning(false, errorMessage)
  34. return
  35. }
  36. Object.keys(tree).forEach(subTreeKey => {
  37. const subTree = tree[subTreeKey]
  38. treeTraverse(
  39. `${path}${path ? '.' : ''}${subTreeKey}`,
  40. subTree,
  41. isLeafNode,
  42. errorMessage,
  43. callback
  44. )
  45. })
  46. }
  47. }
  48. export function flattenFields(maybeNestedFields, isLeafNode, errorMessage) {
  49. const fields = {}
  50. treeTraverse(undefined, maybeNestedFields, isLeafNode, errorMessage, (path, node) => {
  51. fields[path] = node
  52. })
  53. return fields
  54. }
  55. export function normalizeValidateRules(validate, rules, validateTrigger) {
  56. const validateRules = validate.map((item) => {
  57. const newItem = {
  58. ...item,
  59. trigger: item.trigger || [],
  60. }
  61. if (typeof newItem.trigger === 'string') {
  62. newItem.trigger = [newItem.trigger]
  63. }
  64. return newItem
  65. })
  66. if (rules) {
  67. validateRules.push({
  68. trigger: validateTrigger ? [].concat(validateTrigger) : [],
  69. rules,
  70. })
  71. }
  72. return validateRules
  73. }
  74. export function getValidateTriggers(validateRules) {
  75. return validateRules
  76. .filter(item => !!item.rules && item.rules.length)
  77. .map(item => item.trigger)
  78. .reduce((pre, curr) => pre.concat(curr), [])
  79. }
  80. export function getParams(ns, opt, cb) {
  81. let names = ns
  82. let options = opt
  83. let callback = cb
  84. if (cb === undefined) {
  85. if (typeof names === 'function') {
  86. callback = names
  87. options = {}
  88. names = undefined
  89. } else if (Array.isArray(names)) {
  90. if (typeof options === 'function') {
  91. callback = options
  92. options = {}
  93. } else {
  94. options = options || {}
  95. }
  96. } else {
  97. callback = options
  98. options = names || {}
  99. names = undefined
  100. }
  101. }
  102. return {
  103. names,
  104. options,
  105. callback,
  106. }
  107. }
  108. export function isEmptyObject(obj) {
  109. return Object.keys(obj).length === 0
  110. }
  111. export function hasRules(validate) {
  112. if (validate) {
  113. return validate.some((item) => {
  114. return item.rules && item.rules.length
  115. })
  116. }
  117. return false
  118. }
  119. export function getErrorStrs(errors) {
  120. if (errors) {
  121. return errors.map((e) => {
  122. if (e && e.message) {
  123. return e.message
  124. }
  125. return e
  126. })
  127. }
  128. return errors
  129. }
  130. export function startsWith(str, prefix) {
  131. return str.lastIndexOf(prefix, 0) === 0
  132. }
  133. export function getValueFromEvent(e) {
  134. const getValue = (target) =>
  135. target.type === 'checkbox' ? target.checked : target.value
  136. // To support custom element
  137. if (!e || !e.detail) {
  138. if (!!e) {
  139. return getValue(e)
  140. }
  141. return e
  142. }
  143. return getValue(e.detail)
  144. }
  145. /**
  146. * ## 转换校验规则,使之支持 async-validator
  147. * 注意:由于小程序 data 不支持传入 RegExp & function,所以在设置 rules 需要传入字符串映射!!!
  148. *
  149. * @example
  150. * ```
  151. * // 手机号验证
  152. * rules="{{ [{ required: true, pattern: '1[3456789]\d{9}' }] }}"
  153. * ```
  154. * @example
  155. * ```
  156. * // 自定义验证
  157. * rules="{{ [{ required: true, validator: 'checkMobile' }] }}"
  158. *
  159. * Page({
  160. * checkMobile(rule, value) {
  161. * if (value) {
  162. * return Promise.resolve()
  163. * }
  164. * return Promise.reject(new Error('手机号不能为空!'))
  165. * }
  166. * })
  167. * ```
  168. * @example
  169. * ```
  170. * // 校验前去除空格
  171. * rules="{{ [{ required: true, transform: 'trim' }] }}"
  172. *
  173. * Page({
  174. * trim(value) {
  175. * return value.trim()
  176. * }
  177. * })
  178. * ```
  179. * @export
  180. * @param {array} rules 校验规则,设置字段的校验逻辑
  181. * @param {object} vm 当前页面的实例对象
  182. * @return {array}
  183. */
  184. export function transformRules(rules, vm = getCurrentPages()[getCurrentPages().length - 1]) {
  185. return rules.map((rule) => {
  186. const cloneRule = { ...rule }
  187. // 正则表达式校验
  188. if (typeof rule.pattern === 'string') {
  189. cloneRule.pattern = new RegExp(rule.pattern)
  190. }
  191. // 校验前转换字段值
  192. if (typeof rule.transform === 'string' && vm && vm[rule.transform]) {
  193. cloneRule.transform = vm[rule.transform]
  194. }
  195. // 自定义校验,接收 Promise 作为返回值。
  196. if (typeof rule.validator === 'string' && vm && vm[rule.validator]) {
  197. cloneRule.validator = vm[rule.validator]
  198. }
  199. return cloneRule
  200. })
  201. }
  202. /**
  203. * ## Replace with template.
  204. *
  205. * @example
  206. * ```
  207. * `I'm ${name}` + { name: 'bamboo' } = I'm bamboo
  208. * ```
  209. * @export
  210. * @param {*} template
  211. * @param {*} kv
  212. * @return {*}
  213. */
  214. export function replaceMessage(template, kv) {
  215. return template.replace(/\$\{\w+\}/g, (str) => {
  216. const key = str.slice(2, -1);
  217. return kv[key];
  218. });
  219. }
  220. class Field {
  221. constructor(fields) {
  222. Object.assign(this, fields);
  223. }
  224. }
  225. export function isFormField(obj) {
  226. return obj instanceof Field;
  227. }
  228. export function createFormField(field) {
  229. if (isFormField(field)) {
  230. return field;
  231. }
  232. return new Field(field);
  233. }
  234. export function internalFlattenFields(fields) {
  235. return flattenFields(
  236. fields,
  237. (_, node) => isFormField(node),
  238. 'You must wrap field data with `createFormField`.'
  239. )
  240. }
  241. class FieldsStore {
  242. constructor(fields = {}) {
  243. this.fields = internalFlattenFields(fields)
  244. this.fieldsMeta = {}
  245. }
  246. updateFields(fields) {
  247. this.fields = internalFlattenFields(fields)
  248. }
  249. flattenRegisteredFields(fields) {
  250. const validFieldsName = this.getAllFieldsName();
  251. return flattenFields(
  252. fields,
  253. path => validFieldsName.indexOf(path) >= 0,
  254. 'You cannot set a form field before rendering a field associated with the value.'
  255. )
  256. }
  257. setFieldsInitialValue = (initialValues) => {
  258. const flattenedInitialValues = this.flattenRegisteredFields(initialValues)
  259. const fieldsMeta = this.fieldsMeta
  260. Object.keys(flattenedInitialValues).forEach(name => {
  261. if (fieldsMeta[name]) {
  262. const fieldMeta = this.getFieldMeta(name)
  263. const initValue = fieldMeta.initialValue
  264. if (isNullValue(initValue)) {
  265. this.setFieldMeta(name, {
  266. ...fieldMeta,
  267. initialValue: fixNullValue(flattenedInitialValues[name]),
  268. });
  269. }
  270. }
  271. })
  272. }
  273. setFields(fields) {
  274. const nowFields = {
  275. ...this.fields,
  276. ...fields,
  277. }
  278. this.fields = nowFields
  279. }
  280. resetFields(ns) {
  281. const { fieldsMeta } = this
  282. const names = ns ?
  283. this.getValidFieldsFullName(ns) :
  284. this.getAllFieldsName()
  285. return names.reduce((acc, name) => {
  286. const fieldMeta = fieldsMeta[name]
  287. if (fieldMeta) {
  288. acc[name] = { value: fixNullValue(fieldMeta.initialValue) }
  289. }
  290. return acc
  291. }, {})
  292. }
  293. setFieldMeta(name, meta) {
  294. this.fieldsMeta[name] = meta
  295. }
  296. setFieldsAsDirty() {
  297. Object.keys(this.fields).forEach((name) => {
  298. const field = this.fields[name]
  299. const fieldMeta = this.fieldsMeta[name]
  300. if (field && fieldMeta && hasRules(fieldMeta.validate)) {
  301. this.fields[name] = {
  302. ...field,
  303. dirty: true,
  304. }
  305. }
  306. })
  307. }
  308. getFieldMeta(name) {
  309. this.fieldsMeta[name] = this.fieldsMeta[name] || {}
  310. return this.fieldsMeta[name]
  311. }
  312. getValueFromFields(name, fields) {
  313. const field = fields[name]
  314. if (field && 'value' in field) {
  315. return fixNullValue(field.value)
  316. }
  317. const fieldMeta = this.getFieldMeta(name)
  318. return fieldMeta && fixNullValue(fieldMeta.initialValue)
  319. }
  320. getAllValues = () => {
  321. const { fieldsMeta, fields } = this
  322. return Object.keys(fieldsMeta)
  323. .reduce((acc, name) => set(acc, name, this.getValueFromFields(name, fields)), {})
  324. }
  325. getValidFieldsName() {
  326. const { fieldsMeta } = this
  327. return fieldsMeta ?
  328. Object.keys(fieldsMeta).filter(name => !this.getFieldMeta(name).hidden) :
  329. []
  330. }
  331. getAllFieldsName() {
  332. const { fieldsMeta } = this
  333. return fieldsMeta ? Object.keys(fieldsMeta) : []
  334. }
  335. getValidFieldsFullName(maybePartialName) {
  336. const maybePartialNames = Array.isArray(maybePartialName) ?
  337. maybePartialName : [maybePartialName]
  338. return this.getValidFieldsName()
  339. .filter(fullName => maybePartialNames.some(partialName => (
  340. fullName === partialName || (
  341. startsWith(fullName, partialName) &&
  342. ['.', '['].indexOf(fullName[partialName.length]) >= 0
  343. )
  344. )))
  345. }
  346. getFieldValuePropValue(fieldMeta) {
  347. const { name, valuePropName } = fieldMeta
  348. const field = this.getField(name)
  349. const fieldValue = 'value' in field ? field.value : fieldMeta.initialValue
  350. return {
  351. [valuePropName]: fixNullValue(fieldValue),
  352. }
  353. }
  354. getField(name) {
  355. return {
  356. ...this.fields[name],
  357. name,
  358. }
  359. }
  360. getNotCollectedFields() {
  361. const fieldsName = this.getValidFieldsName();
  362. return fieldsName
  363. .filter(name => !this.fields[name])
  364. .map(name => ({
  365. name,
  366. dirty: false,
  367. value: this.getFieldMeta(name).initialValue,
  368. }))
  369. .reduce((acc, field) => set(acc, field.name, createFormField(field)), {});
  370. }
  371. getNestedAllFields() {
  372. return Object.keys(this.fields)
  373. .reduce(
  374. (acc, name) => set(acc, name, createFormField(this.fields[name])),
  375. this.getNotCollectedFields()
  376. );
  377. }
  378. getFieldMember(name, member) {
  379. return this.getField(name)[member]
  380. }
  381. getNestedFields(names, getter) {
  382. const fields = names || this.getValidFieldsName()
  383. return fields.reduce((acc, f) => set(acc, f, getter(f)), {})
  384. }
  385. getNestedField(name, getter) {
  386. const fullNames = this.getValidFieldsFullName(name)
  387. if (
  388. fullNames.length === 0 || // Not registered
  389. (fullNames.length === 1 && fullNames[0] === name) // Name already is full name.
  390. ) {
  391. return getter(name)
  392. }
  393. const isArrayValue = fullNames[0][name.length] === '['
  394. const suffixNameStartIndex = isArrayValue ? name.length : name.length + 1
  395. return fullNames
  396. .reduce(
  397. (acc, fullName) => set(
  398. acc,
  399. fullName.slice(suffixNameStartIndex),
  400. getter(fullName)
  401. ),
  402. isArrayValue ? [] : {}
  403. )
  404. }
  405. getFieldsValue = (names) => {
  406. return this.getNestedFields(names, this.getFieldValue)
  407. }
  408. getFieldValue = (name) => {
  409. const { fields } = this
  410. return this.getNestedField(name, (fullName) => this.getValueFromFields(fullName, fields))
  411. }
  412. getFieldsError = (names) => {
  413. return this.getNestedFields(names, this.getFieldError)
  414. }
  415. getFieldError = (name) => {
  416. return this.getNestedField(
  417. name,
  418. (fullName) => getErrorStrs(this.getFieldMember(fullName, 'errors'))
  419. )
  420. }
  421. isFieldValidating = (name) => {
  422. return this.getFieldMember(name, 'validating')
  423. }
  424. isFieldsValidating = (ns) => {
  425. const names = ns || this.getValidFieldsName()
  426. return names.some((n) => this.isFieldValidating(n))
  427. }
  428. isFieldTouched = (name) => {
  429. return this.getFieldMember(name, 'touched')
  430. }
  431. isFieldsTouched = (ns) => {
  432. const names = ns || this.getValidFieldsName()
  433. return names.some((n) => this.isFieldTouched(n))
  434. }
  435. clearField(name) {
  436. delete this.fields[name]
  437. delete this.fieldsMeta[name]
  438. }
  439. }
  440. export default function createFieldsStore(fields) {
  441. return new FieldsStore(fields)
  442. }