Getting an updated array to update in a Vue3 template - javascript

While this is not the exact code, it represents the issue. I have an object that has a method that returns an array. This array is then updated asynchronously. In the demo it is a timeout but in reality it would be an HTTP call:
function () {
let a = ['apple'];
setTimeout(() => {
a.push['banana'];
}, 3000);
return a;
}
Then it is included in a Vue component via a computed property and displayed, e.g.
<ul>
<li v-for="(row, index) in testObjectArr" v-bind:key="index">
{{row}}
</li>
</ul>
However this only displays the first entry (apple) and not the second (banana). If an event does cause the template to refresh then it does appear.
Using things like watch also doesn't work. The only way to get it working is to pass a handler in to the object that manually triggers a redraw which is oviously less than ideal.
Is there any way to get responsivity to actually work with the above situation?

In order to make variables reactive, you should declare them as such by using ref or reactive. So your code should be:
<script setup>
import { ref } from 'vue';
const items = ref(['apple']);
setTimeout(() => {
items.value.push('banana');
}, 1000);
</script>
<template>
<ul>
<li v-for="(item, index) of items" :key="index">
{{ item }}
</li>
</ul>
</template>
See it live

Related

Vue inline template not triggering method even with emit

I'm wondering what I am doing wrong here, from what I can see this is the solution: Vue: method is not a function within inline-template component tag
However the method is still not triggering.
<b-table
:items="filtered"
:fields="fields"
sort-icon-left
responsive="sm"
#card-click="setUpdate"
>
<template v-slot:head()="data">
<span class="text-info">{{ data.label.toUpperCase() }}</span>
<button #click="$emit('card-click', data)">filter</button>
<input
v-show="data.field.showFilters"
v-model="filters[data.field.key]"
:placeholder="data.label"
/>
</template>
</b-table>
methods: {
setUpdate(field) {
console.log("hello");
console.log(field);
this._originalField = Object.assign({}, field);
field.showFilters = true;
}
}
Update
So the #click allowed me to to trigger the event but this lead to the table wouldn't update with the changed data with showFilters. Thanks to MattBoothDev I found event-based-refreshing-of-data, however this oddly now prevents the data from changing. I.e. if field.showFilters is true it's true if I click the button.
methods: {
setUpdate(field) {
this._originalField = Object.assign({}, field);
field.showFilters = !field.showFilters;
this.refreshTable();
console.log(field.showFilters);
},
refreshTable() {
this.$root.$emit("bv::refresh::table", "my-table");
}
}
It looks like you're using Bootstrap Vue?
What you're essentially doing here is putting a listener on the <b-table> tag for card-click but that event is essentially not happening within a child component of <b-table>.
Regardless, I'm not even sure you need the event.
<button #click="$emit('card-click', data)">filter</button>
can easily just be
<button #click="setUpdate(data)">filter</button>
EDIT:
It is good practice to use MVVM for Vue.js as well.
Rather than: #click="$emit('card-click', data)"
Should be: #click="onFilterClicked"
Then:
methods: {
onFilterClicked (data) {
this.$emit('an-event', data.some.property)
}
}
This will make testing your code a lot easier.

Handle click events in a list

