How to make a pulldown menu that acts on a selected element? - vue.js

TL;TR
How do I:
getMyComponent(selectedMyComponentID).complexOperation()
To me this sounds like such a trivial and useful thing, e.g. from a pulldown menu.
A little more detail
Assume I'm making an editor of some kind (think todo-list or something). The GUI has the concept of a "selected" element. (In our real case it is the currently visible bootstrap nav-tab), and I want to have pull-down menus with menu-items that perform different operations on the selected element.
Assuming I have the id of the "selected" component, getting a reference to the MyComponent that has a complexOperation() method corresponding to the id is surprisingly difficult.
Perhaps that is because I'm not doing this "the Vue way".
I see these ways to accomplish complexOperationOnSelectedMyComponent():
use refs - seems messy and ugly
refactor the complexOperation() out of MyComponent and into a new MyData, so the business logic on the data is used by oth App.vue and MyComponent.vue. Now the parent is changing data and therefore props - sounds vue-ish. But that leads to lots of boilerplate since every operation in every component now has two versions. I'm not a fan of boilerplate and duplication...
use vuex? I think I'm not there yet...
Use an "event bus" Vue instance and $emit events from parent to child. Seems overkill. And is messy and has boilerplate.
Am I missing something? Isn't this pretty standard stuff?
Details
For simplicity, we'll say that there is a template:
<template>
<div id="app">
<div v-for="elem in mydata" :key="elem.id" #click="setSelected(elem)">
<MyComponent :value="elem"/>
</div>
<button #click="complexOperationOnSelectedComponent">
Complex operation on Selected Component
</button>
</div>
</template>
and a data structure where the first one is initially selected:
data() {
return {
mydata: [
{ id: 0, foo: "bar", selected: true },
{ id: 1, foo: "baz", selected: false },
{ id: 2, foo: "world", selected: false }
]
};
}
(Complete codesandbox)
So there is a "Complex operation on Selected Component" button. But what should I put in the complexOperationOnSelectedComponent method?
The codesandbox above also has equivalent buttons inside each MyComponent. They simply call a complexOperation() method in the MyComponent definition.
I'm thinking that whether the button happens to be inside or outside the component is a minor detail. Get a reference to the MyComponent for the selected id and call selectedComponent.complexOperation() in the menu item's #click handler.
In our real scenario, the user selects the "component" by clicking on a nav-bar, (not on the MyComponent instance), so what we have is a id (mydata[n].id or 0, 1 or 2 above).
Using ref-s
What I could do was put ref="components" in the <MyComponents> definition. Because it is in a v-for loop, this.$refs.components will then be an array of MyComponents. Find the one with the right id and use it.
Because there is no guarantee about the order of in this.$refs.components I'd have to search the array for the selectedMyComponentID every time, but hey...
Is this really the best solution?

You typically want to use $refs if you need access to the DOM directly to get to elements that are not connected/controlled to any Vue instance properties.
You can store which element is selected in data and target that specific element.
Putting your method in the parent or the child? It depends, do you need the logic in other places? In this case, I think it makes sense in the child because you want to be able to perform actions there as well.
Use an "event bus" Vue instance and $emit events from parent to child. Seems overkill.
Props go from parent to child. Events are emitted from child to parent.
Vuex and event busses are really helpful in larger applications but really not needed in this case. You should, however, emit changes that you want to make to your props and not edit them directly like you are doing now in MyComponent.
I refactored your code a bit, I do want to repeat that modifying the values of props directly is bad practice: https://codesandbox.io/s/button-onclick-on-selected-child-lf37c?fontsize=14
<template>
<div id="app">
<div v-for="elem in mydata" :key="elem.id" #click="selectedElem = mydata[elem.id]">
<MyComponent :value="elem" :reverse="elem.reverse"/>
</div>
<button #click="reverseSelectedFoo">Reverse Selected Foo</button>
</div>
</template>
<script>
import MyComponent from "./components/MyComponent";
export default {
name: "App",
data() {
// Ideally this data is read from an API somewhere
return {
mydata: [
{ id: 0, foo: "bar", reverse: false },
{ id: 1, foo: "baz", reverse: false },
{ id: 2, foo: "world", reverse: false }
],
selectedElem: null
};
},
methods: {
reverseSelectedFoo() {
this.selectedElem.reverse = !this.selectedElem.reverse;
}
},
components: {
MyComponent
}
};
</script>

Related

Why aren't there props for child -> parent and $emits for parent -> child?

