Vue 3: child to parent and parent to child design pattern - javascript

I am new to the Vue 3 ecosystem. I am building a search form using the composition API.
I have a child component that contains a search form input. It emits a doEmitSearch event, and has a payload of the searchterm.
In the parent component I receive the emitted event #doEmitSearch=”doTriggerSearch”
In the parent component I have
<script lang=”ts” setup>
import {doPerformSearch} from "../composables/doPerformSearch"
function doTriggerSearch (value){
return doPerformSearch(value)
}
<script/>
Inside the doPerformSearch.ts I have various functions Search1(value), Search2(value), Search3(value) etc. that do API calls for multiple API searches, and data cleaning etc, and each one returns search results as JSON, which I want to dispatch/pass/display in either in the parent component or other child-components as props.
What syntax in the composition API can I use to display the returned doPerformSearch(value) in the parent component as {{searchResults}}
What syntax can I use to pass and display multiple search results to child components ?
Is that a good design pattern I'm using, or is there better ways to do it ?
Thank you

I would suggest you to start with Vue 3 Docs Components Basics. Be sure to switch the "API Preference" to Composition API.
I did a very basic concept of your task. Check the code below.
You can make your components statefull, like searchInput or stateless, like searchHistory. It's up to you, where you store your data in the app.
Important is to understand the data flow with Vue components. Usually, components become data over binding and respond with events.
const { ref, createApp } = Vue;
var searchInput = {
emits: ['search'],
data() {
return {
searchValue: ''
}
},
template: `
<label>Search:</label>
<input type="text" :value="searchValue"
#input="searchValue = $event.target.value" />
<button type="button" #click="$emit('search', searchValue)">enter</button>`
};
var searchHistory = {
props: {
history: {
type: Array,
required:true,
default:[]
}
},
template: `
<label>Search History:</label>
<ul><li v-for="item in history">{{item}}</li></ul>`
};
const app = createApp({
productionTip: false,
components: {
searchInput,
searchHistory
},
data() {
return {
searchHistory: []
}
},
methods: {
doSearch(value) {
if (value != '' && this.searchHistory.indexOf(value) == -1) this.searchHistory.push(value);
}
}
});
app.mount('#app')
<div id="app">
<search-input #search="doSearch"></search-input><hr>
<search-history :history="searchHistory"></search-history>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>

If this can help anyone: here is the answer to 1)
<script lang=”ts” setup>
import {doPerformSearch} from "../composables/doPerformSearch"
let searchResults = ref(0);
async function doTriggerSearch(value){
return searchResults.value = await doPerformSearch(value);
}
<script/>
and in the template I have {{searchResults}}

Related

How to inject a mock Pinia store to a Vue component based on routing

I am using Vue together with Pinia stores and Vue-router.
I have a component Projects.vue that uses a project.store.ts to display some data on the screen when a user goes to someSite.com/projects:
<!-- Projects.vue -->
<template>
<div v-each="project in projects">
{{project.name}}
</div>
</template>
<script setup lang="ts">
import { useProjectStore } from "../stores/project.store";
const projectStore = useProjectStore();
const { projects } = storeToRefs(projectStore);
projectStore.getAll();
</script>
<!-- project.store.ts -->
export const useProjectStore = defineStore({
id: "project",
state: () =>
({
projects: [],
} as ProjectStoreState),
actions: {
async getAll() {
this.projects = await fetch.get(`${baseUrl}`);
}
},
});
What I want to do is to introduce Demo functionality, where if a user goes to someSite.com/demo/projects, it's served the same Projects.vue component, however with static data that I've defined in the code.
I was thinking to create a project.store.mock.ts, which follows the same interface just does
actions: {
async getAll() {
this.projects = [{name: "project1"}]
}
},
However, I am struggling to find a way to inject a different store based on the route. This might of course be a completely wrong way to approach it, but I would like to keep the same component, just present different data (data from different sources) based on routing

Composition API with Nuxt 2 to get template refs array

