bind click event to child component using v-bind - vue.js

I created a simple Minesweeper game and when it comes to the decision, which cell to render there are three possibilities:
Unrevealed cell
Revealed mine cell
Revealed neutral cell
I created a row component that renders all the cells contained by the row.
<template>
<div>
<component
v-for="(cell, columnIndex) in row"
:key="columnIndex"
v-bind="getCellProps(cell, columnIndex)"
:is="getComponentCell(cell)"
/>
</div>
</template>
<script>
// imports here
export default {
components: {
UnrevealedCell,
RevealedNeutralCell,
RevealedMineCell
},
props: {
row: Array,
rowIndex: Number
},
methods: {
getCellProps: function(cell, columnIndex) {
if(cell.revealed) {
if (cell.isMine) {
return {};
} else {
return {
mineNeighbours: cell.mineNeighbours
};
}
} else {
return {
unrevealedCell: cell,
x: columnIndex,
y: this.rowIndex,
cellClicked: this.onCellClicked
};
}
},
getComponentCell: function(cell) {
if(cell.revealed) {
if (cell.isMine) {
return RevealedMineCell;
} else {
return RevealedNeutralCell;
}
} else {
return UnrevealedCell;
}
},
onCellClicked: function(x, y) {
debugger;
}
}
}
</script>
Unfortunately my cellClicked event is not working. The child component is able to emit the event correctly but my onCellClicked doesn't get executed. I think this is because I can't write
cellClicked: this.onCellClicked
as it would normally be
#cellClicked
Without the # the attribute might get added as a component property. How can I fix this to listen to the emitted cellClicked event?

A few thoughts occur.
Firstly, the reason this isn't working is because v-bind is used to set component props and element attributes. The # prefix is a shorthand for v-on, so it isn't a prop or attribute in this sense, it's a directive in its own right. v-on does support an object version, just like v-bind, so you can do something like v-on="getCellEvents(cell, columnIndex)" and return a suitable object for each cell type. This is probably the cleanest direct answer to your original question. Less clean and less direct answers are also available...
You could implement this by making cellClicked a prop of the child cell and then calling it as a callback function rather than emitting an event. Not saying you should, but you could. That would work with the code you posted above completely unchanged.
Another alternative is just to add the event listener for all cells. Include #cellClicked="onCellCicked" in the template without worrying about the cell type. If the other cell types don't emit that event then nothing will happen. Vue doesn't know what events a component can fire, you can listen for anything.
Further thoughts...
Your cell template is a bit anaemic. I know people generally advise keeping logic out of the template but in your case I'd say you've probably taken it too far and it just makes things harder to understand. There are two ways you could address this:
Rewrite your component to use a render function instead. Templates exist because humans find them easier to read than render functions but in your case you've got all the logic in JavaScript anyway. The template isn't really adding anything and going all-in with a render function would probably be easier to understand than what you have currently.
Move the logic into the template. I don't see any obvious reason not to do it that way from the code you've posted. I'll post an example at the end.
Either of these two approaches would remove the problem you had adding an event listener.
A final thought on the click events is that you could use event propagation to handle them instead. Add a single click listener on a suitable element of the surrounding component and don't listen for events on the cells/rows at all. The single listener could then establish which cell was clicked (potentially fiddly) and whether anything needs to be done about it. While this would increase the coupling between the components I would imagine that it wouldn't really matter as these components aren't really reusable elsewhere anyway. I'm not recommending this as an approach at this stage but it is worth keeping in mind whenever you find yourself creating large numbers of repetitive components that all need the same events. In your scenario it would probably only make sense if you start to run into performance problems, and even then there will likely be better ways to fix such problems.
So, I promised an example of the template approach:
<template>
<div>
<template v-for="(cell, columnIndex) in row">
<unrevealed-cell
v-if="!cell.revealed"
:key="columnIndex"
:unrevealed-cell="cell"
:x="columnIndex"
:y="rowIndex"
#cellClicked="onCellClicked"
/>
<revealed-mine-cell
v-else-if="cell.mine"
/>
<revealed-neutral-cell
v-else
:mineNeighbours="cell.mineNeighbours"
/>
</template>
</div>
</template>
I'm not sure why the UnrevealedCell needs the x and y but if it's just so that it can emit them as part of the event then you might want to consider registering the listener as #cellClicked="onCellClicked(columnIndex, rowIndex)" and then there's no need to emit the co-ordinates from the cell. I also wonder whether you need 3 separate components for these cells. My gut reaction is that one component would be more appropriate with the row component not needing to have any understanding of the individual cells at all.

