Skip to content

Commit

Permalink
feat(comp: all): all v-model props support controlled (#509)
Browse files Browse the repository at this point in the history
fix #510
  • Loading branch information
danranVm committed Nov 12, 2021
1 parent e2ee3ba commit b30de1c
Show file tree
Hide file tree
Showing 91 changed files with 774 additions and 755 deletions.
4 changes: 2 additions & 2 deletions packages/cdk/click-outside/src/useClickOutside.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { ObjectDirective } from 'vue'

import { isFunction, isObject } from 'lodash-es'

import { noop, on } from '@idux/cdk/utils'
import { NoopFunction, on } from '@idux/cdk/utils'

interface ClickOutsideOptions {
exclude: (HTMLElement | null)[]
Expand Down Expand Up @@ -39,7 +39,7 @@ on(document, 'click', event => {

function createHandler(el: HTMLElement, binding: ClickOutsideBinding): void {
const exclude: HTMLElement[] = [el]
let handler: ClickOutsideHandler = noop
let handler: ClickOutsideHandler = NoopFunction
if (isFunction(binding)) {
handler = binding
} else if (isObject(binding)) {
Expand Down
27 changes: 22 additions & 5 deletions packages/cdk/forms/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
import type { AbstractControl, ControlPathType } from './controls'
import type { InjectionKey, Ref, WatchStopHandle } from 'vue'

import { getCurrentInstance, inject, shallowReactive, shallowRef, toRef, watch } from 'vue'
import { computed, getCurrentInstance, inject, ref, shallowReactive, shallowRef, toRef, watch } from 'vue'

import { isNil } from 'lodash-es'

import { IxPropTypes, Logger } from '@idux/cdk/utils'
import { IxPropTypes, Logger, callEmit } from '@idux/cdk/utils'

import { isAbstractControl } from './typeof'

Expand Down Expand Up @@ -78,22 +78,39 @@ export function useValueAccessor<T = any>(
options: FormAccessorOptions = {},
): { control: Ref<AbstractControl<T> | undefined>; accessor: FormAccessor<T> } {
const { controlKey, valueKey = 'value', disabledKey = 'disabled' } = options
const { props, emit } = getCurrentInstance()!
const { props } = getCurrentInstance()!
const control = useValueControl(controlKey)

const accessor = shallowReactive({}) as FormAccessor<T>
let stopWatcher: WatchStopHandle | undefined

watch(
control,
currControl => {
if (stopWatcher) {
stopWatcher()
stopWatcher = undefined
}

if (currControl) {
accessor.valueRef = currControl.valueRef
accessor.disabled = currControl.disabled
accessor.setValue = value => currControl.setValue(value, { dirty: true })
accessor.markAsBlurred = () => currControl.markAsBlurred()
} else {
accessor.valueRef = toRef(props, valueKey) as Ref<T>
const tempRef = ref(props[valueKey])
stopWatcher = watch(
() => props[valueKey],
value => (tempRef.value = value),
)
accessor.valueRef = computed(() => props[valueKey] ?? tempRef.value)
accessor.disabled = toRef(props, disabledKey) as Ref<boolean>
accessor.setValue = value => emit(`update:${valueKey}`, value)
accessor.setValue = value => {
if (value != accessor.valueRef.value) {
tempRef.value = value
callEmit(props[`onUpdate:${valueKey}`], value)
}
}
accessor.markAsBlurred = () => {}
}
},
Expand Down
16 changes: 7 additions & 9 deletions packages/cdk/popper/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { ComputedRef, Ref } from 'vue'

import { computed, reactive, ref, watch } from 'vue'

import { noop } from '@idux/cdk/utils'
import { NoopFunction, NoopObject } from '@idux/cdk/utils'

export function useElement<T>(): Ref<T | null> {
const element: Ref<T | null> = ref(null)
Expand All @@ -31,7 +31,7 @@ export function useState(options: PopperOptions): Required<PopperOptions> {
visible = false,
strategy = 'absolute',
modifiers = [],
onFirstUpdate = noop,
onFirstUpdate = NoopFunction,
} = options

return reactive({
Expand Down Expand Up @@ -159,15 +159,13 @@ export function usePopperEvents(
const onMouseenter = () => show()
const onMouseleave = () => hide()

const noop = {}

const eventsMap = {
click: noop,
focus: noop,
click: NoopObject,
focus: NoopObject,
hover: { onMouseenter, onMouseleave },
contextmenu: noop,
manual: noop,
contextmenu: NoopObject,
manual: NoopObject,
}

return computed(() => (baseOptions.allowEnter ? eventsMap[baseOptions.trigger] : noop))
return computed(() => (baseOptions.allowEnter ? eventsMap[baseOptions.trigger] : NoopObject))
}
1 change: 1 addition & 0 deletions packages/cdk/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './src/convert'
export * from './src/dom'
export * from './src/easings'
export * from './src/logger'
export * from './src/noop'
export * from './src/offset'
export * from './src/props'
export * from './src/typeof'
Expand Down
56 changes: 48 additions & 8 deletions packages/cdk/utils/src/composable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

import type { DeepReadonly, EffectScope, Ref } from 'vue'
import type { ComputedRef, EffectScope, Ref } from 'vue'

import { effectScope, onScopeDispose, readonly, shallowRef } from 'vue'
import { computed, effectScope, onScopeDispose, ref, shallowRef, watch } from 'vue'

import { isFunction } from 'lodash-es'

import { callEmit } from './props'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createSharedComposable<T extends (...args: any[]) => ReturnType<T>>(composable: T): T {
let subscribers = 0
Expand All @@ -35,11 +37,49 @@ export function createSharedComposable<T extends (...args: any[]) => ReturnType<
}) as T
}

export function useState<T>(init: T | (() => T)): [Ref<DeepReadonly<T>>, (value: T) => void] {
const value = isFunction(init) ? init() : init
const state = shallowRef(value)
const changeState = (value: T) => {
state.value = value
export function useState<T>(defaultOrFactory: T | (() => T), shallow = true): [ComputedRef<T>, (value: T) => void] {
const defaultValue = isFunction(defaultOrFactory) ? defaultOrFactory() : defaultOrFactory

const state = shallow ? shallowRef(defaultValue) : ref(defaultValue)

const setState = (value: T) => {
if (value !== state.value) {
state.value = value
}
}
return [readonly(state), changeState]

return [computed(() => state.value) as ComputedRef<T>, setState]
}

export function useControlledProp<T, K extends keyof T>(props: T, key: K): [ComputedRef<T[K]>, (value: T[K]) => void]
export function useControlledProp<T, K extends keyof T>(
props: T,
key: K,
defaultOrFactory: T[K] | (() => T[K]),
): [ComputedRef<Exclude<T[K], undefined>>, (value: Exclude<T[K], undefined>) => void]
export function useControlledProp<T, K extends keyof T>(
props: T,
key: K,
defaultOrFactory?: T[K] | (() => T[K]),
): [ComputedRef<T[K]>, (value: T[K]) => void] {
const tempProp = ref(props[key]) as Ref<T[K]>

watch(
() => props[key],
value => (tempProp.value = value),
)

const state = computed(
() => props[key] ?? tempProp.value ?? (isFunction(defaultOrFactory) ? defaultOrFactory() : defaultOrFactory)!,
)

const setState = (value: T[K]) => {
if (value !== state.value) {
tempProp.value = value
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callEmit((props as any)[`onUpdate:${key}`], value)
}
}

return [state, setState]
}
2 changes: 0 additions & 2 deletions packages/cdk/utils/src/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,6 @@ export function throttleRAF<T extends (...args: any[]) => void>(
return requestCb
}

export const noop = (): void => {}

function isStyleVisible(element: HTMLElement | SVGElement) {
const { display, visibility, opacity } = getComputedStyle(element)

Expand Down
10 changes: 10 additions & 0 deletions packages/cdk/utils/src/noop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* @license
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

export const NoopFunction = (): void => {}
export const NoopObject = Object.freeze({})
export const NoopArray = Object.freeze([])
1 change: 0 additions & 1 deletion packages/components/_private/overlay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,5 @@ export {
overlayTriggerDef as ɵOverlayTriggerDef,
overlayDelayDef as ɵOverlayDelayDef,
} from './src/types'
export { useVisibility as ɵUseVisibility } from './src/useVisibility'

export type { OverlayInstance as ɵOverlayInstance, OverlayPublicProps as ɵOverlayProps } from './src/types'
32 changes: 0 additions & 32 deletions packages/components/_private/overlay/src/useVisibility.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/components/checkbox/docs/Index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ cover:

| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 |
| --- | --- | --- | --- | --- | --- |
| `v-model:checked` | 指定当前勾选框是否选中 | `boolean \| string \| number` | `false` | - | - |
| `v-model:checked` | 指定当前勾选框是否选中 | `boolean \| string \| number` | - | - | - |
| `control` | 控件控制器 | `string \| number \| AbstractControl` | - | - | 配合 `@idux/cdk/forms` 使用, 参考 [Form](/components/form/zh) |
| `autofocus` | 是否以自动聚焦 | `boolean` | `false` | - | - |.
| `buttoned` | 是否以按钮显示 | `boolean` | - | - | - |
Expand Down Expand Up @@ -49,7 +49,7 @@ cover:

| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 |
| --- | --- | --- | --- | --- | --- |
| `v-model:value` | 指定当前勾选框是否选中 | `any[]` | `[]` | - | - |
| `v-model:value` | 指定当前勾选框是否选中 | `any[]` | - | - | - |
| `control` | 控件控制器 | `string \| number \| AbstractControl` | - | - | 配合 `@idux/cdk/forms` 使用, 参考 [Form](/components/form/zh) |
| `buttoned` |`IxCheckbox``buttoned`属性 | `boolean` | `false` | - | - |
| `disabled` |`IxCheckbox``disabled`属性 | `boolean` | `false` | - |- |
Expand Down
13 changes: 6 additions & 7 deletions packages/components/checkbox/src/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ const useCheckbox = (props: CheckboxProps, checkboxGroup: CheckboxGroupContext |

if (checkboxGroup) {
const { props: groupProps, accessor } = checkboxGroup
isChecked = computed(() => accessor.valueRef.value?.includes(props.value ?? props.trueValue))
isDisabled = computed(() => props.disabled ?? accessor.disabled.value)
isChecked = computed(() => (accessor.valueRef.value ?? []).includes(props.value ?? props.trueValue))
isDisabled = computed(() => accessor.disabled.value)

handleBlur = (evt: FocusEvent) => {
isFocused.value = false
Expand All @@ -123,17 +123,16 @@ const useCheckbox = (props: CheckboxProps, checkboxGroup: CheckboxGroupContext |
const checked = (evt.target as HTMLInputElement).checked
const checkValue = checked ? props.trueValue : props.falseValue
const value = props.value
const groupCheckedValue = [...accessor.valueRef.value]
const checkValueIndex = accessor.valueRef.value.indexOf(value)
const groupCheckedValue = [...(accessor.valueRef.value ?? [])]
const checkValueIndex = groupCheckedValue.indexOf(value)
if (checkValueIndex === -1) {
groupCheckedValue.push(value)
} else {
groupCheckedValue.splice(checkValueIndex, 1)
}
accessor.setValue(groupCheckedValue)
callEmit(props.onChange, checkValue)

callEmit(groupProps.onChange, groupCheckedValue)
accessor.setValue(groupCheckedValue)
}
} else {
const { accessor, control } = useValueAccessor<CheckValue>({ valueKey: 'checked' })
Expand All @@ -150,8 +149,8 @@ const useCheckbox = (props: CheckboxProps, checkboxGroup: CheckboxGroupContext |
handleChange = (evt: Event) => {
const checked = (evt.target as HTMLInputElement).checked
const checkValue = checked ? props.trueValue : props.falseValue
callEmit(props.onChange, checkValue)
accessor.setValue(checkValue)
callEmit(props.onChange, checkValue)
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/components/checkbox/src/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { InjectionKey } from 'vue'

export interface CheckboxGroupContext {
props: CheckboxGroupProps
accessor: FormAccessor
accessor: FormAccessor<unknown[] | undefined>
}

export const checkboxGroupToken: InjectionKey<CheckboxGroupContext> = Symbol('checkboxGroupToken')
4 changes: 2 additions & 2 deletions packages/components/checkbox/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type CheckValue = string | number | boolean
export type CheckboxOption = Omit<CheckboxPublicProps, 'checked' | 'onUpdate:checked' | 'onChange' | 'indeterminate'>

export const checkboxProps = {
checked: IxPropTypes.oneOfType([String, Number, Boolean]).def(false),
checked: IxPropTypes.oneOfType([String, Number, Boolean]),
control: controlPropDef,
autofocus: IxPropTypes.bool.def(false),
buttoned: IxPropTypes.bool,
Expand Down Expand Up @@ -50,7 +50,7 @@ export type CheckboxComponent = DefineComponent<
export type CheckboxInstance = InstanceType<DefineComponent<CheckboxProps, CheckboxBindings>>

export const checkboxGroupProps = {
value: IxPropTypes.array().def(() => []),
value: IxPropTypes.array(),
control: controlPropDef,
buttoned: IxPropTypes.bool.def(false),
disabled: IxPropTypes.bool.def(false),
Expand Down
4 changes: 2 additions & 2 deletions packages/components/collapse/__tests__/collapse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,11 @@ describe('Collapse', () => {

await wrapper.findAll('.ix-header')[0].trigger('click')

expect(wrapper.findAll('.ix-collapse-panel-expanded').length).toBe(1)
expect(onUpdateExpandedKeys).toBeCalledWith([1])

await wrapper.setProps({ expandedKeys: [1] })
await wrapper.findAll('.ix-header')[0].trigger('click')

expect(wrapper.findAll('.ix-collapse-panel-expanded').length).toBe(2)
expect(onUpdateExpandedKeys).toBeCalledWith([1, 0])
})

Expand All @@ -72,6 +71,7 @@ describe('Collapse', () => {

expect(wrapper.findAll('.ix-collapse-panel-expanded').length).toBe(1)
expect(onUpdateExpandedKeys).toBeCalledWith([1])
await wrapper.setProps({ expandedKeys: [1] })

await wrapper.findAll('.ix-header')[0].trigger('click')

Expand Down
2 changes: 1 addition & 1 deletion packages/components/collapse/docs/Index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ order: 0

| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 |
| --- | --- | --- | --- | --- | --- |
| `v-model:expandedKeys` | 当前展开面板的 key | `(string \| number)[]` | `[]` | - |- |
| `v-model:expandedKeys` | 当前展开面板的 key | `(string \| number)[]` | - | - |- |
| `accordion` | 是否开启手风琴模式 | `boolean` | `false` || - |
| `borderless` | 是否显示边框 | `boolean` | `false` ||- |
| `expandIcon` | 自定义展开图标 | `string \| #expandIcon="{key, expanded}"` | `'right'` ||- |
Expand Down
Loading

0 comments on commit b30de1c

Please sign in to comment.