Vue custom directive to simulate getter/setter of computed property - javascript

There is an input tag when user type something, I want to add some symbols to the input value. Well, when I need the real value I should remove those symbols from the value. I know I can achieve this using computed property like following:
<template>
<input type="text" v-model="computedTest" /><!-- shows 20$ -->
</template>
<script>
export default {
data() {
return {
test: 20,
}
},
computed: {
computedTest: {
get() {
return this.test + "$"
},
set(val) {
this.test = this.val.replace(/$/g, "")
},
},
methods: {
doSomething() {
console.log(this.test) // 20
},
},
},
}
</script>
Since Vuejs can do this using computed property feature, I believe I can achieve this feature using custom directive. But, I have no idea of the way to do. This is the first try what I did:
<template>
<input type="text" v-model="test" v-custom />
</template>
<script>
export default {
data() {
return {
test: 0
}
},
}
</script>
And the code for v-custom directive:
export default {
bind: function(el, binding, vnode) {
Vue.set(el, "value", toLanguage(el.value, "en")) // toLanguage is a custom function
},
componentUpdated: function(el, binding, vnode) {
// getContext is a custom function and the `contextObj` variable will be equal to
// the whole data property of the component context and the `model` will be equal to
// the v-model expression (in this example, `test`).
let { contextObj, model } = getContext(vnode)
const vnodeValue = get(contextObj, model) // lodash `get` function
// parsing v-model value to remove symbols
const parsedValue = parseInputValue(el, vnodeValue) ?? vnodeValue
if (contextObj) {
Vue.set(contextObj, model, parsedValue)
}
el.value = toLanguage(el.value, "en")
el.dispatchEvent(new Event("input", { bubbles: true }))
},
}
But this directive snippet creates an infinite loop. I'm using Vue2.x. I'll appreciate anybody can help.

Related

Best practice for implementing custom form in Vue

My goal is to create a custom form component called app-form which provides v-model to access the validation. For the input I want to detect is also a custom component called app-input.
Here is my implementation so far.
app-form
<template>
<div>
<slot></slot>
</div>
</template>
const acceptedTags = ['app-input'];
export default {
/*
props: value,
data: isValid
*/
updated() {
// update isValid whenever the view changes
this.checkValidation();
},
methods: {
checkValidation() {
this.isValid = true;
this.checkValidation(this);
this.$emit('input', this.isValid);
},
checkValidationRecursion(node) {
if (acceptedTags.includes(node.$options.name)) {
let result = node.checkValidation();
this.isValid &&= result;
}
node.$children.forEach((child) => this.checkValidationRecursion(child));
},
}
}
app-input
<input
:value="selfValue"
#input="onInput"/>
export default {
name: 'app-input',
/*
props: value,
data: selfValue,
*/
methods: {
checkValidation() {
let valid = true;
/*
this.rules = [(v) => !!v || 'Required'];
*/
for (let f of this.rules) {
let result = f(this.selfValue);
if (typeof result === 'string') {
valid = false;
}
}
return valid;
},
// onInput() => update selfValue and emit
},
// watch: update selfValue
}
In the code above, the app-form have to traverse the whole component tree to get the target inputs every time anything is updated. Is there any better way to achieve it?
For these Kind of things I like to use provide/inject https://v2.vuejs.org/v2/api/#provide-inject. The idea is to "provide" an object in a Parent-Component (your Form-Component) and to "inject" this object in any of your descandant Components. Data here is not reactive, but you can pass a reference to a reactive Object (for example a Vue Object).
If providing a Vue-Instance you can emit an event, like "check-validation" on that instance and your descandant components can listen to that and then emitting an validate-Event with the validation-Result back to the parent Component.
Here is a very basic Example: https://codepen.io/arossbach/pen/xxdxOVZ
Vue.component('my-form', {
provide () {
return {
formBus: this.formBus,
};
},
mounted() {
setTimeout(() => {
this.formBus.$emit("check-validation");
},4000);
this.formBus.$on("validate-element", isValid => {
this.isValidForm &&= isValid;
});
},
data () {
return {
formBus: new Vue(),
isValidForm: true,
};
},
template: '<div><slot></slot></div>',
});
Vue.component('child', {
inject: ['formBus'],
template: '<div></div>',
data() {
return {
isValid: true,
}
},
methods: {
validate() {
this.isValid = Boolean(Math.round(Math.random()));
this.formBus.$emit("validate-element", this.isValid);
}
},
created() {
this.formBus.$on("check-validation", this.validate);
}
});
new Vue({
el: '#app',
data () {
return {
};
},
});
HTML:
<div id="app">
<my-form>
<child></child>
<child></child>
</my-form>
</div>

How to pass object handler into vue component