Related

Vue3 child component does not recreating, why?

I have made some sandbox code of my problem here:
https://codesandbox.io/s/clever-zeh-kdff1z
<template>
<div v-if="started">
<HelloWorld :msg="msg" #exit="exit" #remake="remake" />
</div>
<button v-if="!started" #click="started = !started">start</button>
</template>
<script>
import HelloWorldVue from "./components/HelloWorld.vue";
export default {
name: "App",
components: {
HelloWorld: HelloWorldVue,
},
data() {
return {
started: false,
msg: "Hello Vue 3 in CodeSandbox!",
};
},
methods: {
exit() {
this.started = false;
},
remake() {
this.msg = this.msg + 1;
//this code should recreate our child but...
this.exit();
this.started = true;
// setTimeout(() => {
// this.started = true;
// });
},
},
};
</script>
So! We have 2 components parent and child. The idea is simple - we have a flag variable in our parent. We have a v-if statement for this - hide / show an element depend on the flag value "false" or "true". After we toggle the flag - the child component should be recreated. This is the idea. Simple.
In our parent we have a button which will set the flag variable to "true" and our child will be created and will appear on our page.
Ok. Now we have 2 buttons inside our child.
One button is "exit" which is emit an event so the flag variable of parent will set to "false" and the elemint will disappear from our page(It will be destroyed btw). Works as charm. Ok.
The second button "remake". It emit event so the flag variable will be just toggled (off then on). Simple. We set to "false", we set to "true". So the current child should dissapear, and then imediatly will be created new one.
But here we are facing the problem! Ok, current child is still here, there is no any recreation, it just updates current one... So in child I have checked our lifecycle hooks - created and unmounted via console.log function. And the second button dont trigger them. Start->Exit->Start != Start->Remake.
So can anyone please explain me why this is happening? I cant figure it out.
Interesting thing, if you can see there is some asynchronous code commented in my demo. If we set our flag to "true" inside the async function the child will be recreated and we will see the created hook message but it seems like crutch. We also can add a :key to our component and update it to force rerender, but it also seems like a crutch.
Any explanations on this topic how things work would be nice.
Vue re-uses elements and components whenever it can. It will also only rerender once per tick. The length of a 'tick' is not something you should worry yourself about too much, other than that it exists. In your case the this.exit() and this.started = true statements are executed within the same tick. The data stored in this.started is both true in the last tick and the current tick as it does not end the tick in between the statements, and so nothing happens to your component.
In general you should think in states in Vue rather than in lifecycles. Or in other words: What are the different situations this component must be able to handle and how do you switch between those states. Rather than determining what to do in which point in time. Using :key="keyName" is indeed generally a crutch, as is using import { nextTick } from 'vue'; and using that to get some cadence of states to happen, as is using a setTimeout to get some code to execute after the current tick. The nasty part of setTimeout is also that it can execute code on a component that is already destroyed. It can sometimes help with animations though.
In my experience when people try to use lifecycle hooks they would rather have something happen when one of the props change. For example when a prop id on the child component changes you want to load data from the api to populate some fields. To get this to work use an immediate watcher instead:
watch: {
id: {
handler(newId, oldId) {
this.populateFromApi(newId);
},
immediate: true
}
}
Now it will call the watcher on component creation, and call it afterwards when you pass a different id. It will also help you gracefully handle cases where the component is created with a undefined or null value in one of the props you expect. Instead of throwing an error you just render nothing until the prop is valid.

Vue mutate prop correctly