I'm trying to get the array of element refs that are not in v-for. I'm using #nuxtjs/composition-api on Nuxt 2.
(Truth: I want to make an array of input elements, so that I can perform validations on them before submit)
This sounds too easy on vue 2 as $refs becomes an array when one or more compnents have the same ref name on html. However, this doesn't sound simple with composition api and trying to perform simple task with that got me stuck from long.
So to handle this scenario, I've created 1 composable function. (Soruce: https://v3-migration.vuejs.org/breaking-changes/array-refs.html#frontmatter-title)
// file: viewRefs.js
import { onBeforeUpdate, onUpdated } from '#nuxtjs/composition-api'
export default () => {
let itemRefs = []
const setItemRef = el => {
console.log('adding item ref')
if (el) {
itemRefs.push(el)
}
}
onBeforeUpdate(() => {
itemRefs = []
})
onUpdated(() => {
console.log(itemRefs)
})
return {
itemRefs,
setItemRef
}
}
Here is my vue file:
<template>
<div>
<input :ref="input.setItemRef" />
<input :ref="input.setItemRef" />
<input :ref="input.setItemRef" />
<input :ref="input.setItemRef" />
<input :ref="input.setItemRef" />
<input :ref="input.setItemRef" />
// rest of my cool html
</div>
</template>
<script>
import {
defineComponent,
reactive,
useRouter,
ref
} from '#nuxtjs/composition-api'
import viewRefs from '~/composables/viewRefs'
export default defineComponent({
setup() {
const input = viewRefs()
// awesome vue code here...
return {
input
}
}
})
</script>
Now when I run this file, I don't see any adding item ref logs. And on click of a button, I'm logging input. That has 0 items in the itemRefs array.
What's going wrong?
Nuxt 2 is based on Vue 2, which only accepts strings for the ref attribute. The docs you linked actually refer to new behavior in Vue 3 for ref, where functions are also accepted.
Template refs in Nuxt 2 work the same way as they do in Vue 2 with Composition API: When a ref is inside a v-for, the ref becomes an array:
<template>
<div id="app">
<button #click="logRefs">Log refs</button>
<input v-for="i in 4" :key="i" ref="itemRef" />
</div>
</template>
<script>
import { ref } from '#vue/composition-api'
export default {
setup() {
const itemRef = ref(null)
return {
itemRef,
logRefs() {
console.log(itemRef.value) // => array of inputs
},
}
}
}
</script>
demo
And setup() does not provide access to $refs, as template refs must be explicitly declared as reactive refs in Composition API.

Get multiple components instead of one. They are rendered from the list. vue.js

I have a list of instruments that should render a c-input with autosuggest window when the user types something. Also, I need an option for c-input to add or remove autosuggest component.
/* instrument component */
<template>
<c-input ref="input"
:values="inputValue"
:placeholder="placeholder"
#input="onInput"
#change="onChangeInput"
#reset="reset" />
<autosuggest
v-if="showSuggests"
:inputValue="inputValue"
:suggests="suggests"
#onSelectRic="selectRicFromList"
></autosuggest>
</div>
</template>
<script>
export default {
name: 'instrument',
data: () => ({
suggests: [],
inputValue: '',
}),
computed: {
showSuggests() {
return this.isNeedAutosuggest && this.showList;
},
showList() {
return this.$store.state.autosuggest.show;
},
isloading() {
return this.$store.state.instruments.showLoading;
},
defaultValue() {
if (this.instrument.name) {
return this.instrument.name;
}
return '';
},
},
[...]
};
</script>
This is a parent component:
<template>
<div>
<instrument v-for="(instrument, index) in instruments"
:key="instrument.name"
:instrument="instrument"
:placeholder="$t('change_instrument')"
:isNeedAutosuggest="true" /> <!--that flag should manage an autosuggest option-->
<instrument v-if="instruments.length < maxInstruments"
ref="newInstrument"
:isNeedAutosuggest="true" <!-- here too -->
:placeholder="$t('instrument-panel.ADD_INSTRUMENT')" />
</div>
</template>
The main issues are I have so many autosuggests in DOM as I have instruments. In other words, there is should be 1 autosuggest component when the option is true. Moving autosuggest to the parent level is not good because of flexibility and a lot of logically connected with c-input.
Have you any ideas to do it?
[UPDATE]
Here is how I've solve this;
I created an another component that wraps input and autosuggest components. If I need need an input with autosuggest I will use this one, either I will use a simple input.
/* wrapper.vue - inserted into the Instrument.vue*/
<template>
<span>
<fc-input ref="input"
:values="value"
:placeholder="placeholder"
:isloading="isloading"
#input="onInput"
#changeInput="$emit('change', $event)"
#resetInput="onResetInput" />
<fc-autosuggest
v-if="isSuggestsExist"
:suggests="suggests"
/>
</span>
</template>
You can do it if you create a function inside each instrument component, which will call the parent component and search the first component instrument to find autosuggest. Function will be like that:
name: 'instrument',
...
computed: {
autosuggestComponent () {
// this is a pseudo code
const parentChildrenComponents = this.$parent.children();
const firstChild = parentChildrenComponents[0];
const autosuggestEl = firstChild.$el.getElementsByTagName('autosuggest')[0];
// return Vue component
return autosuggestEl.__vue__;
}
},
methods: {
useAutosuggestComponent () {
this.autosuggestComponent.inputValue = this.inputValue;
this.autosuggestComponent.suggests = [{...}];
}
}
This solution is not so beautiful, but it allows to keep the logic inside the instrument component.
But my advice is create some parent component which will contain instrument components and I suggest to work with autosuggest through the parent. You can create autosuggest component in the parent and pass it to the children instruments. And if instrument doesn't receive a link to a autosuggest (in props), than it will create autosuggest inside itself. It will allow to use instrument for different conditions.
Let me know if I need to explain my idea carefully.

