Reputation: 11364
I'm probably overlooking something fundamental with Vue, but...
Given dynamic components, each getting a generated ref and id, how can a reference to the component be dynamically passed via Pinia (or any other global means), such that a completely unrelated component elsewhere in the app can have dynamic binding (ideally 2-way) with it?
Indeed with pre-defined state entries in Pinia, you can easily have global binding; this is what Pinia does. In this app there are already many things bound to Pinia states. These are known entities and have explicit Pinia store entries. However dynamic components are not known therefore have no explicit store entries... and there's no desire to manage, let's say, 100's (exaggeration) of store entries.
A kiosk app on custom hardware with no readily available keyboard (hardware or OS). In the app, I have a virtual keyboard component that appears in a sliding drawer component. Across the app are various input fields presented dynamically. As the user focuses (taps) on input fields, I need the field bound (ideally 2-way binding) to the app's virtual keyboard component.
We have the ability to edit input fields with the virtual keyboard, but it's via DOM manipulation, and there's no proper binding...
On input field focus event, pass reference to input field into Pinia state store.activeField
(we don't have to do this, but it's convenient) and open the app's virtual keyboard drawer.
An input component instance
The input components are a little more complicated but it roughly boils down to this:
<input v-model="value" :id="id" :ref="id" @focus="store.openKeyboard($event.id)" />
Store (Pinia)
openKeyboard(id:string) {
this.activeInput = document.getElementById(id) as HTMLInputElement
this.showKeyboardDrawer = true
}
Keyboard component
In the virtual-keyboard, on input we update the value of the focused input field DOM element (via store.activeField
). This is how we edit the input field; there's no binding. After editing the DOM value, the Vue instance wrapping it which feeds the v-model, is out of sync, so we trigger an input event (dispatchEvent
) so Vue knows the input field value has changed.
const OnInputChange = (event:any) => {
store.activeInput.value = event
store.activeInput.dispatchEvent(new Event('input'))
}
This provides editing-ability of the focus input field, but still not true binding and involves DOM work.
Looking for other techniques...
Ideally:
1 - Pass a Vue ref? Typically ref's are local to SFC's, yes? Or, is there a way to globally access a ref? How do you ensure unique ref names?
2 - Broadcast keyboard inputs and listen for them in each instance of an input field, then make changes accordingly (ie check if field is focused). The broadcast could be via Pinia or event bus. This doesn't solve the binding need, but at least it avoids DOM manipulation. Downside is we have a bunch of active watchers (one per field).
3 - Something else?
Any feedback appreciated.
Upvotes: 0
Views: 499
Reputation: 23602
You could create a wrapper component (though implement your our binding rules, I'm using the focusin event):
<script>
import {ref} from 'vue';
export const keyboardText = ref('');
let off;
const activeInput = ref();
watch(activeInput, (input, old) => {
old?.exposed.setActive(false);
input.exposed.setActive(true);
});
</script>
<script setup>
import {useSlots,watch, withDirectives, vModelText, getCurrentInstance, computed} from 'vue';
const model = defineModel();
const slots = useSlots();
const self = getCurrentInstance();
const active = ref(false);
defineExpose({
setActive(val){
active.value = val;
}
});
watch(model, val => active.value && (keyboardText.value = val));
const vClass = (el, {value})=>{
for(const k in value){
el.classList.toggle(k, !!value[k].value);
}
}
const render = () => {
const out = slots.default();
out.forEach(vnode => Object.assign(vnode.props ??= {}, {
onFocusin(){
off?.();
keyboardText.value = model.value;
off = watch(keyboardText, text => model.value = text);
activeInput.value = self;
},
modelValue: model.value,
'onUpdate:modelValue': text => model.value = text
}));
return out.map(vnode => withDirectives(vnode, [[vClass, {active}]]));
};
</script>
<template>
<render :class="{active}"/>
</template>
<style>
input.active{
background: #ffb;
}
</style>
Upvotes: 0