Vue.js Push doesn't update view - javascript

I create a array which contains objects describing information of buttons. In my template I have a class active which is represent the active button. By default the first button of the array is active.
template:
<b-button-group class="page-buttons mt-2" size="sm">
<b-button v-for="btn in buttons"
:key="btn.key"
#click="selectPage(btn.key)"
v-bind:class="{'active': btn.active}">{{ btn.caption }}
</b-button>
</b-button-group>
Script:
methods: {
$createPaginationButtons: function(numberParameters) {
const numberPages = Math.ceil(numberParameters / NUMBER_ELEMENT_PER_PAGE);
this.buttons = [];
for (let i = 0; i < numberPages; i++) {
this.buttons.push({
caption: `PAGE ${i + 1}`,
active: (i === 0),
key: (i + 1)
});
}
Vue.set(this.buttons[0], 'active', true);
}
}
The buttons array is initialize in data as an empty array.
when my buttons array changes, the view is updated and display the correct number of buttons but my class active is not triggered.
Have you any idea.

Push is a wrapped method so it will trigger a view update.
Do you have declared buttons on the data property?
data() {
buttons:[]
}
Since you are creating the active property as i === 0 you don't need Vue.set.
I don't know if has something to do but instead of making buttons empty and then pushing, use an aux variable to create the array and then just do this.buttons = auxVariable. This should trigger an update.

Related

display text based on the value returned by a class binded to a method

I am running a loop with each item that has a **button **that has a **class **that is **binded **to a method. i want to display a certain text for this button, depending on the value returned by the aforementioned method
HTML Template
<button v-for="(item, index) in items"
:key="index"
:class="isComplete(item.status)"
> {{ text_to_render_based_on_isComplete_result }}
</button>
Method
methods: {
isComplete(status) {
let className
// there is another set of code logic here to determine the value of className. below code is just simplified for this question
className = logic ? "btn-complete" : "btn-progress"
return className
}
}
what im hoping to achieve is that if the value of the binded class is equal to "btn-completed", the button text that will be displayed is "DONE". "ON-GOING" if the value is "btn-in-progress"
my first attempt was that i tried to access the button for every iteration by using event.target. this only returned undefined
another option is to make another method that will select all of the generated buttons, get the class and change the textContent based on the class.
newMethod() {
const completed = document.getElementsByClassName('btn-done')
const progress= document.getElementsByClassName('btn-progress')
Array.from(completed).forEach( item => {
item.textContent = "DONE"
})
Array.from(progress).forEach( item => {
item.textContent = "PROGRESS"
})
}
but this may open another set of issues such as this new method completing before isComplete()
i have solved this by returning an array from the isComplete method, and accessed the value by using the index.
<template>
<button v-for="(item, index) in items"
:key="index"
:class="isComplete(item.status)[0]"
:v-html="isComplete(item.status)[1]"
>
</button>
</template>
<script>
export default {
methods: {
isComplete(status) {
let className, buttonText
// there is another set of code logic here to determine the value of className. below code is just simplified for this question
if (className == code logic) {
className = "btn-complete"
buttonText = "DONE"
}
else if (className != code logic) {
className = "btn-progress"
buttonText = "ON-GOING"
}
return [ className, buttonText ]
}
}
}
</script>

is it possible to select child element when you click on parent element in react, and the add a class to the parent element after an event

