Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 105 additions & 1 deletion docs/v3/vue.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Usage with Vue

Maska provides custom Vue directive for use with input:
Maska provides custom Vue directive and Composable for use with input:

```html
<input v-maska:argument.modifier="value">
Expand Down Expand Up @@ -205,3 +205,107 @@ Vue.directive("maska", vMaska)
Vue.directive("maska", Maska.vMaska)
```
<!-- tabs:end -->


## Usage as Composable

You can use Maska as a composable in your setup script:

```vue
<script setup>
import { ref, useTemplateRef } from "vue";
import { useMaska } from "maska/vue"

const model = ref('');

const config = {
mask: '#-#'
}

const { unmasked, masked, completed } = useMaska(useTemplateRef('input'), config, model);
</script>
```

Signature: `useMaska(target, options, model?): UseMaskaReturn`

- `target` is a target maska element (string selector, HTMLElement, ref or computed template element)
- `options` can be passed as a string (mask template) or an object with configuration:
- `maskType`: what value (`masked` or `unmasked`) should be applied to the passed model (default: `unmasked`)
- ...other maska [options](/options).
- `model` is a model (`Ref<string>`) to bind maska value, optionally

<!-- tabs:start -->
### **Plain template**

```vue
<script setup>
import { useTemplateRef } from "vue"
import { useMaska } from "maska/vue"

const inputEl = useTemplateRef('inputEl')

const { unmasked: unmaskedValue, masked: maskedValue } = useMaska(inputEl, '#-#')
</script>

<template>
<input ref="inputEl">

Masked value: {{ maskedValue }}
Unmasked value: {{ unmaskedValue }}
</template>
```

### **Configurable options**

```vue
<script setup>
import { useTemplateRef } from "vue"
import { useMaska } from "maska/vue"

const options = {
mask: '#-#',
eager: true
}

const inputEl = useTemplateRef('inputEl')

const { unmasked: unmaskedValue, masked: maskedValue } = useMaska(inputEl, options)
</script>

<template>
<input ref="inputEl">

Masked value: {{ maskedValue }}
Unmasked value: {{ unmaskedValue }}
</template>
```


### **Computed options**

```vue
<script setup>
import { computed, ref, useTemplateRef } from 'vue'
import { useMaska } from 'maska/vue'

const modelValue = defineModel({ default: '123' });

const maskaTemplate = ref('#-#')

const inputEl = useTemplateRef('inputEl')

const maskaOptions = computed(() => ({
mask: maskaTemplate.value,
modelType: 'masked'
}))

const { unmasked } = useMaska(inputEl, maskaOptions, modelValue)
</script>

<template>
<input ref="inputEl" />
<input v-model="maskaTemplate" />
</template>

```
<!-- tabs:end -->
205 changes: 205 additions & 0 deletions src/vue/composable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import {
ComponentPublicInstance,
computed,
ComputedGetter,
isRef,
MaybeRefOrGetter,
onMounted,
onUnmounted,
readonly,
ref,
Ref,
shallowRef,
ShallowRef,
toValue,
watch
} from 'vue'
import { MaskaDetail, MaskInput, MaskInputOptions } from '../input'

const DEFAULT_MODEL_TYPE = 'unmasked'

export type MaybeElement<T extends HTMLElement = HTMLElement> = T | ComponentPublicInstance | undefined | null
export type MaybeComputedElementRef<T extends MaybeElement = MaybeElement> = MaybeRefOrGetter<T>

export type RefOrGetter<T> = Ref<T> | ComputedGetter<T>

export function unrefElement<T extends MaybeElement>(elRef: MaybeComputedElementRef<T>): T | undefined {
const plain = toValue(elRef)
return (plain as ComponentPublicInstance)?.$el ?? plain
}

export function isRefOrGetter<T>(value: MaybeRefOrGetter<T>): value is RefOrGetter<T> {
return isRef(value) || typeof value === 'function'
}

export type MaskElement = MaybeElement<HTMLInputElement>