I'm trying to create a simple component whose focus is to display an element in an array, but I'm having issues with Vue's philosophy.
As you may know, if a mutation on a prop is triggered, Vue goes crazy because it doesn't want you to update the value of a prop. You should probably use a store, or emit an event.
The issue is: that since I'm adding functionalities to my codebase (for instance the possibility to start again when I reach the last element of the array), it would be wrong to have an upper component be responsible for this management, as it would be wrong to ask an upper component to change their variable, given that my component is supposed to manage the array, so an emit would be a bad solution.
In the same way, given that I'm making a generic component that can be used multiple times on a page, it would be incorrect to bind it to a store.
EDIT: the reason why the prop needs to be updated is that the component is basically acting as a <select>
Am I missing an obvious way to set this up?
To give an example of my end goal, I'm aiming for a component looking like the one in the picture below, and I think a 2 way bind like in v-model would be more appropriate than having to set an #change just to say to update the value of the passed prop.
If you have a prop the correct way to update the value is with a sync, as in the following example
Parent:
<my-component :title.sync="myTitle"></my-component>
Child:
this.$emit("update:title", this.newValue)
Here is a very good article talking about the sync method.
By the other hand you can alter a Vuex state variable by calling a Vuex mutation when you change the value:
computed: {
title: {
// getter
get() {
return this.$store.state.title
},
// setter
set(newValue) {
this.setTitle(newValue) // Requires mutation import, see the methods section.
// Or without import:
this.$store.commit('setTitle', newValue);
}
}
},
methods: {
...mapMutations("global", ["setTitle"]) // It is important to import the mutation called in the computed section
}
In this StackOverflow question they talk about changing state from computed hook in Vue. I hope it works for you.

Can anyone tell how to speed up my vuejs app?

I am a noob in vuejs. This piece of my code is making my app very slow.
<div v-for="(attribute, i) in attributes" :key="i">
<div>{{ AttributeClicked(attribute) }}</div>
</div>
This is the function:
AttributeClicked(attribute) {
this.$store.commit("entities/Attribute/select", attribute.id);
}
This is the mutation:
mutations: {
select(state, id) {
let selection = Attribute.find(id);
Attribute.update({
where: (a) => a.selected,
data: {
selected: false
}
});
if (selection !== null) {
Attribute.update({
where: id,
data: {
selected: true
}
})
}
},
}
The purpose of this code is to make a webpage like this one https://www.tesla.com/models/design#overview
My objective is for example to show the 5 options below Paint attribute when the page loads.
Can anyone tell me how to speed up this app?
You might need to provide more code or info to get to what you want to be doing, but with the code provided I can see several issues. Maybe understanding the problems will help you get to the solution you are looking for.
You've got a function inside the template, these are fine to pass to event handles such as #click, but they can have a negative effect on performance. Whenever you have the template re-render (which happens when certain data changes) it will re-run the functions. In this case you run the function as many times as you have attributes, and if any if the AttributeClicked method causes a reactivity update to propagate to this template, you will have an endless loop.
looks like you're calling a mutation when an action may be more appropriate. Mutations are strictly for updating state in a synchronous manner. The select mutation does not mutate the state, so it's simply wrong to put it in there. Even though it may work, vuex is a tool that not only stores global state, but also organizes it in an opinionated way. If you're going to go against the intended design, you may find it easier to just avoid using it.
I suspect you may be able to execute AttributeClicked(attribute) one time during component mount.

Vue - same mutation refreshes (or not!) components depending on which component it is called from?

