Add condition to .sync in vue.js - javascript

I have a component that takes an options property. The options can be synced. As described below.
<component :options.sync="options">...</component>
The component is found in a parent component that has a shouldSync prop. As described below:
<template>
<div>
<component :options.sync="options">...</component>
</div>
</template>
<script>
export default {
name: 'parent',
props: ['shouldSync']
}
</script>
I want to use .sync modifier only if shouldSync property of the parent component is true. I tried using a dynamic prop with a computed property as below but it didn't work as expected.
<template>
<div>
<component :[optionsProp]="options">...</component>
</div>
</template>
<script>
export default {
name: 'parent',
props: ['shouldSync'],
computed: {
optionsProp: () => {
return this.shouldSync ? 'options.sync' : 'options'
}
}
}
</script>
Unfortunately, it didn't work.
Another alternative would be to duplicate the component tag, one with .sync and the other without, and use the v-if directive to determine which one to use. As described below:
<template>
<div>
<component v-if="shouldSync" :options.sync="options">...</component>
<component v-else :options="options">...</component>
</div>
</template>
But I don't want to do this because the default slot of <component /> contains much code and I don't want to duplicate that. I don't also want to transfer the default slot codes to a new component and include it here.
Is there any better way I can handle this situation? Thanks.

<component :options.sync="options"> is just syntactic sugar for <component :options="options" #update:options="options = $event">
So just use:
<component :options="options" #update:options="onUpdateOptions">
methods: {
onUpdateOptions(newValue) {
if(this.shouldSync)
this.options = newValue
}
}
or...
<component :options="options" #update:options="options = shouldSync ? $event : options">

Related

How to pass a ref to a child input component with Vue?

I'm currently trying to pass a ref to get the value of the input (base-input component) on submit. You will find below the two components. With the console.log in handleSubmit, email is always undefined.
Thanks in advance for your help.
Parent component
<template>
<form #submit.prevent="handleSubmit">
<div class="flex flex-col mt-10">
<form-label forInput="email" label="Email Address" />
<base-input type="email" name="email" ref="email" />
</div>
</form>
</template>
<script>
import BaseInput from "../UI/BaseInput.vue";
export default {
components: {
BaseInput,
},
methods: {
handleSubmit() {
const email = this.$refs.email.value;
console.log(email);
},
},
};
</script>
Child Input component
<template>
<input
:type="type"
:name="name"
:ref="name"
/>
</template>
<script>
export default {
props: ["type", "name"],
};
</script>
If you want to access the value from the child's input field in the parent component, you need a way for data to flow from child to parent component, and that is done through emits.
But wouldn't it be nice to be able to use v-model with your custom BaseInput component? The same way one would use form input binding?
<input
:value="text"
#input="event => text = event.target.value">
or v-model to simplify
<input v-model="text">
Something like this
<BaseInput v-model="email" />
Well, we can do just that. What you need is a modelValue prop and update:modelValue emit event.
You can wrap both inside a writeable computed property for a cleaner and clearer syntax.
const props = defineProps({
modelValue: {
type: String,
},
});
const emit = defineEmits(['update:modelValue']);
const internalValue = computed({
get() {
return props.modelValue;
},
set(value: string) {
return emit('update:modelValue', value);
},
});
When internalValue is set to a new value, it emits that event to the parent component and it syncs it through props.modelValue. Meaning, you can change props.modelValue in the parent component and the change would be reflected inside your custom component. And vice versa.
I like this approach since it gives you a very natural way of reasoning about your component. Now, this concept isn't restricted to v-model per se, you can use it with any prop you want to synchronize to the parent component. Just use name prop, update:name emit in child component and use v-model:name in parent component.
Resources:
https://vuejs.org/guide/components/events.html#usage-with-v-model
first the BaseInput is spelt wrong in the template.
Instead of
<base-input type="email" name="email" ref="email" />
you need to change it to
<BaseInput :type="'email'" :name="'email'" ref="email" />
A way better approach will be to use #emit()
Child.vue
<template>
<input
:type="type"
:name="name"
#change="$emit('inputContent', Content)"
v-model="Content"
/>
</template>
<script>
export default {
emits: ['inputContent'],
data() {
return {
Content: '',
}
},
props: ["type", "name"],
};
</script>
Don't forget to declare your props as strings. 😉
Parent.vue
<template>
<BaseInput :type="'email'" :name="'email'" ref="email" #inputContent="handleSubmit"/>
</template>
<script>
import BaseInput from "../UI/BaseInput.vue";
export default {
components: {
BaseInput,
},
methods: {
handleSubmit(content) {
const email = content;
console.log(email);
},
},
};
</script>
Here is something about emits in the vue docs
and how to use v-model
I hope this helps :)

vue.js and slot in attribute

