Newer
Older
XiaoGanWXMini / uni_modules / uni-forms / components / uni-forms-item / uni-forms-item.vue
@zhangdeliang zhangdeliang on 29 Jul 14 KB 迁移
<template>
	<view class="uni-forms-item"
		:class="['is-direction-' + localLabelPos ,border?'uni-forms-item--border':'' ,border && isFirstBorder?'is-first-border':'']">
		<slot name="label">
			<view class="uni-forms-item__label" :class="{'no-label':!label && !required}"
				:style="{width:localLabelWidth,justifyContent: localLabelAlign}">
				<text v-if="required" class="is-required">*</text>
				<text>{{label}}</text>
			</view>
		</slot>
		<!-- #ifndef APP-NVUE -->
		<view class="uni-forms-item__content">
			<slot></slot>
			<view class="uni-forms-item__error" :class="{'msg--active':msg}">
				<text>{{msg}}</text>
			</view>
		</view>
		<!-- #endif -->
		<!-- #ifdef APP-NVUE -->
		<view class="uni-forms-item__nuve-content">
			<view class="uni-forms-item__content">
				<slot></slot>
			</view>
			<view class="uni-forms-item__error" :class="{'msg--active':msg}">
				<text class="error-text">{{msg}}</text>
			</view>
		</view>
		<!-- #endif -->
	</view>
</template>