I have problem understanding why THE SAME mutation fails to refresh data displayed in components (although it does change underlying vuex store data!) if it is called from one of the components, but it does refresh the data if called from another component?
I am updating Filter objects stored in store this way: state.report.filters[], where filters is array of Filter objects.
const state = {
report: {
filters: [], // array of Filter objects
...
}
}
My mutation looks for a filter in the array and substitutes the whole Filter object.
const mutations = {
setFilter: (state, newFilterValue) => {
let changedFilter = state.report.filters.find(filter => {
return filter.fieldName === newFilterValue.fieldName;
});
changedFilter = newFilterValue;
}
}
The mutation is called from a method of Filter class defined like this (separate module):
import { store } from './store';
export class Filter {
constructor ({
...
} = {}) {
this.operators = []; // Array of Operator objects
this.value = []; // Array of values - in this case Dates
};
updateOperator (operatorName) { // this mutation refreshes components when executed
this.operator[0] = new Operator(operatorName);
store.commit('setFilter', this); // whole object passed to the mutation
};
updateValue (newValue) { // this mutation changes store value, but fails to refresh components
this.value[0] = newValue; // newValue is a Date
store.commit('setFilter', this);
};
};
The app displays data in rows (each Filter has a separate row), each row contains cells, of which one contains components dedicated to Filter's value and Operator. These dedicated components receive as props callback functions which are methods of the Filter object. They execute the callback functions when a new value is entered passing the value to the Filter which then updates a relevant property and calls the mutation passing in both cases the whole Filter object as payload.
// TABLE CELL COMPONENT displaying filter value and operator
<template>
<td>
<operator-component
:iconName="proppedFilterObject.operator.iconName"
:callback="proppedFilterObject.updateOperator.bind(proppedFilterObject)"
></operator-component>
<value-component
:date="proppedFilterObject.value[0]"
:callback="proppedFilterObject.updateValue.bind(proppedFilterObject)"
></value-component>
</td>
</template>
<script>
export default {
props: ['proppedFilterObject'] // whole filter object
};
</script>
// OPERATOR COMPONENT
<template>
<div #click.stop="chooseOperator">
{{ iconName }} // some operator value display
</div>
</template>
<script>
export default {
methods: {
chooseOperator () {
const modal = new ChooseOperatorModal({
callback: this.callback // this displays another modal for receiving data. The modal calls the callback.
});
},
},
props: ['callback', 'iconName']
};
</script>
// VALUE COMPONENT
<template>
<date-picker v-model="computedDate"> // THIRD PARTY COMPONENT
</date-picker>
{{ date }} // additional display to verify if there's a problem within 'date-picker'
</template>
<script>
import DatePicker from 'vue2-datepicker'; // THIRD PARTY COMPONENT
export default {
components: { DatePicker },
computed: {
computedDate: {
get: function () {
return this.date;
},
set: function (newValue) {
this.callback(newValue);
}
}
},
props: ['callback', 'date']
};
</script>
So, if eg. I enter new operator value from Operator component, everything refreshes. When I enter a new value in the value component, the mutation is executed and store value changed, but displayed data are not refreshed. However, if afterwards I change an operator all the components will refresh and value will get displayed. Even if I change operator in a different Filter object(!). Ie:
a) Change in report.filters[0].value - display not refreshed, but...
b) then change report.filters[1].operator - both report.filters[1].operator AND PREVIOUSLY CHANGED report.filters[0].value get refreshed(?!).
What can be a reason of such behaviour? Where to look for the problem?
Some additional remarks:
1) I am using a third party component "vue2-date-picker" for date choice and display. However it does not seem to be responsible for the problem, as if I try to display the new value just in {{ }} notation it behaves the same way. I have used the date picker in other components and there it functions correctly as well.
2) In the code samples I left out most imports/exports and other seemingly irrelevant elements to keep the question reasonably short.
There are a lot of problems with the code and several of them are contributing to the problems you're seeing. A full, thorough answer that addresses all of these problems would be ridiculously long so instead I will skim through them without going into huge amounts of detail. You will need to do some further reading and experimentation to understand each of these topics properly.
Let's start with this line in the mutation:
changedFilter = newFilterValue;
This line assigns a new value to the local variable changedFilter. That's all. As it's the last line of the mutation the net result is that it doesn't really do anything.
Presumably your intent was to update the array state.report.filters, replacing the old entry with a new entry. However, just updating a local variable isn't going to do that.
At this point you may be wondering 'If that doesn't do anything, then why is the state in my store changing?'. I'll come to that in a moment but first let me prove to you that your existing code does nothing.
Try removing the code inside setFilter completely. Just leave an empty function. Then try clicking around in the UI just like you did before. You'll find that the store state updates just the same as it did before, even though you've removed the code to update the array.
The correct way to implement that mutation would be to use findIndex to find the relevant index and then use either Vue.set or the array's splice method to update the array accordingly. That will change the item in the array. However...
This brings us back to the earlier question. Why is the state updating if the mutation does nothing?
This is because you're using the same object in multiple places. The Filter object held in the array is the same object that your UI is editing. There are no copies being taken, there is just a single object. So when you change the properties of that object inside updateOperator or updateValue this will immediately be reflected inside the store. Calling the setFilter mutation is just asking the store to replace an object with itself.
There's nothing specific to Vue about this. This is just the standard behaviour of reference types in JavaScript. It is also common with many other programming languages that don't directly expose pointers. It can be useful to learn a little about how pointers work in other languages as it will give you a better initial mental model before attempting to understand how reference types behave in JavaScript. Understanding the difference between 'by value' and 'by reference' may also be a useful starting point.
The next topic to cover is reactivity, which very much is a Vue topic.
Specifically, there are certain changes that Vue can't detect. These are usually referred to as the reactivity caveats. You can find more about them in the official documentation:
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
https://v2.vuejs.org/v2/guide/list.html#Caveats
There are at least two lines in your code that violate these rules:
this.operator[0] = new Operator(operatorName);
and
this.value[0] = newValue;
You can't set array entries directly by index. The array will update but it won't trigger any reactive dependencies within Vue. Instead you need to use either Vue.set or one of the array methods, e.g. push, pop, splice, etc.. In this example you could use splice.
e.g. Using Vue.set:
Vue.set(this.value, 0, newValue);
e.g. Using splice:
this.value.splice(0, 0, newValue);
Why does all of this matters?
Well Vue will only re-render a component if its reactive dependencies have changed. They are very similar to computed properties in that regard. Here's how it works...
Vue compiles the template down to a function. That function is referred to as the render function. When rendering a component Vue calls the render function and that function returns a description of how to render the component. Any reactive properties that are touched while that function is running will be recorded as dependencies. If, at some point in the future, the value of one of those reactive properties changes then Vue will rerun the render function to generate a new rendering of that component.
There are two key points to take out of this description:
If you fall foul of one of the reactivity caveats then Vue won't know the dependency has changed, so it won't re-render the component.
The render function runs as a whole. It doesn't just target a small chunk of the template, it always runs the whole thing.
So if you change a dependency in a non-reactive way (i.e. one of the caveats) it won't trigger a rendering update. But if you subsequently update a dependency properly, Vue will detect that and will rerun the render function. When it runs it will run the whole thing, so any new values will be picked up, even if they weren't detected when they changed.
It isn't immediately clear to me which rendering dependency is causing your component to re-render. However, it only needs one of them to change in a detectable manner. Any other changes will then get pulled in incidentally when the render function runs and reads their current values.
That covers why your code isn't working. However, I would also worry about your decision to introduce a Filter class. I understand how that may be appealing if you've come from some other OO environment but it isn't typically how Vue is used. It is possible to make it work but you will need a good understanding of both JavaScript reference types and the Vue reactivity system to avoid falling through the cracks. There is no reason why using a specific class to hold your data can't be made to work but in practice it usually ends up being less maintainable than not using such a class. A more typical Vue approach would be to use simple, anonymous objects/arrays to hold the data and then for the data owner (either a component or store module) to be responsible for making any mutations to that data. Events are used to pass changes up the component hierarchy rather than callback props.
Ultimately you will need to judge whether the Filter class is justified but it is probably not what future maintainers of your code will be expecting.