Here is a ToDoList from Vue examples.
I want to add some extra features to this small app, e.g. set date for task. Therefore I'd like to show more operations of the task when I click "...".
Below is what I want to avoid, which after clicking another task, the previous click action doesn't be removed:
I try to add a property for each todo, and bind a click function on the "..." (more). Each time click "more", firstly set "isMoreClick" property of all task to false, then toggle the value of "isMoreClick" of current clicked task:
<button class="more"
#click="isMoreClick(todo)"
v-show="!todo.isMoreClick">
</button>
<div class="more-opt" v-show="todo.isMoreClick">
<button class="destroy" #click="removeTodo(todo)"></button>
</div>
...
this.todos.push({
id: todoStorage.uid++,
title: value,
completed: false,
isMoreClick: false // this is what I added
})
...
isMoreClick (todo) {
this.todos.forEach(todo => {
todo.isMoreClick = false
})
todo.isMoreClick = !todo.isMoreClick
}
I think my approach is a little stupid. Is there any better solution? (set a flag symbol?)
You don't say how you're rendering the todo elements. But if you're using a v-for loop, one approach could be
<ul>
<li
v-for="(todo, index) in todos"
:key="index"
>
{{todo.whatever}}
<button
v-if="index !== visibleTodoIndex"
class="more"
#click="visibleTodoIndex = index"
/>
<div
v-else
class="more-opt"
>
<button
class="destroy"
#click="visibleTodoIndex = -1"
/>
</div>
</li>
<ul>
Then just add visibleTodoIndex to the component's data.
It looks to me you need to use a global variable accessible from all todos, not to have a local variable inside each todo and updating it everywhere every time. I recommend using vuex store and updating isMoreClick value via mutations.

vuex state splice keep removing the last item

I created a list on each list I've set up a remove method. Every-time I click the list in my jsfiddle I get what I want, but when I attach my project up with vuex it seems like the index is wrong. It just keeps removing the last item on my list no mater where i click. instead of the one i click.
<md-table>
<md-table-row v-for="(opt, index) in this.getAddedSegmentationList" :key="index">
<md-table-cell>
<ul >
<li>Name: API_NEEDS_TO_BE_UPDATED</li>
<li>Created: {{opt.created_time | newTime}}</li>
<li>Finished: {{opt.end_time | newTime}}</li>
</ul>
<ul class="channel-segements__tab-handlers">
<li class="channel-segements__tab-handlers-items" #click="removeSegement(index, opt)">
<md-icon>delete</md-icon>
</li>
</ul>
</md-table-cell>
</md-table-row>
</md-table>
methods: {
removeSegement(index, opt) {
this.getAddedSegmentationList.splice(index, 1)
}
},
computed: {
getAddedSegmentationList() {
return this.$store.state.channels.channelSegmentList
}
},
My original jsfiddle https://jsfiddle.net/ronoc4/eywraw8t/65812/ for testing to make sure it was possible.

componentDidUpdate() doesn't seem to get called after actual re-rendering occurs