<script>
	/**
	 * uni-fomrs-item 表单子组件
	 * @description uni-fomrs-item 表单子组件,提供了基础布局已经校验能力
	 * @tutorial https://ext.dcloud.net.cn/plugin?id=2773
	 * @property {Boolean} required 是否必填,左边显示红色"*"号
	 * @property {String } 	label 				输入框左边的文字提示
	 * @property {Number } 	labelWidth 			label的宽度,单位rpx(默认70)
	 * @property {String } 	labelAlign = [left|center|right] label的文字对齐方式(默认left)
	 * 	@value left		label 左侧显示
	 * 	@value center	label 居中
	 * 	@value right	label 右侧对齐
	 * @property {String } 	errorMessage 		显示的错误提示内容,如果为空字符串或者false,则不显示错误信息
	 * @property {String } 	name 				表单域的属性名,在使用校验规则时必填
	 * @property {String } 	leftIcon 			【1.4.0废弃】label左边的图标,限 uni-ui 的图标名称
	 * @property {String } 	iconColor 		【1.4.0废弃】左边通过icon配置的图标的颜色(默认#606266)
	 * @property {String} validateTrigger = [bind|submit|blur]	【1.4.0废弃】校验触发器方式 默认 submit
	 * 	@value bind 	发生变化时触发
	 * 	@value submit 提交时触发
	 * 	@value blur 	失去焦点触发
	 * @property {String } 	labelPosition = [top|left] 【1.4.0废弃】label的文字的位置(默认left)
	 * 	@value top	顶部显示 label
	 * 	@value left	左侧显示 label
	 */

	export default {
		name: 'uniFormsItem',
		options: {
			virtualHost: true
		},
		provide() {
			return {
				uniFormItem: this
			}
		},
		inject: {
			form: {
				from: 'uniForm',
				default: null
			},
		},
		props: {
			// 表单校验规则
			rules: {
				type: Array,
				default () {
					return null;
				}
			},
			// 表单域的属性名,在使用校验规则时必填
			name: {
				type: [String, Array],
				default: ''
			},
			required: {
				type: Boolean,
				default: false
			},
			label: {
				type: String,
				default: ''
			},
			// label的宽度
			labelWidth: {
				type: [String, Number],
				default: ''
			},
			// label 居中方式,默认 left 取值 left/center/right
			labelAlign: {
				type: String,
				default: ''
			},
			// 强制显示错误信息
			errorMessage: {
				type: [String, Boolean],
				default: ''
			},
			// 1.4.0 弃用,统一使用 form 的校验时机
			// validateTrigger: {
			// 	type: String,
			// 	default: ''
			// },
			// 1.4.0 弃用,统一使用 form 的label 位置
			// labelPosition: {
			// 	type: String,
			// 	default: ''
			// },
			// 1.4.0 以下属性已经废弃,请使用  #label 插槽代替
			leftIcon: String,
			iconColor: {
				type: String,
				default: '#606266'
			},
		},
		data() {
			return {
				errMsg: '',
				userRules: null,
				localLabelAlign: 'left',
				localLabelWidth: '70rpx',
				localLabelPos: 'left',
				border: false,
				isFirstBorder: false,
			};
		},
		computed: {
			// 处理错误信息
			msg() {
				return this.errorMessage || this.errMsg;
			}
		},
		watch: {
			// 规则发生变化通知子组件更新
			'form.formRules'(val) {
				// TODO 处理头条vue3 watch不生效的问题
				// #ifndef MP-TOUTIAO
				this.init()
				// #endif
			},
			'form.labelWidth'(val) {
				// 宽度
				this.localLabelWidth = this._labelWidthUnit(val)

			},
			'form.labelPosition'(val) {
				// 标签位置
				this.localLabelPos = this._labelPosition()
			},
			'form.labelAlign'(val) {

			}
		},
		created() {
			this.init(true)
			if (this.name && this.form) {
				// TODO 处理头条vue3 watch不生效的问题
				// #ifdef MP-TOUTIAO
				this.$watch('form.formRules', () => {
					this.init()
				})
				// #endif

				// 监听变化
				this.$watch(
					() => {
						const val = this.form._getDataValue(this.name, this.form.localData)
						return val
					},
					(value, oldVal) => {
						const isEqual = this.form._isEqual(value, oldVal)
						// 简单判断前后值的变化,只有发生变化才会发生校验
						// TODO  如果 oldVal = undefined ,那么大概率是源数据里没有值导致 ,这个情况不哦校验 ,可能不严谨 ,需要在做观察
						// fix by mehaotian 暂时取消 && oldVal !== undefined ,如果formData 中不存在,可能会不校验
						if (!isEqual) {
							const val = this.itemSetValue(value)
							this.onFieldChange(val, false)
						}
					}, {
						immediate: false
					}
				);
			}

		},
		// #ifndef VUE3
		destroyed() {
			if (this.__isUnmounted) return
			this.unInit()
		},
		// #endif
		// #ifdef VUE3
		unmounted() {
			this.__isUnmounted = true
			this.unInit()
		},
		// #endif
		methods: {
			/**
			 * 外部调用方法
			 * 设置规则 ,主要用于小程序自定义检验规则
			 * @param {Array} rules 规则源数据
			 */
			setRules(rules = null) {
				this.userRules = rules
				this.init(false)
			},
			// 兼容老版本表单组件
			setValue() {
				// console.log('setValue 方法已经弃用,请使用最新版本的 uni-forms 表单组件以及其他关联组件。');
			},
			/**
			 * 外部调用方法
			 * 校验数据
			 * @param {any} value 需要校验的数据
			 * @param {boolean} 是否立即校验
			 * @return {Array|null} 校验内容
			 */
			async onFieldChange(value, formtrigger = true) {
				const {
					formData,
					localData,
					errShowType,
					validateCheck,
					validateTrigger,
					_isRequiredField,
					_realName
				} = this.form
				const name = _realName(this.name)
				if (!value) {
					value = this.form.formData[name]
				}
				// fixd by mehaotian 不在校验前清空信息,解决闪屏的问题
				// this.errMsg = '';

				// fix by mehaotian 解决没有检验规则的情况下,抛出错误的问题
				const ruleLen = this.itemRules.rules && this.itemRules.rules.length
				if (!this.validator || !ruleLen || ruleLen === 0) return;

				// 检验时机
				// let trigger = this.isTrigger(this.itemRules.validateTrigger, this.validateTrigger, validateTrigger);
				const isRequiredField = _isRequiredField(this.itemRules.rules || []);
				let result = null;
				// 只有等于 bind 时 ,才能开启时实校验
				if (validateTrigger === 'bind' || formtrigger) {
					// 校验当前表单项
					result = await this.validator.validateUpdate({
							[name]: value
						},
						formData
					);

					// 判断是否必填,非必填,不填不校验,填写才校验 ,暂时只处理 undefined  和空的情况
					if (!isRequiredField && (value === undefined || value === '')) {
						result = null;
					}

					// 判断错误信息显示类型
					if (result && result.errorMessage) {
						if (errShowType === 'undertext') {
							// 获取错误信息
							this.errMsg = !result ? '' : result.errorMessage;
						}
						if (errShowType === 'toast') {
							uni.showToast({
								title: result.errorMessage || '校验错误',
								icon: 'none'
							});
						}
						if (errShowType === 'modal') {
							uni.showModal({
								title: '提示',
								content: result.errorMessage || '校验错误'
							});
						}
					} else {
						this.errMsg = ''
					}
					// 通知 form 组件更新事件
					validateCheck(result ? result : null)
				} else {
					this.errMsg = ''
				}
				return result ? result : null;
			},
			/**
			 * 初始组件数据
			 */
			init(type = false) {
				const {
					validator,
					formRules,
					childrens,
					formData,
					localData,
					_realName,
					labelWidth,
					_getDataValue,
					_setDataValue
				} = this.form || {}
				// 对齐方式
				this.localLabelAlign = this._justifyContent()
				// 宽度
				this.localLabelWidth = this._labelWidthUnit(labelWidth)
				// 标签位置
				this.localLabelPos = this._labelPosition()
				// 将需要校验的子组件加入form 队列
				this.form && type && childrens.push(this)

				if (!validator || !formRules) return
				// 判断第一个 item
				if (!this.form.isFirstBorder) {
					this.form.isFirstBorder = true;
					this.isFirstBorder = true;
				}

				// 判断 group 里的第一个 item
				if (this.group) {
					if (!this.group.isFirstBorder) {
						this.group.isFirstBorder = true;
						this.isFirstBorder = true;
					}
				}
				this.border = this.form.border;
				// 获取子域的真实名称
				const name = _realName(this.name)
				const itemRule = this.userRules || this.rules
				if (typeof formRules === 'object' && itemRule) {
					// 子规则替换父规则
					formRules[name] = {
						rules: itemRule
					}
					validator.updateSchema(formRules);
				}
				// 注册校验规则
				const itemRules = formRules[name] || {}
				this.itemRules = itemRules
				// 注册校验函数
				this.validator = validator
				// 默认值赋予
				this.itemSetValue(_getDataValue(this.name, localData))
			},
			unInit() {
				if (this.form) {
					const {
						childrens,
						formData,
						_realName
					} = this.form
					childrens.forEach((item, index) => {
						if (item === this) {
							this.form.childrens.splice(index, 1)
							delete formData[_realName(item.name)]
						}
					})
				}
			},
			// 设置item 的值
			itemSetValue(value) {
				const name = this.form._realName(this.name)
				const rules = this.itemRules.rules || []
				const val = this.form._getValue(name, value, rules)
				this.form._setDataValue(name, this.form.formData, val)
				return val
			},

			/**
			 * 移除该表单项的校验结果
			 */
			clearValidate() {
				this.errMsg = '';
			},

			// 是否显示星号
			_isRequired() {
				// TODO 不根据规则显示 星号,考虑后续兼容
				// if (this.form) {
				// 	if (this.form._isRequiredField(this.itemRules.rules || []) && this.required) {
				// 		return true
				// 	}
				// 	return false
				// }
				return this.required
			},

			// 处理对齐方式
			_justifyContent() {
				if (this.form) {
					const {
						labelAlign
					} = this.form
					let labelAli = this.labelAlign ? this.labelAlign : labelAlign;
					if (labelAli === 'left') return 'flex-start';
					if (labelAli === 'center') return 'center';
					if (labelAli === 'right') return 'flex-end';
				}
				return 'flex-start';
			},
			// 处理 label宽度单位 ,继承父元素的值
			_labelWidthUnit(labelWidth) {

				// if (this.form) {
				// 	const {
				// 		labelWidth
				// 	} = this.form
				return this.num2rpx(this.labelWidth ? this.labelWidth : (labelWidth || (this.label ? 70 : 'auto')))
				// }
				// return '70rpx'
			},
			// 处理 label 位置
			_labelPosition() {
				if (this.form) return this.form.labelPosition || 'left'
				return 'left'

			},

			/**
			 * 触发时机
			 * @param {Object} rule 当前规则内时机
			 * @param {Object} itemRlue 当前组件时机
			 * @param {Object} parentRule 父组件时机
			 */
			isTrigger(rule, itemRlue, parentRule) {
				//  bind  submit
				if (rule === 'submit' || !rule) {
					if (rule === undefined) {
						if (itemRlue !== 'bind') {
							if (!itemRlue) {
								return parentRule === '' ? 'bind' : 'submit';
							}
							return 'submit';
						}
						return 'bind';
					}
					return 'submit';
				}
				return 'bind';
			},
			num2rpx(num) {
				if (typeof num === 'number') {
					return `${num}rpx`
				}
				return num
			}
		}
	};