I want to cache state inside a handler passed by props.
Problem is, the template renders and text content changes well, but the console always throw error:
[Vue warn]: Error in v-on handler: "TypeError: Cannot read property 'apply' of undefined"
Codes below:
<template>
<picker-box :options="item.options" #change="handlePickerChange($event, item)">
<text>{{ item.options[getPickerValue(item)].text }}</text>
</picker-box>
</template>
<script>
export default {
props: {
item: {
type: Object,
default() {
return {
options: [{ text: 'op1' }, { text: 'op2' }],
handler: {
current: 0,
get() {
return this.current
},
set(value) {
this.current = value
},
},
/* I also tried this: */
// new (function () {
// this.current = 0
// this.get = () => {
// return this.current
// }
// this.set = (value) => {
// this.current = value
// }
// })(),
}
},
},
},
methods: {
handlePickerChange(value, item) {
item.handler.set(value)
},
getPickerValue(item) {
return item.handler.get()
},
},
}
</script>
I know it's easy using data() or model prop, but I hope to cahce as this handler.current, in other words, I just want to know why this handler object isn't correct (syntax layer), how can I fix it.
What exactly state are you trying pass?
If you need to keep track of a static property, you could use client's localstorage, or a watcher for this.
If you need to keep of more dynamic data, you could pass it to a computed property that keeps track and sets them everytime they change.

Vue computed property returns the whole function instead of the value

I have this computed prop:
methods: {
url_refresh: function (id) {
return `${this.url_base}?start=${Date.now()}`
}
}
And when i try to console log on mount:
mounted() {
console.log(this.url_refresh);
},
It logs the function instead of the value:
How do I get the value instead of the function?
Its supposed to return http://localhost/admin/agenda/refresh?agenda_id=2&start=2020-11-29T00:00:00-03:00
You are probably use methods instead of computed, look here
Should Work for you (tested):
<template>
<div>
Your Template
</div>
</template>
<script>
export default {
mounted() {
console.log(this.url_refresh);
},
data() {
return { url_base: "http://localhost/admin/agenda/refresh" };
},
computed: {
url_refresh() {
return `${this.url_base}?start=${Date.now()}`;
}
},
};
</script>
Will log: http://localhost/admin/agenda/refresh?start=1607431794589
Computed Caching vs Methods
Main point of using computed is caching behavior. Hence it doesn't make sense to pass any argument into computed (the result should depend only on other reactive data). If you need argument, use methods instead...

Algolia vue-places: Clear input after selection

I'm using the vue-places component to render an Algolia places search input in Vue - it works brilliantly.
After a user selects / accepts a suggestion from the search dropdown, I want to clear the input and allow them to search again. Based on the standard example provided, I have tried to set form.country.label v-model value back to null in the change handler:
<template>
<places
v-model="form.country.label"
placeholder="Where are we going ?"
#change="selectLocation"
:options="options">
</places>
</template>
<script>
import Places from 'vue-places';
export default {
data() {
return {
options: {
appId: <YOUR_PLACES_APP_ID>,
apiKey: <YOUR_PLACES_API_KEY>,
countries: ['US'],
},
form: {
country: {
label: null,
data: {},
},
},
};
},
components: {
Places,
},
methods: {
selectLocation: function(event: any) {
if (event.name !== undefined) {
/**
* implementation not important
*/
this.form.country.label = null;
}
}
}
}
</script>
The selectLocation method fires as expected - but I cannot find any way to rest the input value to be empty.
How can I update a data value from a component method and have this reflected in a referencing component - in this case the Algolia places input?
The issue occurs because of how vue-places is proxying change events from the underlying implementation. When a change event is received, it broadcasts the same event and then updates the input value:
Places.vue:
this.placesAutocomplete.on('change', (e) => {
this.$emit('change', e.suggestion);
this.updateValue(e.suggestion.value);
});
This means that any attempt to set a value in our change handler will immediately be overridden.
My solution is to create a ref to the vue-places instance and then use the built-in Vue.nextTick to use the internal places.js setVal method after the call to updateValue:
methods: {
selectLocation: function(event: any) {
if (event.name !== undefined) {
// do something with the value
Vue.nextTick(() => {
// clear the input value on the next update
this.$refs.placesSearch.placesAutocomplete.setVal(null);
});
}
}
}

How to use enums (or const) in VueJS?