vue js - can't find child component template

After reading the vue.js docs I just jumped into components.
I want to create a custom (local) input component that emits an event to the parent on keyup, but I have two problems. (see code example at the end of the post)
[solved] 1. I already get an error when I register the child component that says
[Vue warn]: Failed to mount component: template or render function not defined.
found in
---> <InputTest>
<Root>
I guess it's a complete no-brainer, but I just don't get it.
[solved] 2. The child event doesn't even fire
Before abstracting and simplyfing the code for this question I tried to create the same behaviour with single-file (.vue) components. With SFCs the template compiles / mounts successfully, but the child component events doesn't fire. Obviously I can not tell for sure if this problem will occur in my provided example as well, but I'd guess so.
EDIT 1: Solved problem 1
My child-component should be an object instead of a vue instance. I updated the code for that. I also changed the onChange method from lambda to function, as this doesn't point to the vue instance in a lambda.
EDIT 2: Solved problem 2
There may be times when you want to listen for a native event on the root element of a component.
Apparently the native modifier can only be used on components and not on native elements. Removing the modifier fixed the problem. I changed the code accordingly.
CODE
const inputText = {
data () {
return {
model: ''
}
},
template: '<input type="text" v-model="model" #keyup="onChange">',
methods: {
onChange: function () {
this.$emit('update', this.model);
}
}
};
const app = new Vue({
el: '#app',
data () {
return {
txt: ''
}
},
methods: {
onUpdate: function(txt) {
this.txt = txt;
}
},
components: {
'input-text': inputText
}
});
<script src="https://unpkg.com/vue#2.5.13/dist/vue.js"></script>
<div id="app">
<input-text #update="onUpdate"></input-text><br>
{{ txt }}
</div>
You don't need two vue instances. You can create a component as a simple object and use it in your vue instance
const inputText = {
template: '<div> <input type="text" #keyup.native="onChange"> </div>',
methods: {
onChange: () => {
console.log('onChange');
this.$emit('update')
}
}
}
const app = new Vue({
el: '#app',
template: '<input-test #keyup.native="onKeyup" #update="onUpdate"></input-test>',
methods: {
onUpdate: () => console.log('onUpdate'),
onKeyup: () => console.log('onKeyup')
},
components: {
'input-test': inputText
}
});
<script src="https://unpkg.com/vue#2.5.13/dist/vue.js"></script>
<div id="app"></div>

Call a Vue.js component method from outside the component

