Reputation: 3832
I created a simple Minesweeper game and when it comes to the decision, which cell to render there are three possibilities:
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?
Upvotes: 1
Views: 184
Reputation: 29132
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:
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.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.
Upvotes: 1