</script>

<style lang="scss">
	.uni-forms-item {
		position: relative;
		display: flex;
		/* #ifdef APP-NVUE */
		// 在 nvue 中,使用 margin-bottom error 信息会被隐藏
		padding-bottom: 22rpx;
		/* #endif */
		/* #ifndef APP-NVUE */
		margin-bottom: 22rpx;
		/* #endif */
		flex-direction: row;

		&__label {
			display: flex;
			flex-direction: row;
			align-items: center;
			text-align: left;
			font-size: 14rpx;
			color: #606266;
			height: 36rpx;
			padding: 0 12rpx 0 0;
			/* #ifndef APP-NVUE */
			vertical-align: middle;
			flex-shrink: 0;
			/* #endif */

			/* #ifndef APP-NVUE */
			box-sizing: border-box;

			/* #endif */
			&.no-label {
				padding: 0;
			}
		}

		&__content {
			/* #ifndef MP-TOUTIAO */
			// display: flex;
			// align-items: center;
			/* #endif */
			position: relative;
			font-size: 14rpx;
			flex: 1;
			/* #ifndef APP-NVUE */
			box-sizing: border-box;
			/* #endif */
			flex-direction: row;

			/* #ifndef APP || H5 || MP-WEIXIN || APP-NVUE */
			// TODO 因为小程序平台会多一层标签节点 ,所以需要在多余节点继承当前样式
			&>uni-easyinput,
			&>uni-data-picker {
				width: 100%;
			}

			/* #endif */

		}

		& .uni-forms-item__nuve-content {
			display: flex;
			flex-direction: column;
			flex: 1;
		}

		&__error {
			color: #f56c6c;
			font-size: 12rpx;
			line-height: 1;
			padding-top: 4rpx;
			position: absolute;
			/* #ifndef APP-NVUE */
			top: 100%;
			left: 0;
			transition: transform 0.3s;
			transform: translateY(-100%);
			/* #endif */
			/* #ifdef APP-NVUE */
			bottom: 5rpx;
			/* #endif */

			opacity: 0;

			.error-text {
				// 只有 nvue 下这个样式才生效
				color: #f56c6c;
				font-size: 12rpx;
			}

			&.msg--active {
				opacity: 1;
				transform: translateY(0%);
			}
		}

		// 位置修饰样式
		&.is-direction-left {
			flex-direction: row;
		}

		&.is-direction-top {
			flex-direction: column;

			.uni-forms-item__label {
				padding: 0 0 8rpx;
				line-height: 1.5715;
				text-align: left;
				/* #ifndef APP-NVUE */
				white-space: initial;
				/* #endif */
			}
		}

		.is-required {
			// color: $uni-color-error;
			color: #dd524d;
			font-weight: bold;
		}
	}


	.uni-forms-item--border {
		margin-bottom: 0;
		padding: 10rpx 0;
		// padding-bottom: 0;
		border-top: 1rpx #eee solid;

		/* #ifndef APP-NVUE */
		.uni-forms-item__content {
			flex-direction: column;
			justify-content: flex-start;
			align-items: flex-start;

			.uni-forms-item__error {
				position: relative;
				top: 5rpx;
				left: 0;
				padding-top: 0;
			}
		}

		/* #endif */

		/* #ifdef APP-NVUE */
		display: flex;
		flex-direction: column;

		.uni-forms-item__error {
			position: relative;
			top: 0rpx;
			left: 0;
			padding-top: 0;
			margin-top: 5rpx;
		}

		/* #endif */

	}

	.is-first-border {
		/* #ifndef APP-NVUE */
		border: none;
		/* #endif */
		/* #ifdef APP-NVUE */
		border-width: 0;
		/* #endif */
	}
</style>