How to implement a switcher/viewstack component with slot?

I'm trying to implement a 'switcher' or 'viewstack' component in Aurelia, this would be useful for wizards, paged content, and stepping through a list of tasks. This would show a single child component at a time from a number of possible children. I'd like the usage markup to resemble:
<my-switcher>
<one-component></one-component>
<two-component></two-component>
<three-component></three-component>
</my-switcher>
Now, the obvious alternatives here are:
<compose view-model.bind="currentStep"> and point the currentStep variable to each component at a time.
(+ves: components are only instantiated when accessed, -ves: needing to know the path for each component, all children need to be valid view-models)
Add an if.bind='active' within the definition of each component in the slot, and just set this active member from the my-switcher class. (+ves: easier to follow, -ves: components need to be specifically written for use here).
Retrieve the children via #children (if this now works reliably?) and add the Element as a child DOM element manually, then call ViewCompiler.enhance. (-ves: can't seem to get get #children to work, larger amount of custom code)
Each of these feels a bit contrived a solution. Does anyone have any idea about whether there a cleaner approach that could/should be used instead?
Combine options 2 and 3 while avoiding the negatives (not sure why you can't get #children to work).
consumer.html
<my-switcher>
<my-switcher-item>
<one-component></one-component>
</my-switcher-item>
<my-switcher-item>
<two-component></two-component>
</my-switcher-item>
<my-switcher-item>
<three-component></three-component>
</my-switcher-item>
</my-switcher>
my-switcher-item.js
export class MySwitcherItem {
constructor() {
this.isActive = false;
}
}
my-switcher-item.html
<template show.bind="isActive">
<slot></slot>
</template>
my-switcher.js
import {children} from 'aurelia-framework';
export class MySwitcher {
#children('my-switcher-item') items = [];
// Here you have access to this.items[index].isActive.
// You can set this value to true or false from this class
// and make sure only one item's isActive is true.
// You could add next() and previous() methods to move between
// the items, etc.
}