export interface UseMaskaReturn {
masked: Readonly<Ref<string>>
unmasked: Readonly<Ref<string>>
completed: Readonly<Ref<boolean>>
instance: Readonly<ShallowRef<MaskInput>>
}

export type MaskaElementTarget = MaybeComputedElementRef<MaskElement>
export type MaskaSelectorTarget = string

interface UseMaskaModelType {
modelType?: 'masked' | 'unmasked'
}

export type MaskaPattern = MaybeRefOrGetter<string>
export type MaskaConfig = MaybeRefOrGetter<Partial<MaskInputOptions> & UseMaskaModelType>
export type MaskaModel = Ref<string | undefined> | undefined

export function useMaska(target: MaskaElementTarget, pattern: MaskaPattern, model?: Ref<string>): UseMaskaReturn
export function useMaska(target: MaskaElementTarget, config: MaskaConfig, model?: Ref<string>): UseMaskaReturn
export function useMaska(selector: MaskaSelectorTarget, pattern: MaskaPattern, model?: Ref<string>): UseMaskaReturn
export function useMaska(selector: MaskaSelectorTarget, config: MaskaConfig, model?: Ref<string>): UseMaskaReturn

export function useMaska(
target: MaskaElementTarget | MaskaSelectorTarget,
options: MaskaPattern | MaskaConfig,
model: MaskaModel = undefined
): UseMaskaReturn {
let prevTarget: HTMLInputElement | undefined | null
const instance = shallowRef<MaskInput>()

const masked = ref('')
const unmasked = ref('')
const completed = ref(false)

const modelType = computed(() => {
const plainOptions = toValue(options)

if (typeof plainOptions === 'string') {
return DEFAULT_MODEL_TYPE
}

if ((plainOptions as UseMaskaModelType)?.modelType !== undefined) {
return (plainOptions as UseMaskaModelType).modelType ?? DEFAULT_MODEL_TYPE
}

return DEFAULT_MODEL_TYPE
})

function getTarget(): Exclude<MaskElement, ComponentPublicInstance> {
let el

if (isRefOrGetter(target)) {
el = toValue(target)
}

if (typeof el === 'string') {
return document.querySelector(el)
}

if (el != null) {
return unrefElement(el) as Exclude<MaskElement, ComponentPublicInstance>
}

return undefined
}

if (model != null) {
watch(model, value => {
if (instance.value === undefined) {
return
}

const targetValue = modelType.value === 'masked' ? masked.value : unmasked.value

if (targetValue === value) {
return
}

const target = getTarget()

if (target != null) {
target.value = value ?? ''
}
})
}

function onMaska(detail: MaskaDetail): void {
masked.value = detail.masked
unmasked.value = detail.unmasked
completed.value = detail.completed

if (model != null) {
model.value = modelType.value === 'masked' ? detail.masked : detail.unmasked
}
}

function createConfig(): MaskInputOptions {
const optionsValue = toValue(options)

if (typeof optionsValue === 'string') {
return {
onMaska,
mask: optionsValue
}
}

return {
...optionsValue,
onMaska
}
}

function initialize(): void {
prevTarget = getTarget()

if (prevTarget == null) {
return
}

instance.value = new MaskInput(prevTarget, createConfig())

if (model != null) {
prevTarget.value = model.value ?? ''
}
}

function refresh(): void {
if (instance.value === undefined) {
initialize()
return
}

if (prevTarget !== target) {
destroy()
initialize()
}
}

function update(): void {
instance.value?.update(createConfig())
}

function destroy(): void {
instance.value?.destroy()
}

if (isRefOrGetter(target)) {
watch(() => unrefElement(target), value => {
if (value != null) refresh()
})
}

if (isRefOrGetter(options)) {
watch(() => toValue(options as RefOrGetter<MaskInputOptions>), value => {
if (value) update()
}, { deep: true })
}

onMounted(initialize)
onUnmounted(destroy)

return {
masked: readonly(masked),
unmasked: readonly(unmasked),
completed: readonly(completed),
instance: readonly(instance)
}
}
Loading