I feel like an idiot for having to ask about something so seemingly simple, but I'm trying to figure out how to use "enums" in VueJS. Currently, in a file called LandingPage.js I have this bit of code:
const Form = {
LOGIN: 0,
SIGN_UP: 1,
FORGOT_PASSWORD: 2,
};
function main() {
new Vue({
el: "#landing-page",
components: {
LoginForm,
WhoIsBehindSection,
WhatIsSection,
Form,
},
data () {
return {
form: Form.LOGIN,
};
},
template: `
<div>
<LoginForm v-if="form === Form.LOGIN"></LoginForm>
<WhatIsSection></WhatIsSection>
<WhoIsBehindSection></WhoIsBehindSection>
</div>
`
});
}
It is the conditional v-if="form === Form.LOGIN" that is failing with the error messages:
Property or method "Form" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
Cannot read property 'LOGIN' of undefined
Just so you guys know without the conditional everything is working, and if I were to put this bit in the template
<p>{{ form }}</p>
it will print 0 on the screen. Though, putting this in the template
<p>{{ Form.LOGIN }}</p>
Will not result in it printing 0 on the screen. So I just cannot for the life of me figure out why it will not accept Form.LOGIN.
The Answer
I did add it to components, but never did I think of adding it to data. Happy that it's working now. :)
data () {
return {
form: Form.LOGIN,
Form, // I had to add this bit
};
},
Thank you MarcRo 👍
If you are using Vue in Typescript, then you can use:
import { TernaryStatus } from '../enum/MyEnums';
export default class MyClass extends Vue {
myVariable: TernaryStatus = TernaryStatus.Started;
TernaryStatus: any = TernaryStatus;
}
and then in Template you can just use
<div>Status: {{ myVariable == TernaryStatus.Started ? "Started It" : "Stopped it" }}</div>
You can use https://stackoverflow.com/a/59714524/3706939.
const State = Object.freeze({ Active: 1, Inactive: 2 });
export default {
data() {
return {
State,
state: State.Active
};
},
methods: {
method() {
return state === State.Active;
}
}
}
You only have access to properties of the Vue instance in your template. Just try accessing window or any global in your template, for example.
Hence, you can access {{ form }} but not {{ Form.LOGIN }}.
A wild guess is that it has something to do with how Vue compiles, but I don't know enough about the internals to answer this.
So just keep declaring all the properties you wish to use in your template in your Vue instance (usually as data).
You can enclose enum into class. All your data, the state, the enum variants would be in one place. The same about behaviours, so you will call form.isLogin() rather than form === Form.LOGIN and form.setLogin() rather than form = Form.Login.
The class to generate enums:
class Fenum {
constructor(start, variants) {
this.state = start;
variants.forEach(value => {
const valueC = value.charAt(0).toUpperCase() + value.slice(1);
this['is' + valueC] = () => this.state === value;
this['set' + valueC] = () => this.state = value;
})
}
}
Example of usage:
function main() {
new Vue({
el: "#landing-page",
components: {
LoginForm,
WhoIsBehindSection,
WhatIsSection,
Form,
},
data () {
return {
form: new Fenum("login", ["login", "signUp", "forgotPassword"]),
};
},
template: `
<div>
<LoginForm v-if="form.isLogin()"></LoginForm>
<WhatIsSection></WhatIsSection>
<WhoIsBehindSection></WhoIsBehindSection>
</div>
`
});
}
Vue observe nested objects, so each call of a set method (from.setLogin(), form.setSignUp(), ...) will trigger updates of the component as it should be.
The generated object from this example:
You can use $options instead of $data https://vuejs.org/v2/api/#vm-options
You can use Proxy to create object which throw runtime errors if someone will read non-defined value or try to add new value - here is createEnum (and use it in data() section)
function createEnum(name,obj) {
return new Proxy(obj, {
get(target, property) {
if (property in target) return target[property];
throw new Error(`ENUM: ${name}.${property} is not defined`);
},
set: (target, fieldName, value) => {
throw new Error(`ENUM: adding new member '${fieldName}' to Enum '${name}' is not allowed.`);
}
});
}
// ---------------
// ----- TEST ----
// ---------------
const Form = createEnum('Form',{
LOGIN: 0,
SIGN_UP: 1,
FORGOT_PASSWORD: 2,
});
// enum value exists
console.log(Form.LOGIN);
// enum value not exists
try{ console.log(Form.LOGOUT) } catch(e){ console.log(e.message)}
// try to add new value
try{ Form.EXIT = 5 } catch(e){ console.log(e.message)}
for string-like Enums where values are equal to keys you can use following helper
export function createEnumArr(name='', values=[]) {
let obj = {};
values.forEach(v => obj[v]=v);
return createEnum(name,obj);
}
const Form = createEnumArr('Form',[
"LOGIN",
"SIGN_UP",
"FORGOT_PASSWORD",
]);
The easiest way!
in main.js
const enumInfo = {
SOURCE_TYPE: {
WALLET: 1,
QR: 2
}
}
Vue.prototype.enumInfo = enumInfo
index.vue
{{enumInfo}}
For 2022 and beyond you should probably be using Vue 3 and Typescript.
The easiest way to use an enum is to map it to string values and then simply return it from your setup function.
<template>
...
<div v-if="mode == DarkModes.DARK">
do something for dark mode
</div>
...
</template>
<script lang="ts">
enum DarkModes {
BRIGHT = 'bright',
DARK = 'dark',
}
export default defineComponent({
name: 'MyDarkOrBrightComponent',
setup() {
const mode = ref(DarkModes.BRIGHT);
...
return {
mode,
DarkModes, // <- PASS YOUR ENUM HERE!
}
}
});
</script>
And if you're using the new <script setup> functionality it's just as easy ... all top level imports are automatically accessible from the template (if you want to put your enum in a separate file).
I've this problem, too.
Here my solution, just put this in the first line:
<script setup>
const Form = {
LOGIN: 0,
SIGN_UP: 1,
FORGOT_PASSWORD: 2,
};
</script>

Categories

Resources