Best practice for implementing custom form in Vue - javascript

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>

Related

Vue custom directive to simulate getter/setter of computed property

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.

How to avoid checking object in object to exists in data Vue.js?

<template>
<img :src="user.avatar_thumbnail">
<a :href="user.profile.link"/>
</template>
<script>
export default {
data: function () {
user: {}
},
mounted: function() {
let vm = this;
axios.get('user/profile/<id>').then((response) => {
vm.user = response.data.user
});
}
}
</script>
if server does not send field 'avatar_thumbnail' in object user so page isn't rendered,
so i can user if else expression (ternary) but I can't do it everywhere
<img :src="user.avatar_thumbnail ? user.avatar_thumbnail : '' ">
maybe has vuejs some tool for this situation?
and more server send like this objects (nested): user.profile.avatar_thumbnail and I don't want to make nested ternary expression (please do not recommend me use computed methods - I'll be use this if I can't find another solution)
<img :src="user.avatar_thumbnail || '' ">
or:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
and, correct
export default {
data: function () {
return {
user: {}
}
},
There are many different different way to resolve your issues. But in your given code there are many errors. you can try this solution
<template>
<div>
<img :src="getThumbnail()" />
<a :href="user.profile.link" />
</div>
</template>
<script>
export default {
data: function() {
return{
user: {}
}
},
methods: {
getThumbnail() {
if (this.user.avatar_thumbnail) {
return this.user.avatar_thumbnail
} else {
return 'default_thumbnil'
}
}
},
mounted: function() {
let vm = this
axios.get('user/profile/<id>').then(response => {
vm.user = response.data.user
})
}
}
</script>
It turns out to be quite simple, just set a default value to the object with the same structure as the object that you're gonna receive. For example:
data: function () {
user: {
avatar_thumbnail: '',
profile: {
link: ''
}
}
}
You can also define a function that return the initial data, like so:
const initialUser = () => {
return {
avatar_thumbnail: '',
profile: {
link: ''
}
}
}
Then use it in the data property:
data: function () {
user: initialUser()
}
Note: You might only need to specify the properties of the object which are used in the template to overcome the potential errors.

Vue plugin, adding global component which takes data

I am using Vue plugins so that user can access a global component once registering the global component and configuring it inside Vue.use. For this I need to pass some data from Vue.use() to Component.vue.
Take a look at the following code:
Vue.use(MyPlugin, { data: true });
the code of MyPlugin is
import Plugin from './plugin';
const IconPlugin = {
install(Vue, options) {
console.log(options); // Here I can see {data: true}
Vue.component('GlobalComponent', Icon);
},
};
Now I need to pass this options variable to the component. So that a user whenever use
<GlobalComponent />
{data: true} should always be there.
Basically, that is a configuration which user is passing and the further component computation will be dependent on this.
You can use Vue.extend to extend components
var Icon = Vue.extend({
data: function() {
return {
foo: 'fooooo',
bar: 'barr'
}
},
template: '<div><button #click="doFooStuff">{{foo}}</button><button #click="doBarStuff">{{bar}}</button></div>',
methods: {
doBarStuff: function() {
console.log(this.foo, this.bar)
},
doFooStuff: function() {
console.log(this.foo)
}
}
})
const IconPlugin = {
install(Vue, options) {
// console.log(options);
// normalize data (select only props you want)
var data = Object.assign({}, options);
var NewIcon = Icon.extend({
data: function() {
return data;
}
})
Vue.component('GlobalComponent', NewIcon);
},
};
Vue.use(IconPlugin, {
foo: 'FOOO'
});
new Vue({
el: '#app',
components: {
Icon
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<icon></icon>
<global-component></global-component>
</div>
it sounds like you want to take a look at the component guide. It would seem that you want to merge the data with where you are passing Icon.

Vue: Accessing data from an unmounted component

I have an issue where I want to retreive data from a child component, but the parent needs to use that data, before the child is mounted.
My parent looks like this
<template>
<component :is="childComp" #mounted="setData"/>
</template>
<script>
data : {
childComp : null,
importantData : null
},
methods : {
addComponent : function() {
this.prepareToAdd(this.importantData);
this.childComp = "componentA"; //sometimes will be other component
},
setData : function(value) {
this.importantData = value;
},
prepareToAdd : function(importantData){
//something that has to be run before childComp can be mounted.
}
}
</script>
My child (or rather, all the potential children) would contain something like this:
<script>
data : {
importantData : 'ABC',
},
created: function() {
this.$emit('mounted', this.importantData);
},
</script>
This clearly doesn't work - importantData is set when the childComponent is mounted, but prepareToAdd needs that data first.
Is there another way of reaching in to the child component and accessing its data, before it is mounted?
You can use $options to store your important data and have it available in beforeCreate. You can also use it to initialize a data item, and you can emit data items in created (you don't have to initialize from $options to emit in created, I'm just pointing out two things that can be done). The $options value is, itself, reactive (to my surprise) and can be used like any data item, with the added benefit that it is available before other data items.
new Vue({
el: '#app',
methods: {
doStuff(val) {
console.log("Got", val);
}
},
components: {
theChild: {
template: '<div>Values are {{$options.importantData}} and {{otherData}}</div>',
importantData: 'abc',
data() {
return {
otherData: this.$options.importantData
};
},
beforeCreate() {
this.$emit('before-create', this.$options.importantData);
},
created() {
this.$emit('on-created', this.otherData + ' again');
// It's reactive?!?!?
this.$options.importantData = 'changed';
}
}
}
});
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<the-child #before-create="doStuff" #on-created="doStuff"></the-child>
</div>
My bad :(
We cannot get the data inside beforeCreated() hook.
Use the beforeCreate() hook instead of created() hook:
beforeCreate: function() {
this.$emit('mounted', this.importantData);
},
We can use a watcher or computed option, so now your parent component would look:
data: {
childComp: null,
importantData: null,
isDataSet: false
},
methods: {
addComponent: function() {
this.prepareToAdd(this.importantData);
this.childComp = "componentA"; //sometimes will be other component
},
setData: function(value) {
this.importantData = value;
this.isDataSet = true;
},
prepareToAdd: function(importantData) {
//something that has to be run before childComp can be mounted.
}
},
watch: {
isDataSet: function(newValue, oldValue) {
if (newValue) {
this.addComponent();
}
}
}
I would suggest to use computed method as it caches the results. Use watcher when you have to perform asynchronous code.

vue watch can detect external js file attributes?

An external js file that changes the value of a property by a click
event, but how does it detect that this property has changed since the
vue page used this property value?
1.External js
var obj = {},initValue = true;
Object.defineProperty(obj, "newKey", {
get: function () {
return initValue;
},
set: function (value) {
return initValue = value;
}
});
var con=function () {
document.getElementById('btncc').onclick = function () {
setTimeout(() => {
console.log(obj.newKey)
}, 3000)
initValue = false;
}
}
export default {
con,obj
}
> 2.vue page
> Change the style
<input id="color" type="color" :class="obj ? 'transparent': 'transparent2'" :disabled ="obj"/>
import Canvas from '../../Api/js'
export default {
name: 'HelloWorld',
data () {
return {
obj:Canvas.obj.newKey
}
},
mounted () {
Canvas.con()
console.log(this.obj,'我是msg啊');
},
watch: {
[Canvas.obj.newKey] (val) {
if(val){
console.log(this.obj,'我改变了吗,');
}
}
}
}
watch no effect.
Probably like this.
https://jsfiddle.net/qjwanglei/L5afz75t/3/

Categories

Resources