I am making a game with react, it's a memory game where you match images.so what I want to archive is, when I click on the div it selects the child of the div(the image) and then add class to the parent and then add class to the child itself. Based on the game logic I have to select the the child element first then if pass some conditions I then add a class to it and it's parent element. look at the code I currently have but it's not working, please help me out.
`
let currentElement;
const imageclick=(e)=>{
//select current image
currentElement=e.target.firstElementChild;
// some game logic the add class to child and parent
//add class to parent
currentElement.closest("div").classList.add("show");
//add class to child
currentElement.classList.remove("hidden")
}
const app=()=>{
return(
<div className="imgDiv transition" key={i} onClick={imageclick}>
<img src={img} alt="" className="tileImage hidden" />
</div>
)
}
`
There are multiple approaches.
One way is to store your images in an array of objects. This array is used to render all of your images. You could even shuffle the array to make the order random.
Inside your component you have a state. This state tracks the index of the currently selected image of the array. The initial state can be null to indicate that there is no current selected image.
While looping over your images, check for each image if the selectedImageIndex is equal to the index of the current image. If so, pass some extra classes.
(You don't need to toggle hidden class on the image. Use the show class on the div to either show or hide the child image).
Pass the index of the current image to the imageClick function in the onClick handler of the div. Whenever we click an image, we want to set the index of the image as our selectedImageIndex.
The component will now rerender and add the class to the clicked div.
Edit
I've modified the answer according to your comment. This example allows for 2 indexes to be stored into the state that tracks the selected images.
Whenever you click the same, the image is deselected. Whenever you click another image it will add its index to the state.
In the useEffect hook you can asses if the images corresponding to the indexes have a similar src or other property that matches.
(I would recommend creating a more robust system in which YOU say which images are the same instead of depending on the URL to be the same. E.g. two images can be the same while having different URL's)
const images = [
{
id: 'A',
src: 'your-image.jpg',
alt: 'Something about your image'
},
{
id: 'B'
src: 'your-other-image.jpg',
alt: 'Something about your image'
},
{
id: 'A' // The match to the other A
src: 'the-other-image-that-matches-the-first.jpg',
alt: 'Something about your image'
}
];
const App = () => {
const [selectedImageIndexes, setSelectedImageIndexes] = useState([null, null]);
const imageClick = index => {
setSelectedImageIndex(currentIndexes => {
// If the same index is clicked, deselect all.
if (currentIndexes.includes(index)) {
return [null, null];
}
// If no indexes have been set.
if (currentIndexes.every(index => index === null)) {
return [index, null];
}
// Set the second clicked image.
return [currentIndexes[0], index];
});
};
useEffect(() => {
// If both indexes are set.
if (selectedImageIndexes.every(index => index !== null) {
/**
* With the indexes in the selectedImageIndexes state,
* check if the images corresponding to the indexes are matches.
*/
if (selectedImageIndexes[0].id === selectedImageIndexes[1].id) {
// Match!
}
}
}, [selectedImageIndexes]);
return (
<div className="images">
{images.map((image, index) => {
const className = selectedImageIndex.includes(index) ? 'show' : '';
return (
<div className="imgDiv transition" key={`img-div-${index}`} onClick={() => imageClick(index)}>
<img src={image.src} className="tileImage" alt={image.alt} />
</div>
);
})}
</div>
)
};

Sorting array kills javascript instance on element (i.e. wysiwyg-editor)

I've got a CMS-like feature that has an article with multiple particles (called blocks). A particle can be a either a rich text field or a table. Based on the Block's discr attribute, a Quill or Handsontable instance should be initiated.
This works perfectly, until I reorder the blocks. When I've got a Quill instance and a Handsontable instance, after reordering them, the Quill gets a context menu from the Handsontable and the Quill instance gets a toolbar.
I'm new to Vue.js, but I already understand that happens. I've read List Rendering Caveats and Why isn’t the DOM updating?. The two div.chapterblock elements don't get reordered (like a jQuery-like application probably would do), but only their content changes. When I use the inspector, I see the .chapterblock#id and it's content changing, not moving. The (Quill/Handsontable/whatever) instance is bound to a specific DOM element and stays bound to the element, even if it changes.
But what I don't (yet) understand is how to solve the problem. How can I reorder items and keep the Quill/Handsontable instance on the right elements? Destroying and re-initializing the instances doesn't feel right.
My template:
<div class="chapterblock" v-for="(block, index) in blocks" v-bind:data-id="block.id">
<template v-if="block.discr == 'html'">
<div class="quill" v-html="block.content"></div>
</template>
<template v-if="block.discr == 'table'">
<script type="application/json" v-html="block.content"></script>
<div v-bind:id="'handsontable_' + block.id" class="handsontable-wrapper"></div>
</template>
<button v-if="index !== 0" v-on:click="move(block, 'up')">up</button>
<button v-if="index !== 1" v-on:click="move(block, 'down')">down</button>
</div>
Vue instance:
return new Vue({
//...
computed: {
blocks: function () {
return this.chapter.blocks.sort(function compare (a, b) {
if (a.position < b.position) {
return -1
}
if (a.position > b.position) {
return 1
}
return 0
})
}
},
methods: {
move: function (block, direction) {
if (direction === 'up') {
block.position = block.position - 1
} else if (direction === 'down') {
block.position = block.position + 1
}
// fetch to save position
}
}
Use the key attribute on the v-for loop to reorder the elements, instead of replacing their contents:
From https://v2.vuejs.org/v2/guide/list.html#key:
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item. An ideal value for key would be the unique id of each item.

How do I target all items in a list, when a change occurs in Vue.js?

I'm building a site that uses Vue for to power the majority of the UI. The main component is a list of videos that is updated whenever a certain URL pattern is matched.
The main (video-list) component looks largely like this:
let VideoList = Vue.component( 'video-list', {
data: () => ({ singlePost: '' }),
props: ['posts', 'categorySlug'],
template: `
<div>
<transition-group tag="ul">
<li v-for="(post, index) in filterPostsByCategory( posts )">
<div #click.prevent="showPost( post )">
<img :src="post.video_cover" />
/* ... */
</div>
</li>
</transition-group>
</div>`,
methods: {
orderPostsInCategory: function ( inputArray, currentCategory ) {
let outputArray = [];
for (let i = 0; i < inputArray.length; i++) {
let currentCategoryObj = inputArray[i].video_categories.find( (category) => {
return category.slug === currentCategory;
});
let positionInCategory = currentCategoryObj.category_post_order;
outputArray[positionInCategory] = inputArray[i];
}
return outputArray;
},
filterPostsByCategory: function ( posts ) {
let categorySlug = this.categorySlug,
filteredPosts = posts.filter( (post) => {
return post.video_categories.some( (category) => {
return category.slug === categorySlug;
})
});
return this.orderPostsInCategory( filteredPosts, categorySlug );
}
}
});
The filterPostsByCategory() method does its job switching between the various possible categories, and instantly updating the list, according to the routes below:
let router = new VueRouter({
mode: 'history',
linkActiveClass: 'active',
routes: [
{ path: '/', component: VideoList, props: {categorySlug: 'home-page'} },
{ path: '/category/:categorySlug', component: VideoList, props: true }
]
});
The difficulty I'm having is transitioning the list in the way that I'd like. Ideally, when new category is selected all currently visible list items would fade out and the new list items would then fade in. I've looked at the vue transitions documentation, but haven't been able to get the effect I'm after.
The issue is that some items have more than one category, and when switching between these categories, those items are never affected by whatever transition I try to apply (I assume because Vue is just trying to be efficient and update as few nodes as possible). It's also possible that two or more categories contain the exact same list items, and in these instances enter and leave methods don't seem to fire at all.
So the question is, what would be a good way to ensure that I can target all current items (regardless of whether they're still be visible after the route change) whenever the route patterns above are matched?
Have you noticed the special key attribute in the documentation?
Vue.js is really focused on performance, because of that, when you modify lists used with v-for, vue tries to update as few DOM nodes as possible. Sometimes it only updates text content of the nodes instead of removing the whole node and then append a newly created one. Using :key you tell vue that this node is specifically related to the given key/id, and you force vue to completely update the DOM when the list/array is modified and as a result the key is changed. In your case is appropriate to bind the key attribute to some info related to the post and the category filter itself, so that whenever the list is modified or the category is changed the whole list may be rerendered and thus apply the animation on all items:
<li v-for="(post, index) in filterPostsByCategory( posts )" :key="post.id + categorySlug">
<div #click.prevent="showPost( post )">
<img :src="post.video_cover" />
/* ... */
</div>
</li>

What's the React way to manipulate multiple elements according to event of another element?

I am confused about this for a long time.
Here is the case:
1, I create a table with multiple rows, in this way:
tableRow(basicInfoArray) {
return basicInfoArray.map((basicInfo, index) => (
<tr
key={basicInfo._id}
className={index % 2 === 0 ? 'alt' : null}
onClick={event => this.props.showDetail(basicInfo._id, event)}
>
<td>{basicInfo.mentee_id}</td>
<td>{`${basicInfo.firstname} ${basicInfo.lastname}`}</td>
<td>{basicInfo.othername}</td>
<td>{basicInfo.location}</td>
</tr>
));
}
As you can see, I bind a onClick event to each row. If the row is clicked, it will highlight, and there will be a drilldown to show detail information.
The question is here, after clicked on the backdrop(which bind a onClick event), the drilldown hide and I should remove the highlight effect from the row. Currently I use this way:
const highLightRows = document.getElementsByClassName('highLight');
for (let i = 0; i < highLightRows.length; i += 1) {
highLightRows[i].classList.toggle('highLight');
}
As the documents of React.js says that it's not a good practice to manipulate the dom directly, the UI change should be caused by the props/state change. Obviously it's not a good idea to bind a state for each row of the table because of the quantity. What's the best practice to do this?
It's important to keep in mind that react renders whatever you have in memory, in this case you have an array of items that you want to display in a table, when clicking any of those items you want to highlight the selected, right?
Well, for that you could define a property in each element of the array called selected, this property will be true/false depending on the user selection, then when rendering the row you will check for this property and if it's there you will assign thehighLight class or not. With this approach you will only need to worry to change the value of this property on memory and it will automatically get highlighted on the DOM.
Here's an example:
renderRow(info, index) {
const styles = [
index % 2 === 0 ? 'alt' : '',
info.selected = 'highLight' : '',
];
return (
<tr
key={info._id}
className={styles.join(' ')}
onClick={event => this.props.showDetail(info, event)}
>
<td>{basicInfo.mentee_id}</td>
<td>{`${info.firstname} ${info.lastname}`}</td>
<td>{info.othername}</td>
<td>{info.location}</td>
</tr>
);
}
renderContent(basicInfoArray) {
return basicInfoArray.map((basicInfo, index) => this.rendeRow(basicInfo, index));
}
Just make sure to set to true the selected property on showDetail function, and then set to false when you need to hide and remove the highLight class.
Good luck!

Categories