I'm trying to build a vue.js template that implements following:
<MyComponent></MyComponent> generates <div class="a"></div>
<MyComponent>b</MyComponent> generates <div class="a" data-text="b"></div>.
Is such a thing possible?
EDIT
Here is the best I can reach:
props: {
text: {
type: [Boolean, String],
default: false
}
},
and template
<template>
<div :class="classes()" :data-text="text">
<slot v-bind:text="text"></slot>
</div>
</template>
but the binding does not work, text always contains false.
You can use the mounted() method to get text using $slot.default property of the component to get the enclosing text. Create a text field in data and update inside mounted() method like this :
Vue.component('mycomponent', {
data: () => ({
text: ""
}),
template: '<div class="a" :data-text=text></div>',
mounted(){
let slot = this.$slots.default[0];
this.text=slot.text;
}
});
Note: It will only work for text, not for Html tags or components.
You're mixing slots and properties here. You'll have to pass whatever you want to end up as your data-text attribute as a prop to your component.
<MyComponent text="'b'"></MyComponent>
And in your template you can remove the slot
<template>
<div :class="classes()" :data-text="text"></div>
</template>
Another thing: it looks like your binding your classes via a method. This could be done via computed properties, take a look if you're not familiar.
You can try this.
<template>
<div :class="classes()">
<slot name="body" v-bind:text="text" v-if="hasDefaultSlot">
</slot>
</div>
</template>
computed: {
hasDefaultSlot() {
console.log(this)
return this.$scopedSlots.hasOwnProperty("body");
},
}
Calling
<MyComponent>
<template v-slot:body="props">
b
</template>
</MyComponent>

How to extend the behaviour of components in Vue

I mostly have experience in React and am wondering what the Vue centric way of doing this would be:
I would like extend this component: https://element.eleme.io/#/en-US/component/form so that label-position is top on mobile and left on desktop. I'm unusure how I would spread the properties passed into the component on to the element.
Here is the pseudo-code I have so far:
<template>
<el-form v-bind="formProps" :label-position="labelPosition">
<slot />
</el-form>
</template>
<script>
import Vue from 'vue';
export default Vue.component('el-form-responsive', {
data() {
return {
labelPosition: 'top',
};
},
created() {
this.mobileQuery = window.matchMedia('(max-width: 720px)');
this.onMobileQueryTrigger(this.mobileQuery);
this.mobileQuery.addListener(this.onMobileQueryTrigger);
},
beforeDestroy() {
this.mobileQuery.removeListener(this.onMobileQueryTrigger);
},
methods: {
onMobileQueryTrigger(query) {
if (query.matches) {
console.log('is mobile');
this.$data.labelPosition = 'top';
} else {
this.$data.labelPosition = 'left';
console.log('is not mobile');
}
},
},
});
</script>
From what I understand v-bind does not copy over events and directives, so this doesn't work:
<el-form-responsive
:formProps="{
class: 'form',
':model': 'formValues',
'status-icon': true,
':rules': 'rules',
ref: 'form',
'label-width': 'auto',
'#submit.native.prevent': 'submitForm'
}"
>
It's also inconvenient and ugly, I would rather just do:
<el-form-responsive
class="form"
:model="formValues"
status-icon
:rules="rules"
ref="form"
label-width="auto"
#submit.native.prevent="submitForm"
>
But I'm unsure how to spread these props on to el-form? Is this not the Vue centric way to go about this? Seems like a fundamental thing so maybe I have it wrong.
I'm not able to test this now, but I believe $attrs and $listeners should do what you want to achieve or at least point you in the right direction:
<template>
<el-form v-bind="$attrs" v-on="$listeners" :label-position="labelPosition">
<slot />
</el-form>
</template>
You made a mistake in passing props like below:
<el-form v-bind="formProps" label-position=":labelPosition">
<slot />
</el-form>
In the above codes, label-position=":labelPosition" is wrong.
It should be:label-position="labelPosition", so;
<el-form v-bind="formProps" :label-position="labelPosition">
<slot />
</el-form>

How to create async dynamic components in Vue

I understand dynamic components, I understand async components, but I have no clue how to combine the two (rhyme unintended).
How can I rewrite
<template>
<component :is="name" v-if="component" />
</template>
<script>
export default {
name: 'dynamic-async-loader',
props: ['name'],
components: {
compA: () => import('./components/compA'),
compB: () => import('./components/compB'),
compC: () => import('./components/compC'),
},
};
</script>
to be able to omit the components object and instead pass the path as a prop? e.g.
<template>
<component :is="name" v-if="component" />
</template>
<script>
export default {
name: 'dynamic-async-loader',
props: ['name', 'path'],
// magic
};
</script>
So I'll be able to do the following
<dynamic-async-loader :name="'compA'" :path="'./components/compA'" />
<dynamic-async-loader :name="'compB'" :path="'./components/compB'" />
<dynamic-async-loader :name="'compC'" :path="'./components/compC'" />
Can someone explain me the magic?

How pass a v-model on the parent into a template

I'm trying to compose the UI for a search page, but wanna use components to reuse code. However, I need a way to pass the model of the page to the search component, and don't see how:
In index.html:
<template id="search">
<q-search inverted placeholder="Look" float-label="Search" v-model="search" /> <-- BIND HERE
</template>
<template id="ListCustomersPage">
<q-layout>
<q-layout-header>
<search v-model="search"></search> <-- HOW PASS INTO THIS
</q-layout-header>
</q-layout>
</template>
And the code:
const search = {
template: '#search',
props: ['search']
};
const ListCustomersPage = {
key: 'ListCustomersPage',
template: '#ListCustomersPage',
components: { search },
data() {
return {
title: 'Select Customer',
search:'' <-- FROM THIS TO 'BIND HERE'
}
}
};
I'm not sure if I'm 100% following what you're asking, but it seems like you just want to pass a property to a child component?
<search :search="search"></search> <-- HOW PASS THIS
Passing a prop to a child is done with v-bind or the colon short hand for it.
<child-component :property="parent_data"></child-component>
<child-component v-bind:property="parent_data"></child-component>
See the documentation here.

Categories

Resources