The basic code flow is like this.
item.js
export default class Item extends React.Component {
constructor(props) {
super(props);
this.state = {
modalIsOpen: false,
logs: []
};
this._fetchLogs = this._fetchLogs.bind(this);
this._fetchAnotherLogs = this._fetchAnotherLogs.bind(this);
}
render() {
let tableBody;
if (this.state.logs.length > 0) {
let body = [];
this.state.logs.forEach((log) => {
body.push(
<tr key={`log${log.id}`}>
<td>{log.operator_name} / {log.operator_code}</td>
</tr>
);
});
tableBody = (
<table className="logs">
<thead>
<tr>
<th className="operator">{i18next.t('operator')}</th>
</tr>
</thead>
<tbody>{body}</tbody>
</table>
);
}
return (
<div>
<ul className="tabs">
<li onClick={this._fetchLogs}>Logs</li>
<li onClick={this._fetchAnotherLogs}>Another Logs</li>
</ul>
<div className="modalContent">
{tableBody}
</div>
</div>
);
}
componentDidUpdate() {
console.log('didUpdate', $('.ReactModal__Content').height(), $(window).height());
}
_fetchLogs() {
$.ajax({
context: this,
url: URL.DEVICE_LOGS(this.props.id),
success(resp) {
const logs = resp.data;
this.setState({logs});
},
error(resp) {
console.log(resp);
},
});
}
_fetchAnotherLogs() {
$.ajax({
context: this,
url: URL.DEVICE_LOGS_ANOTHER(this.props.id),
success(resp) {
const logs = resp.data;
this.setState({logs});
},
error(resp) {
console.log(resp);
},
});
}
};
So when the <li> element is clicked, it calls _fetchLogs(), which sets the logs state to some array from the server, and inside render(), it sets the tableBody variable and fills <div className="modalContent">.
What I want to achieve is that by using jQuery after the state changes and the component re-renders the table, measure the height of the table and changes some styles accordingly.
But what componentDidUpdate() logs out is the height before all the children(say, the <table>) are re-rendered.
For example, if I say the default height (without <table> content) is 170px, the after-height (with <table> content of logs) is 340px, and the after-height (with <table> content of another logs) is 500px, when I first click <li> it says the height is 170px although the result height is 340px actually. And if I click another <li> to fetch another logs, now it says the height is 340px, which is also wrong because it should be 500px.
So I'm thinking that componentDidUpdate() is called only after the state changes, not actual re-rendering (due to the state change) finishes. Is there something that I'm doing wrong or is there another options should I take into account?
Edit 1
Following the Matthew Herbst's answer, which I thought is reasonable enough to be the right answer, I've made couple of changes but it didn't really change.
<div className="modalContent" ref={c => this._modalContent = c}>
{tableBody}
</div>
And
componentDidUpdate() {
console.log('didUpdate', this._modalContent.offsetHeight);
}
(I also tried to wrap it with jQuery but it didn't change...)
I tried to log out the sequence of rendering and componentDidUpdate() by adding console.log() right before the render()'s return (though it cannot be achieved this way, I know), it clearly shows that the array of objects log comes before the didUpdate log.
So, what's happening here is that you're not actually accessing the component in componentDidMount, you're accessing the DOM, which may not have been updated yet even though your component has rendered (remember, React has to render all the components that may have updated and then does a single DOM diff).
You are correct about using componentDidUpdate, but you are just accessing the wrong thing. What you needs are refs.
In your render, add a ref:
return (
<div>
<ul className="tabs">
<li onClick={this._fetchLogs}>Logs</li>
<li onClick={this._fetchAnotherLogs}>Another Logs</li>
</ul>
<div
className="modalContent"
ref={(myTableRef) => this.myTableRef = myTableRef}
>
{tableBody}
</div>
</div>
);
Then in your componentDidUpdate, you can do:
componentDidUpdate() {
console.log('didUpdate', $(this.myTableRef).height(), $(window).height());
}
That should give you the correct height for the table model.

Meteor : wait until all templates are rendered

I have the following template code
<template name="home">
<div class="mainBox">
<ul class="itemList">
{{#each this}}
{{> listItem}}
{{/each}}
</ul>
</div>
</template>
<template name="listItem">
<li class="item">
{{username}}
</li>
</template>
And I'd like to execute a code once ALL of the "listItem" are rendered. There are about 100 of them. I tried the following
Template.home.rendered = function() {
// is this called once all of its 'subviews' are rendered?
};
But it doesn't wait until all views are loaded.
What's the best way of knowing when all sub-view templates are loaded?
This is how I proceed :
client/views/home/home.html
<template name="home">
{{#if itemsReady}}
{{> itemsList}}
{{/if}}
</template>
<template name="itemsList">
<ul>
{{#each items}}
{{> item}}
{{/each}}
</ul>
</template>
<template name="item">
<li>{{value}}</li>
</template>
client/views/home/home.js
Template.home.helpers({
itemsReady:function(){
return Meteor.subscribe("items").ready();
}
});
Template.itemsList.helpers({
items:function(){
return Items.find();
}
});
Template.itemsList.rendered=function(){
// will output 100, once
console.log(this.$("li").length);
};
lib/collections/items.js
Items=new Mongo.Collection("items");
server/collections/items.js
insertItems=function(){
var range=_.range(100);
_.each(range,function(index){
Items.insert({value:"Item "+index});
});
};
Meteor.publish("items",function(){
return Items.find();
});
server/startup.js
Meteor.startup(function(){
Items.remove({});
if(Items.find().count()===0){
insertItems();
}
});
We specify that we want to render our list of items only when the publication is ready, so by that time data is available and the correct number of li elements will get displayed in the list rendered callback.
Now the same using iron:router waitOn feature :
client/views/home/controller.js
HomeController=RouteController.extend({
template:"home",
waitOn:function(){
return Meteor.subscribe("items");
}
});
client/lib/router.js
Router.configure({
loadingTemplate:"loading"
});
Router.onBeforeAction("loading");
Router.map(function(){
this.route("home",{
path:"/",
controller:"HomeController"
});
});
client/views/loading/loading.html
<template name="loading">
<p>LOADING...</p>
</template>
Using iron:router is probably better because it solves a common pattern elegantly : we don't need the itemsReady helper anymore, the home template will get rendered only when the WaitList returned by waitOn will be globally ready.
One must not forget to add both a loading template and setup the default "loading" hook otherwise it won't work.
I had this same problem with needing to wait on all my subtemplates to load before calling a slick JavaScript carousel plugin (or any cool JavaScript plugin like charts or graphs that need your whole data set loaded in the DOM before calling it).
I solved it by simply comparing the rank of the subtemplate to the overall count that should be returned for whatever query I was doing. Once the rank is equal to the count, you can call your plugin from the subtemplate.rendered helper because all the subtemplates have been inserted into the DOM. So in your example:
Template.listItem.rendered = function() {
if(this.data.rank === ListItems.find({/* whatever query */}).count()) {
console.log("Last item has been inserted into DOM!");
// Call your plugin
$("#carousel").owlCarousel({
// plugin options, etc.
});
}
}
Then you just need your helper for listItems to return a rank, which is easy enough:
Template.home.helpers({
listItems: function() {
return ListItems.find({/* whatever query */}).map(function(listItem, index) {
listItem.rank = index + 1; // Starts at 1 versus 0, just a preference
});
}
}
the method rendered works of this way
This callback is called once when an instance of Template.myTemplate is rendered into DOM nodes and put into the document for the first time.
so, when is rendered you doesn't have variable reactive in this case.
// this would sufficient
Template.listItem.helpers = function() {
username:function(){
return ...
}
};
I'd suggest something like:
var unrendered = [];
Template.listItem.created = function () {
var newId = Random.id();
this._id = newId;
unrendered.push(newId);
};
Template.listItem.rendered = function () {
unrendered = _.without(unrendered, this._id);
if (!unrendered.length) {
// WHATEVER NEEDS DOING WHEN THEY'VE ALL RENDERED
}
};
CAVEAT
This works on the assumption that essentially all template instances will be created before they first ones have been rendered, otherwise your code will run before it should. I think this should be the case, but you'll have to try it out as I don't really have time to run a 100+ sub-template test. If it's not the case, then I can't see how you can achieve this behavior without knowing in advance exactly how many sub-templates will be created.
If you do know how many there will be then the code above can be simplified to a counter that decrements every time rendered runs, and it's easy.
unrendered = [number of listitems];
Template.listItem.rendered = function () {
unrendered--;
if (!unrendered) {
// WHATEVER NEEDS DOING WHEN THEY'VE ALL RENDERED
}
};
Also, you may need to meteor add random, but I think this package is now included in core.
Apparently there are various ways to handle your situation. You could easily use template subscriptions.
Template.myView.onCreated(function() {
var self = this;
self.autorun(function(){
self.mySub = self.subscribe('mySubscription');
});
if(self.mySub.ready()) {
// my sweet fancy code...
}
});
<template name="myTemplate">
<ul>
{{#if Template.subscriptionsReady}}
{{#each items}}
<li>{{item}}</li>
{{/each}}
{{else}}
<div class="loading">Loading...</div>
{{/if}}
</ul>
</template>

Categories

Resources