form.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. <template>
  2. <form ref="form" class="cube-form" :class="formClass" :action="action" @submit="submitHandler" @reset="resetHandler">
  3. <slot>
  4. <cube-form-group v-for="(group, index) in groups" :fields="group.fields" :legend="group.legend" :key="index" />
  5. </slot>
  6. </form>
  7. </template>
  8. <script>
  9. import { dispatchEvent } from '../../common/helpers/dom'
  10. import { cb2PromiseWithResolve } from '../../common/helpers/util'
  11. import CubeFormGroup from './form-group.vue'
  12. import LAYOUTS from './layouts'
  13. import mixin from './mixin'
  14. const COMPONENT_NAME = 'cube-form'
  15. const EVENT_SUBMIT = 'submit'
  16. const EVENT_RESET = 'reset'
  17. const EVENT_VALIDATE = 'validate'
  18. const EVENT_VALID = 'valid'
  19. const EVENT_INVALID = 'invalid'
  20. export default {
  21. name: COMPONENT_NAME,
  22. mixins: [mixin],
  23. props: {
  24. action: String,
  25. model: {
  26. type: Object,
  27. default() {
  28. /* istanbul ignore next */
  29. return {}
  30. }
  31. },
  32. schema: {
  33. type: Object,
  34. default() {
  35. /* istanbul ignore next */
  36. return {}
  37. }
  38. },
  39. options: {
  40. type: Object,
  41. default() {
  42. return {
  43. scrollToInvalidField: false,
  44. layout: LAYOUTS.STANDARD
  45. }
  46. }
  47. },
  48. immediateValidate: {
  49. type: Boolean,
  50. default: false
  51. }
  52. },
  53. data() {
  54. return {
  55. validatedCount: 0,
  56. dirty: false,
  57. firstInvalidField: null,
  58. firstInvalidFieldIndex: -1
  59. }
  60. },
  61. computed: {
  62. groups() {
  63. const schema = this.schema
  64. const groups = schema.groups || []
  65. if (schema.fields) {
  66. groups.unshift({
  67. fields: schema.fields
  68. })
  69. }
  70. return groups
  71. },
  72. layout() {
  73. const options = this.options
  74. const layout = (options && options.layout) || LAYOUTS.STANDARD
  75. return layout
  76. },
  77. formClass() {
  78. const invalid = this.invalid
  79. const valid = this.valid
  80. const layout = this.layout
  81. return {
  82. 'cube-form_standard': layout === LAYOUTS.STANDARD,
  83. 'cube-form_groups': this.groups.length > 1,
  84. 'cube-form_validating': this.validating,
  85. 'cube-form_pending': this.pending,
  86. 'cube-form_valid': valid,
  87. 'cube-form_invalid': invalid,
  88. 'cube-form_classic': layout === LAYOUTS.CLASSIC,
  89. 'cube-form_fresh': layout === LAYOUTS.FRESH
  90. }
  91. }
  92. },
  93. watch: {
  94. validatedCount() {
  95. this.$emit(EVENT_VALIDATE, {
  96. validity: this.validity,
  97. valid: this.valid,
  98. invalid: this.invalid,
  99. dirty: this.dirty,
  100. firstInvalidFieldIndex: this.firstInvalidFieldIndex
  101. })
  102. }
  103. },
  104. beforeCreate() {
  105. this.form = this
  106. this.fields = []
  107. this.validity = {}
  108. },
  109. mounted() {
  110. if (this.immediateValidate) {
  111. this.validate()
  112. }
  113. },
  114. methods: {
  115. submit(skipValidate = false) {
  116. this.skipValidate = skipValidate
  117. dispatchEvent(this.$refs.form, 'submit')
  118. this.skipValidate = false
  119. },
  120. reset() {
  121. dispatchEvent(this.$refs.form, 'reset')
  122. },
  123. submitHandler(e) {
  124. if (this.skipValidate) {
  125. this.$emit(EVENT_SUBMIT, e, this.model)
  126. return
  127. }
  128. const submited = (submitResult) => {
  129. if (submitResult) {
  130. this.$emit(EVENT_VALID, this.validity)
  131. this.$emit(EVENT_SUBMIT, e, this.model)
  132. } else {
  133. e.preventDefault()
  134. this.$emit(EVENT_INVALID, this.validity)
  135. }
  136. }
  137. if (this.valid === undefined) {
  138. this._submit(submited)
  139. if (this.validating || this.pending) {
  140. // async validate
  141. e.preventDefault()
  142. }
  143. } else {
  144. submited(this.valid)
  145. }
  146. },
  147. resetHandler(e) {
  148. this._reset()
  149. this.$emit(EVENT_RESET, e)
  150. },
  151. _submit(cb) {
  152. this.validate(() => {
  153. if (this.invalid) {
  154. if (this.options.scrollToInvalidField && this.firstInvalidField) {
  155. this.firstInvalidField.$el.scrollIntoView()
  156. }
  157. }
  158. cb && cb(this.valid)
  159. })
  160. },
  161. _reset() {
  162. this.fields.forEach((fieldComponent) => {
  163. fieldComponent.reset()
  164. })
  165. this.setValidity()
  166. this.setValidating()
  167. this.setPending()
  168. },
  169. validate(cb) {
  170. const promise = cb2PromiseWithResolve(cb)
  171. if (promise) {
  172. cb = promise.resolve
  173. }
  174. let doneCount = 0
  175. const len = this.fields.length
  176. this.originValid = undefined
  177. this.fields.forEach((fieldComponent) => {
  178. fieldComponent.validate(() => {
  179. doneCount++
  180. if (doneCount === len) {
  181. // all done
  182. cb && cb(this.valid)
  183. }
  184. })
  185. })
  186. return promise
  187. },
  188. updateValidating() {
  189. const validating = this.fields.some((fieldComponent) => fieldComponent.validating)
  190. this.setValidating(validating)
  191. },
  192. updatePending() {
  193. const pending = this.fields.some((fieldComponent) => fieldComponent.pending)
  194. this.setPending(pending)
  195. },
  196. setValidating(validating = false) {
  197. this.validating = validating
  198. },
  199. setPending(pending = false) {
  200. this.pending = pending
  201. },
  202. updateValidity(modelKey, valid, result, dirty) {
  203. const curResult = this.validity[modelKey]
  204. if (curResult && curResult.valid === valid && curResult.result === result && curResult.dirty === dirty) {
  205. return
  206. }
  207. this.setValidity(modelKey, {
  208. valid,
  209. result,
  210. dirty
  211. })
  212. },
  213. setValidity(key, val) {
  214. let validity = {}
  215. if (key) {
  216. Object.assign(validity, this.validity)
  217. if (val === undefined) {
  218. delete validity[key]
  219. } else {
  220. validity[key] = val
  221. }
  222. }
  223. let dirty = false
  224. let invalid = false
  225. let valid = true
  226. let firstInvalidFieldKey = ''
  227. this.fields.forEach((fieldComponent) => {
  228. const modelKey = fieldComponent.fieldValue.modelKey
  229. if (modelKey) {
  230. const retVal = validity[modelKey]
  231. if (retVal) {
  232. if (retVal.dirty) {
  233. dirty = true
  234. }
  235. if (retVal.valid === false) {
  236. valid = false
  237. } else if (valid && !retVal.valid) {
  238. valid = retVal.valid
  239. }
  240. if (!invalid && retVal.valid === false) {
  241. // invalid
  242. invalid = true
  243. firstInvalidFieldKey = modelKey
  244. }
  245. } else if (fieldComponent.hasRules) {
  246. if (valid) {
  247. valid = undefined
  248. }
  249. validity[modelKey] = {
  250. valid: undefined,
  251. result: {},
  252. dirty: false
  253. }
  254. }
  255. }
  256. })
  257. this.validity = validity
  258. this.dirty = dirty
  259. this.originValid = valid
  260. this.setFirstInvalid(firstInvalidFieldKey)
  261. this.validatedCount++
  262. },
  263. setFirstInvalid(key) {
  264. if (!key) {
  265. this.firstInvalidField = null
  266. this.firstInvalidFieldIndex = -1
  267. return
  268. }
  269. this.fields.some((fieldComponent, index) => {
  270. if (fieldComponent.fieldValue.modelKey === key) {
  271. this.firstInvalidField = fieldComponent
  272. this.firstInvalidFieldIndex = index
  273. return true
  274. }
  275. })
  276. },
  277. addField(fieldComponent) {
  278. this.fields.push(fieldComponent)
  279. },
  280. destroyField(fieldComponent) {
  281. const i = this.fields.indexOf(fieldComponent)
  282. this.fields.splice(i, 1)
  283. this.setValidity(fieldComponent.fieldValue.modelKey)
  284. }
  285. },
  286. beforeDestroy() {
  287. this.form = null
  288. this.firstInvalidField = null
  289. },
  290. components: {
  291. CubeFormGroup
  292. }
  293. }
  294. </script>
  295. <style lang="stylus" rel="stylesheet/stylus">
  296. @require "../../common/stylus/variable.styl"
  297. @require "../../common/stylus/mixin.styl"
  298. .cube-form
  299. position: relative
  300. font-size: $fontsize-large
  301. line-height: 1.429
  302. color: $form-color
  303. background-color: $form-bgc
  304. .cube-form_groups
  305. .cube-form-group-legend
  306. padding: 10px 15px
  307. &:empty
  308. padding-top: 5px
  309. padding-bottom: 5px
  310. .cube-form_standard
  311. .cube-form-item
  312. min-height: 46px
  313. .cube-form-field
  314. flex: 1
  315. font-size: $fontsize-medium
  316. .cube-validator
  317. display: flex
  318. align-items: center
  319. position: relative
  320. .cube-validator_invalid
  321. color: $form-invalid-color
  322. .cube-validator-content
  323. flex: 1
  324. .cube-validator-msg-def
  325. font-size: 0
  326. .cube-validator_invalid
  327. .cube-validator-msg
  328. &::before
  329. content: "\e614"
  330. padding-left: 5px
  331. font-family: "cube-icon"!important
  332. font-size: $fontsize-large-xx
  333. font-style: normal
  334. -webkit-font-smoothing: antialiased
  335. -webkit-text-stroke-width: 0.2px
  336. -moz-osx-font-smoothing: grayscale
  337. .cube-form-label
  338. width: 125px
  339. padding-right: 10px
  340. .cube-checkbox-group, .cube-radio-group
  341. &::before, &::after
  342. display: none
  343. .cube-input
  344. input
  345. padding: 13px 0
  346. background-color: transparent
  347. &::after
  348. display: none
  349. .cube-textarea-wrapper
  350. padding: 13px 0
  351. height: 20px
  352. &.cube-textarea_expanded
  353. height: 60px
  354. padding-bottom: 20px
  355. .cube-textarea-indicator
  356. bottom: 2px
  357. .cube-textarea
  358. padding: 0
  359. background-color: transparent
  360. &::after
  361. display: none
  362. .cube-select
  363. padding-left: 0
  364. background-color: transparent
  365. &::after
  366. display: none
  367. .cube-upload-def
  368. padding: 5px 0
  369. .cube-upload-btn, .cube-upload-file
  370. margin: 5px 10px 5px 0
  371. .cube-form_classic
  372. .cube-form-item
  373. display: block
  374. padding: 15px
  375. &:last-child
  376. padding-bottom: 30px
  377. &::after
  378. display: none
  379. .cube-validator-msg
  380. position: absolute
  381. margin-top: 3px
  382. &::before
  383. display: none
  384. .cube-validator-msg-def
  385. font-size: $fontsize-small
  386. .cube-form-item_btn
  387. padding-top: 0
  388. padding-bottom: 0
  389. &:last-child
  390. padding-bottom: 0
  391. .cube-form-label
  392. padding-bottom: 15px
  393. .cube-form_fresh
  394. .cube-form-item
  395. display: block
  396. padding: 2em 15px 10px
  397. &::after
  398. display: none
  399. .cube-validator-msg
  400. position: absolute
  401. top: 1em
  402. right: 15px
  403. bottom: auto
  404. margin-top: -.4em
  405. font-size: $fontsize-small
  406. &::before
  407. display: none
  408. .cube-validator-msg-def
  409. font-size: 100%
  410. .cube-form-item_btn
  411. padding-top: 0
  412. padding-bottom: 0
  413. &:last-child
  414. padding-bottom: 0
  415. .cube-form-label
  416. position: absolute
  417. top: 1em
  418. margin-top: -.4em
  419. font-size: $fontsize-small
  420. </style>