Let's say I have a main Vue instance that has child components. Is there a way of calling a method belonging to one of these components from outside the Vue instance entirely?
Here is an example:
var vm = new Vue({
el: '#app',
components: {
'my-component': {
template: '#my-template',
data: function() {
return {
count: 1,
};
},
methods: {
increaseCount: function() {
this.count++;
}
}
},
}
});
$('#external-button').click(function()
{
vm['my-component'].increaseCount(); // This doesn't work
});
<script src="http://vuejs.org/js/vue.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="app">
<my-component></my-component>
<br>
<button id="external-button">External Button</button>
</div>
<template id="my-template">
<div style="border: 1px solid; padding: 5px;">
<p>A counter: {{ count }}</p>
<button #click="increaseCount">Internal Button</button>
</div>
</template>
So when I click the internal button, the increaseCount() method is bound to its click event so it gets called. There is no way to bind the event to the external button, whose click event I am listening for with jQuery, so I'll need some other way to call increaseCount.
EDIT
It seems this works:
vm.$children[0].increaseCount();
However, this is not a good solution because I am referencing the component by its index in the children array, and with many components this is unlikely to stay constant and the code is less readable.
In the end I opted for using Vue's ref directive. This allows a component to be referenced from the parent for direct access.
E.g.
Have a component registered on my parent instance:
var vm = new Vue({
el: '#app',
components: { 'my-component': myComponent }
});
Render the component in template/html with a reference:
<my-component ref="foo"></my-component>
Now, elsewhere I can access the component externally
<script>
vm.$refs.foo.doSomething(); //assuming my component has a doSomething() method
</script>
See this fiddle for an example: https://jsfiddle.net/0zefx8o6/
(old example using Vue 1: https://jsfiddle.net/6v7y6msr/)
Edit for Vue3 - Composition API
The child-component has to return the function in setup you want to use in the parent-component otherwise the function is not available to the parent.
Note: <sript setup> doc is not affacted, because it provides all the functions and variables to the template by default.
You can set ref for child components then in parent can call via $refs:
Add ref to child component:
<my-component ref="childref"></my-component>
Add click event to parent:
<button id="external-button" #click="$refs.childref.increaseCount()">External Button</button>
var vm = new Vue({
el: '#app',
components: {
'my-component': {
template: '#my-template',
data: function() {
return {
count: 1,
};
},
methods: {
increaseCount: function() {
this.count++;
}
}
},
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-component ref="childref"></my-component>
<button id="external-button" #click="$refs.childref.increaseCount()">External Button</button>
</div>
<template id="my-template">
<div style="border: 1px solid; padding: 2px;" ref="childref">
<p>A counter: {{ count }}</p>
<button #click="increaseCount">Internal Button</button>
</div>
</template>
For Vue2 this applies:
var bus = new Vue()
// in component A's method
bus.$emit('id-selected', 1)
// in component B's created hook
bus.$on('id-selected', function (id) {
// ...
})
See here for the Vue docs.
And here is more detail on how to set up this event bus exactly.
If you'd like more info on when to use properties, events and/ or centralized state management see this article.
See below comment of Thomas regarding Vue 3.
You can use Vue event system
vm.$broadcast('event-name', args)
and
vm.$on('event-name', function())
Here is the fiddle:
http://jsfiddle.net/hfalucas/wc1gg5v4/59/
A slightly different (simpler) version of the accepted answer:
Have a component registered on the parent instance:
export default {
components: { 'my-component': myComponent }
}
Render the component in template/html with a reference:
<my-component ref="foo"></my-component>
Access the component method:
<script>
this.$refs.foo.doSomething();
</script>
Say you have a child_method() in the child component:
export default {
methods: {
child_method () {
console.log('I got clicked')
}
}
}
Now you want to execute the child_method from parent component:
<template>
<div>
<button #click="exec">Execute child component</button>
<child-cmp ref="child"></child_cmp> <!-- note the ref="child" here -->
</div>
</template>
export default {
methods: {
exec () { //accessing the child component instance through $refs
this.$refs.child.child_method() //execute the method belongs to the child component
}
}
}
If you want to execute a parent component method from child component:
this.$parent.name_of_method()
NOTE: It is not recommended to access the child and parent component like this.
Instead as best practice use Props & Events for parent-child communication.
If you want communication between components surely use vuex or event bus
Please read this very helpful article
This is a simple way to access a component's methods from other component
// This is external shared (reusable) component, so you can call its methods from other components
export default {
name: 'SharedBase',
methods: {
fetchLocalData: function(module, page){
// .....fetches some data
return { jsonData }
}
}
}
// This is your component where you can call SharedBased component's method(s)
import SharedBase from '[your path to component]';
var sections = [];
export default {
name: 'History',
created: function(){
this.sections = SharedBase.methods['fetchLocalData']('intro', 'history');
}
}
Using Vue 3:
const app = createApp({})
// register an options object
app.component('my-component', {
/* ... */
})
....
// retrieve a registered component
const MyComponent = app.component('my-component')
MyComponent.methods.greet();
https://v3.vuejs.org/api/application-api.html#component
Here is a simple one
this.$children[indexOfComponent].childsMethodName();
I am not sure is it the right way but this one works for me.
First import the component which contains the method you want to call in your component
import myComponent from './MyComponent'
and then call any method of MyCompenent
myComponent.methods.doSomething()
Declare your function in a component like this:
export default {
mounted () {
this.$root.$on('component1', () => {
// do your logic here :D
});
}
};
and call it from any page like this:
this.$root.$emit("component1");
If you're using Vue 3 with <script setup> sugar, note that internal bindings of a component are closed (not visible from outside the component) and you must use defineExpose(see docs) to make them visible from outside. Something like this:
<script setup lang="ts">
const method1 = () => { ... };
const method2 = () => { ... };
defineExpose({
method1,
method2,
});
</script>
Since
Components using are closed by default
Sometimes you want to keep these things contained within your component. Depending on DOM state (the elements you're listening on must exist in DOM when your Vue component is instantiated), you can listen to events on elements outside of your component from within your Vue component. Let's say there is an element outside of your component, and when the user clicks it, you want your component to respond.
In html you have:
Launch the component
...
<my-component></my-component>
In your Vue component:
methods() {
doSomething() {
// do something
}
},
created() {
document.getElementById('outsideLink').addEventListener('click', evt =>
{
this.doSomething();
});
}
I have used a very simple solution. I have included a HTML element, that calls the method, in my Vue Component that I select, using Vanilla JS, and I trigger click!
In the Vue Component, I have included something like the following:
<span data-id="btnReload" #click="fetchTaskList()"><i class="fa fa-refresh"></i></span>
That I use using Vanilla JS:
const btnReload = document.querySelector('[data-id="btnReload"]');
btnReload.click();

Categories