I have a page with a component and the page needs to access a variable in that component. Would be nice if it were reactive. Then from the page I need to activate a function in the component. Would be nice if it could be done without a reactive variable. My question is 1: what's the best way to activate the function from the parent, for example when I click a button and 2: it seems very unintuitive and random to me that they aren't both possible in both directions? Anyone maybe know how Vue suggest you do it? This whole thing seems so complex relative to the relatively simple thing I'm trying to do.
I guess I try to use props? Or are refs a better option here?
So in general: you use refs, if you need the dom element, that's the whole purpose of refs. Since you don't mention that you n ed the dom element, you don't need to use that here.
There are 3 ways of communication: parent to child via props: https://vuejs.org/guide/components/props.html
child to parent via events
https://vuejs.org/guide/components/events.html
and anyone to anyone via event bus, which need an extra lib in vue3 and is out of scope for your question
https://v3-migration.vuejs.org/breaking-changes/events-api.html#event-bus
If you want to execute a function in the component whenever the value changes, you can put a watcher on the prop.
The other way around, from child to parent, you just create a listener to your emitted event and invoke a function of your choice. There are good examples in the docs in my opinion.
As per my understanding, You want to trigger the child component method from the Parent component without passing any prop as a input parameter and in same way you want to access child component data in the parent component without $emit. If Yes, You can simply achieve this using $refs.
You can attach the ref to the child component and then access it's variables and methods with the help of this $refs.
Live Demo (Just for a demo purpose I am using Vue 2, You can make the changes as per Vue 3) :
Vue.component('child', {
data: {
childVar: ''
},
methods: {
triggerEventInChildFromParent() {
console.log('Child Function triggered from Parent');
}
},
mounted() {
this.childVar = 'Child component variable'
}
});
var app = new Vue({
el: '#app',
methods: {
triggerEventInChild() {
this.$refs.child.triggerEventInChildFromParent()
}
},
mounted() {
console.log(this.$refs.child.childVar)
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button #click="triggerEventInChild">
Button in Parent
</button>
<child ref="child"></child>
</div>

Passing parent vm as a prop to a component

So I’m building a Nuxt app for working with docs (in a broad sense), and it will have a menu, which I will obviously make a component. The menu will be home to lots of actions on the doc itself, such as opening/saving files, editing, etc. etc.
I know the standard way to pass info from a component to its parent (the doc vm in this case) is via messages, but it feels like a bit of an overkill, what with the syntax (emit handlers just don’t feel natural to me in this case) and whatnot.
For this reason I was wondering why can’t I just pass the parent vm as a prop to the menu component? It will contain all kinds of methods, and I will be able to easily invoke them via the menu. Something like:
Parent (Document.vue):
<template>
<main-menu :document='vm'/>
</template>
<script>
import MainMenu from '~/components/MainMenu.vue'
export default {
data(): {
return {
vm: this,
//...
}
},
methods: {
save() {
//...
}
}
//...
</script>
Menu component (MainMenu.vue):
<template>
<button #click='document.save()'>Save document</button>
</template>
<script>
export default {
props = ['document']
}
</script>
The question: Is there something intrinsically bad in this approach?
(I imagine this could be problematic if the app architecture could change, but it’s hard to imagine that I would for some reason need a menu without an underlying document.)
IF your Menu is always the child of the component, then you don't have to pass your parent. It is already held in a Vue variable called this.$parent.
I made a little sandbox to give you an example.
The parent has a function, for example:
/// PARENT
export default {
name: "App",
components: {
HelloWorld,
},
methods: {
iExist(add) {
console.log("I am in parent" + add);
},
},
};
Then you can call it from child with this.$parent.iExist('something').
Since this.$parent is not defined when the template is being evaluated, we have to make a method in the child as well, to call(super) the corresponding function on it's parent.
/// CHILD
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<button #click="iExist(', but was called from child')">Click Me</button>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
methods: {
iExist(add) {
this.$parent.iExist(add);
},
},
};
</script>
The question: Is there something intrinsically bad in this approach?
(I imagine this could be problematic if the app architecture could change, but it’s hard to imagine that I would for some reason need a menu without an underlying document.)
Yes, this is bad design. Parents can be aware of children, children shouldn't be aware of parents. A child could be tested in isolation, or be nested inside wrapper component that doesn't have this method.
As another answer suggests, a way to access a parent is to use $parent property. This part was borrowed in Vue from AngularJS 1.x, accessing it was considered a bad practice even then.
This is generally achieved by providing a callback from a parent that does exactly a desired thing, without allowing to access the whole instance and break the encapsulation. It's unnecessary to explicitly define callback function in Vue because this is naturally provided by Vue template syntax:
In a parent:
<child #save="save()">
In a child:
<button #click="$emit('save')">
In case of deeply nested components the event can be passed through them to a parent.

Accessing DOM element in Vuejs component not reliable

In the simplified example below I demonstrate my problem:
I have a for-loop that asynchronously updates myItems.
I want to be able and update accordingly selectableItems by using this.$el.querySelector('selectable-item').
<template>
<div>
<p>selectableItems: {{selectableItems}}</p>
<div v-for="item in myItems" class="selectable-item">item</div>
</div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return {
myItems: [],
selectableItems: [],
}
},
created(){
// Populate myItems with a delay
self = this
setTimeout(function() {
self.myItems = [1, 2, 3]
}, 1000);
},
mounted(){
// Fetch some of myItems based on a class
this.selectableItems = this.$el.querySelectorAll('.selectable-item')
},
}
</script>
<style scoped lang="scss">
</style>
I've tried many different things I've found online; TickNext, computed, updated, etc. I think I'm doing something fundamentally wrong. But it is important for my case to be able and select DOM elements by a class.
Any help is deeply appreciated.
Updated: More context
Some people asked me for the bigger picture so I give a bit more info here.
Currently I have a big Vue component where the user is able to select elements. I am trying to factor out all this user interaction into a mixin so I can re-use it in other places of my code.
To make re-usability easy I need to be able and simply add a class selectable to any HTML tag in the template. That's the interface and then the mixin does all the magic and populates selectedElements depending on user interaction.
That's why it is important to avoid refs, etc. since then too much logic leaks everywhere and beats the purpose of making the mixin re-usable. Unless I'm missing something.
OK, after trying many different things I've managed to solve this by using a non-reactive intermediate variable. This means that I can't use the variable in a template but that is fine.
export default {
..
// NOT reactive so you can't just use it in your templates.
_selectableItems: [],
updated(){
self._selectableItems = self.$el.querySelectorAll('.selectable-item')
},
..
}

add class to a specific parent div when click a component created with v-for

I am new to vuejs, this is what I want to do:
I have a list of components, each in a div. Now if I do something with the component (i.e. click it). I want to add a class to the parent div. This is what I did so far, the code is simplified, just to show what I want to do with a simple case.
my app.vue:
<div class="toggle-box" v-for="(name, index) in names" :class="classActive" :key="index">
<app-comp :myName="name" :myIndex="index" #someEvent="doSomething"></app-counter>
</div>
data() {
classActive: '',
names: ['alpha', 'beta', 'gamma']
},
methods: {
doSomething() {
this.classActive === '' ? this.classActive = 'is-active': this.classActive='';
}
}
the component:
<div>
<button #click="toggle">{{ myName }} - {{ myIndex }}</button>
</div>
props: ['myName', 'myIndex'],
methods: {
toggle() {
this.$emit('someEvent', index);
}
}
what this do: it creates 3 divs with "toggle-box"-class with a button in it which has the label "name - index". When I click a button, it emits the "someEvent"-event with index attached. The parent listens to this event and toggle the class 'is-active' on the div with the 'toggle-box' class. The thing is, right now, when I click a button, it adds the class to all 3 divs. Probably because there is no different between the 3 divs for vuejs. I know I can append the index to the event and call this with $event in the parent, but how do I use this? Or is there perhaps a better way to achieve what I want?
thanks for helping.
There are several different ways to approach this but I think the starting point is to think about how you want to represent this as data rather than how that appears in the UI. So model before view.
Presumably you're going to want to do something with these active items after they've been selected. I'd focus on that rather than the problem of highlighting them. The highlighting would then fall out relatively painlessly.
For the sake of argument, let's assume that an array of active items is a suitable model for what you're trying to achieve. It might not be but it makes for a simple example.
So:
data() {
return {
activeNames: [],
names: ['alpha', 'beta', 'gamma']
}
},
No mention of classes here as we're not worrying about the UI concern, we're trying to model the underlying data.
For the toggle method I'd be more inclined to emit the name than the index, but you're better placed to judge which represents the data better. For my example it'll be name:
methods: {
toggle() {
this.$emit('someEvent', this.myName);
}
}
Then in the parent component we'll add/remove the name from the array when the event is emitted. Other data structures might be better for this, I'll come back to that at the end.
methods: {
doSomething(name) {
if (this.activeNames.includes(name)) {
this.activeNames = this.activeNames.filter(item => item !== name);
} else {
this.activeNames.push(name);
}
}
}
Now we have an array containing the active names we can use that to derive the class for those wrapper divs.
<div
class="toggle-box"
v-for="(name, index) in names"
:class="{'is-active': activeNames.includes(name)}"
:key="index"
>
Done.
As promised, I'll now return to other data structures you could use.
Instead of an array we might use an object with boolean values:
data() {
return {
names: ['alpha', 'beta', 'gamma'],
activeNames: {
alpha: false,
beta: false,
gamma: false
}
}
}
In many ways this is an easier structure to work with for this particular example but we do end up duplicating the names as property keys. If we don't prepopulate it like this we can end up with reactivity problems (though those can be solved using $set).
A further alternative is to use objects to represent the names in the first place:
data() {
return {
names: [
{name: 'alpha', active: false},
{name: 'beta', active: false},
{name: 'gamma', active: false}
]
}
}
Whether this kind of data structure makes sense for your use case I can't really judge.
Update:
Based on what you've said in the comments I'd be inclined to create another component to represent the toggle-box. Each of these can store its own active state rather than trying to hold them all on the parent component. Your v-for would then create instances of this new component directly. Depending on the circumstances it may be that this new component could merged into your original component.
There are all sorts of other considerations here that make it really difficult to give a definitive answer. If the active state needs to be known outside the toggle-box component then it's a very different scenario to if it's just internal state. If only one toggle-box can be open at once (like an accordion) then that's similarly tricky as internal state is not sufficient.

Vue 2 pass props to child [old : "call child's method"]

ok so I've learned that I'm not supposed to call a child's method but pass it props instead.
I've got (parent) :
<template>
<div id="main">
<Header :title ="title"/>
<router-view/>
<LateralMenu/>
</div>
</template>
<script>
export default {
name: 'app'
data: function () {
return {
title: true
}
},
methods: {
hideTitle: function () {
this.title = false
console.log(this.title)
},
showTitle: function () {
this.title = true
console.log(this.title)
}
}
}
</script>
and (child) :
<script>
export default {
name: 'Header',
props: ['title'],
created () {
console.log(this.title)
},
methods: {
}
}
</script>
the first console logs (inside the parent) print correctly on each method but the second console log within the child stays true all the time. I got this from : Pass data from parent to child component in vue.js
inside what method does the console.log need to be to be printed everytime the methods in the parent are triggered?
(this is why I wanted to go for method-calling, originally, by going with variables instead, we're potentially omitting valuable parts of the process such as optimization and a "when" for the execution(s!!) of our code. pontetally being the key word here, don't blow up on me, keep in mind that I'm learning.)
OLD:
I've browsed the web and I know there a a million different answers
and my point is with the latest version of vue none of those millions
of answers work.
either everything is deprecated or it just doesn't apply but I need a
solution.
How do you call a child method?
I have a 1 component = 1 file setup.
DOM is declared inside a <template> tag javascript is written inside
a <script> tag. I'm going off of vue-cli scaffolding.
latest method I've tried is #emit (sometimes paired with an #on
sometimes not) doesn't work :
child :
<script>
export default {
name: 'Header',
created () {
this.$on('hideTitlefinal', this.hideTitlefinal)
},
methods: {
hideTitlefinal: function () {
console.log('hideeeee')
},
showTitlefinal: function () {
console.log('shwowwww')
}
}
}
</script>
parent :
<template>
<div id="main">
<Header v-on:hideTitle="hideTitlefinal" v-on:showTitle="showTitlefinal"/>
<router-view/>
<LateralMenu/>
</div>
</template>
<script>
export default {
methods: {
hideTitle: function () {
this.$emit('hideTitle')
},
showTitle: function () {
this.$emit('showTitle')
}
}
}
</script>
console :
Uncaught TypeError: this.$emit is not a function
at Object.showTitle (Main.vue?1785:74)
at VueComponent.showTitle (LateralMenu.vue?c2ae:113)
at boundFn (vue.esm.js?efeb:186)
at invoker (vue.esm.js?efeb:1943)
at HTMLDivElement.fn._withTask.fn._withTask (vue.esm.js?efeb:1778)
Please don't do this. You're thinking in terms of events. When x happens, do y. That's sooo jquery 2005 man. Vue still has all that stuff, but we're being invited to think in terms of a view model...
You want your state in a variable, in window scope, and you want reactive pipes linking your vue stuff to your state object. To toggle visibility, use a dynamic class binding, or v-if. Then think about how to represent your state. It could be as simple as having a property like store.titleVisible. But, you want to 'normalize' your store, and avoid relationships between items of state. So if title visibility really depends on something higher up, like an editMode or something, then just put the higher-up thing in the store, then create computed properties if you need them.
The goal is that you don't care when things happen. You just define the relationships between the markup and the store, then let Vue take care of it. The docs will tell you to use props for parent=>child and $emit for child=>parent communication. Truth is you don't need this until you have multiple instances of a component, or reusable components. Vue stuff talks to a store, not to other vue stuff. For single-use components, as for your root Vue, just use the data:.
Whenever you find yourself writing show/hide methods, you're doing it wrong. It's intuitive (because it's procedural), but you'll quickly appreciate how much better the MVVM approach is.