I have this structure inside Vue DIV:
<div class="row">
<div class="well chart-container">
<div id="chart" v-if="chartShown"></div>
<h1 v-if="textShown">{{staticPropertyValue}}</h1>
</div>
</div>
In my application I'd like to be able to display chart div OR h1 tag. Here's a part of my javaScript:
app.textShown = false;
app.chartShown = true;
if (data.length == 0) {
MG.data_graphic({
title: "Missing Data",
description: "",
error: 'No data',
chart_type: 'missing-data',
missing_text: 'There is no data',
target: '#chart',
full_width: true,
height: 600
});
return;
};
if (data.length == 1) {
app.staticPropertyValue = data[0].value;
app.chartShown = false;
app.textShown = true;
return;
}
console.log('DRAWING GRAPH');
console.log(document.getElementById('chart'));
MG.data_graphic({
title: "Fetched data",
description: "",
data: data,
full_width: true,
height: 600,
target: '#chart',
x_accessor: 'time',
y_accessor: 'value'
});
So, depending on data.length property, the chart is shown or h1 tag is shown. The problem appears, when I first time call above code and display h1 tag (because data.length == 1) and then next time I call it with date.length > 1 (chart should appear). I get error:
The specified target element "#chart" could not be found in the page.
The chart will not be rendered.
It is from the library that I'm using for drawing charts - metricsgraphics.js.
So I console logged the result of
document.getElementById('chart')
and it was null. So it means that although i switch chartShown to true, it's not done fast enough. How can I fix this?
I also tried using v-show instead of v-if - didn't work well - I had some errors about the width of some elements being NaN.
You should run this piece of code in the mounted() callback of your component.
To me, it looks like you're running it when the script has finished loading, which is not necessarily when the DOM is fully built and definitely not when Vue has finished rendering its components.
Working with v-if while using an external library is not a good idea anyway, you're much better off initializing your view in the mounted() callback and then watching your chartShown variable like so:
{
...,
watch: {
chartShown(nv) {
if (nv) {
// setup chart
} else {
// remove chart
}
}
},
...
}
Related
I am destroying the chart but when it's not rendered I get error.
Is there a way to check if chart is rendered, then destroy it?
if(chart)
chart.destroy()
Each time i destroy an object that does not exist i get TypeError: Failed to execute 'removeChild' on 'Node': parameter 1 is not of type 'Node'.
Also i need to render it again if it's not rendered, i won't render it again and again. I need that check
The linked documentation states that render() returns a promise once the chart is drawn to the page.
The code however seems to return that promise immediately (which makes sense) and resolves that promise, when the chart was drawn.
As far as I can see, it should be sufficient to set and keep a state-flag after the promise is resolved like so:
let chart = new ApexCharts(el, options);
chart.render().then(() => chart.ohYeahThisChartHasBeenRendered = true);
/* ... */
if (chart.ohYeahThisChartHasBeenRendered) {
chart.destroy();
}
Update after comment
Yes this works! I made this runnable example for you (typically this is the duty of the person asking the question ;) ) Press the button and inspect the log):
<html>
<head>
<title>chart test</title>
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<head>
<body>
<div id="chart"></div>
<script>
let options = {
chart: { type: 'line' },
series: [{ name: 'sales', data: [30,40,35,50,49,60,70,91,125] }],
xaxis: { categories: [1991,1992,1993,1994,1995,1996,1997, 1998,1999]}
},
chart = new ApexCharts(document.querySelector("#chart"), options),
logChart = () => console.log(chart),
destroyChart = () => {
if (chart.ohYeahThisChartHasBeenRendered) {
chart.destroy();
chart.ohYeahThisChartHasBeenRendered = false;
}
};
chart.render().then(() => chart.ohYeahThisChartHasBeenRendered = true);
</script>
<button onclick="logChart()">Log chart</button>
<button onclick="destroyChart()">Destroy chart</button>
</body>
</html>
I suspect that you tried something like this to check for the flag:
chart.render().then(() => chart.ohYeahThisChartHasBeenRendered = true);
console.log(chart.ohYeahThisChartHasBeenRendered);
It will not do what you expect because the promise is not resolved yet.
Update after another comment
As pointed out by a comment there is a related known issue with apexcharts:
https://github.com/apexcharts/apexcharts.js/pull/415
Even though this question asks to "check if the chart is rendered", the code suggests that they actually want to "check if the chart exists". I would also like to check if a chart exists before rendering it, and I suspect this is the more common issue.
I'm not sure about the accepted answer here. It seems that this answer always creates a new chart, hence there is no need to check if the chart exists.
I worked on this for some time - got no help from documentations- and finally discovered the Apex object. Check out Apex._chartInstances: this field is undefined before any charts render, and as they render, they store references here. After at least one rendering, the length of this field is equal to the number of existing charts.
Check if any charts have ever existed: (Apex._chartInstances === undefined)
Check if any charts currently exist: (Apex._chartInstances.length > 0)
Access the id's of existing charts: (Apex._chartInstances[0].id)
These bits were enough to make it work for my case. Hope this helps somebody else.
I was able to use the beforeMount and mounted events to check if the chart was rendered or not. For some reason, I am not able to catch the error that ApexChart throws.
My logic:
in beforeMount, make a delayed call to error handler and set error flag to true.
in mounted, set error flag to false. When the error handler runs, if the error flag is false, you skip othwe
{
...
chart: {
type: "scatter",
height: height,
events: {
beforeMount: function (chartContext, config) {
setTimeout(() => {
PAGE_DATA.ShowChartError = true;
showChartErrorMessage($(chartContext.el));
}, 1000);
},
mounted: function (chartContext, config) {
PAGE_DATA.ShowChartError = false;
},
},
},
...
}
Error handler,
function showChartErrorMessage($el) {
if (PAGE_DATA.ShowChartError) {
// show error msg
$el.siblings(".error-help-container").removeClass("hidden");
// hide chart div
$el.hide();
}
PAGE_DATA.ShowChartError = false;
}
I tried all sorts of suggestions on destroying a rendered chart and nothing seemed to work. Finally I tried this and it worked.
Before you render it, put a destroy in a try catch with no error, basically an on error resume next and then render it.
var chart22 = new ApexCharts(document.querySelector("#row2-2"), options1);
try{
chart22.destroy();
}
catch{
}
chart22 = new ApexCharts(document.querySelector("#row2-2"), options1);
chart22.render();
I'm using jquery jTable in my web-based project. Its pagination work fine when the number of rows less than 10,000. But when the number of rows exceeded from 10000, it causes an unfamiliar issue in its pagination. The pagination start skipping odd page numbering. You can see it in picture below:
My javaScript code for jTable is:
$("#AllProductTable").jtable({
paging: true,
pageSize: 10,
columnSelectable: false,
actions: {
listAction: '/ProductDefinition/Select'
},
fields: {
ProductId: { visibility: 'hidden', listClass: 'right-align' },
ProductCode: { title: 'Product Code' },
// more fields...
},
recordsLoaded: function (event, data) {
}
});
while my html tag for jTable is:
<div id="AllProductTable" style="width:400%;"></div>
I explored a lot\, but didn't find the solution related to it. I'm further unable to understand this miss behavior.
And at last, I got succeeded to solve this issue. I go through the jquery.jtable.js whole file and find something strange in one of its function. The function was;
_refreshGotoPageInput: function() {
// different logic statements
//...
//Skip some pages is there are too many pages
var pageStep = 1;
if (currentPageCount > 10000) { // if you having more than 10,000 pages
pageStep = 100;
} else if (currentPageCount > 5000) { // if you having more than 5000 pages
pageStep = 10;
} else if (currentPageCount > 2000) { // if you having more than 2000 pages
pageStep = 5;
} else if (currentPageCount > 1000) { // if you having more than 1000 pages
pageStep = 2;
}
//....
// Differnet logic statements
}
All you need is just to comment this portion of the above given function, or modify it according to your own implementation logic.
In my above mentioned case, my no_of_pages were increasing from 1000, that's why its taking 2-steps on changing page and so on.
Until recently I was using v-show to display each element in an array, one at a time, in my Vue instance. My html had the following line: <li v-for="tweet in tweets" v-show="showing == $index">{{{ tweet }}}</li>". My root Vue instance was constructed this way (thanks #Jeff!):
new Vue({
el: '#latest-tweets',
data: function(){
return {
tweets: [],
showing: 0
};
},
methods:{
fetch:function(){
var LatestTweets = {
"id": '706642968506146818',
"maxTweets": 5,
"showTime": false,
"enableLinks": true,
"customCallback":this.setTweets,
"showInteraction": false,
"showUser": false
};
twitterFetcher.fetch(LatestTweets);
},
setTweets: function(tweets){
this.tweets = tweets;
console.log(tweets);
},
rotate: function(){
if(this.showing == this.tweets.length - 1){
this.showing = -1;
}
this.showing += .5;
setTimeout(function(){
this.showing += .5;
}.bind(this), 1000);
}
},
ready:function() {
this.fetch();
setInterval(this.rotate, 10000);
}
It was all good until I came accross duplicate values. In order to handle these, I replaced v-show with track-by $index, as specified here. I now have this on my html: <li v-for="tweet in tweets" track-by="$index">{{{ tweet }}}</li>. The problem is that, instead of rendering each list item individually, the whole list is rendered at once.
As to the above rotate method, since I cannot do track-by="showing == $index", it is now useless. As far as I understand, this is due to Vue not being able to detect changes in the length of the Array. There seems to be a workaround, as detailed here, which is to "replace items with an empty array instead", which I did at no avail. I cannot figure out what am I missing.
Here're a couple of JsFiddles, with v-show and track-by $index.
The solution was after all rather simple and the resulting code leaner. Doing away with the v-for and track-by $index directives altogether and using a computed property instead did the trick:
computed: {
currentTweet: function () {
return this.tweets[this.showing]
}
}
On the html file, it is just a question of adding the computed property currentTweet as you normally would, with a mustache tag, here interpreted as raw html:
<li>{{{ currentTweet }}}<li>
No need therefore for anything like this:
<li v-for="tweet in tweets" track-by="$index">{{{ tweet }}}</li>
JsFiddle here
My intention is to retrieve one random entry from a collection and display it on the website - if all sentences are through (read: the user has "seen" them), display something else (therefore a dummy sentence gets returned). But, on server start and on button-click events, this helper gets fired at least twice. Here is some code:
In client.js:
Template.registerHelper('random_sentence', function() {
fetched = _.shuffle(Sentences.find({
users: {
$nin: [this.userId]
}
}).fetch())[0];
if (fetched === undefined) {
return {
sentence: "done",
_id: 0,
done: true
};
}
Session.set('question', fetched._id);
console.log(fetched);
return fetched;
});
The helper function for the template:
sent: function(){
sent = Session.get('question');
return Sentences.findOne(sent);
}
in main template:
{{#with random_sentence}}
{{#if done}}
<!-- Display something else -->
{{else}}
<div class="container">
{{> question}}
</div>
{{/if}}
{{/with}}
the "question" template:
<div class="well">
<div class="panel-body text-center">
<h3>{{sent.sentence}}</h3>
</div>
</div>
If I don't return anything in the "random_sentences"-function,nothing get's displayed.
I don't know where my "logic failure" is situated? I'm new to meteor - so I might overlook something obvious.
Thanks in advance :-)
UPDATE: This is how I intended to get the new sentence and display it:
Template.answer.events({
'click': function(event) {
var text = event.target.getAttribute('id');
if (text !== null) {
var question = Session.get('question');
var setModifier = {
$inc: {}
};
setModifier.$inc[text] = 1;
Sentences.update(question, setModifier);
Meteor.call('update_user', question);
Notifications.success('Danke!', 'Deine Beurteilung wurde gespeichert.');
Blaze.render(Template.question, document.head);
}
}
});
In server.js (updating the question and a counter on the user):
Meteor.methods({
update_user: function(question) {
Sentences.update(question, {
$push: {
"users": this.userId
}
});
Meteor.users.update({
_id: this.userId
}, {
$inc: {
"profile.counter": 1
}
});
},
});
I found the Blaze.render function somewhere on the web. the "document.head" part is simply because this function needs a DOM Element to render to, and since document.body just "multiplies" the body, I ust moved it to the head. (DOM logic isn't my strong part).
An Idea I had: would it make the whole idea simpler to implement with iron-router? atm. I wanted to create a "one-page app" - I therefore thought that I don't need a router there.
Another problem: Getting this logic to work (User gets one random sentence, which he has not seen) and publishing small sets of the collection (so the Client don't have to download 5 MB of data before using).
Template helpers can be called multiple times so it's good to avoid making them stateful. You're better off selecting the random entry in an onCreated or onRendered template handler. There you can do your random select, update the state, and put your choice in a Session variable to be retrieved by the helper.
I have a problem with the pull down refresh. It works the first time, but then if I change to a different view, then come back to the original view, the Pull to refresh and Release to refresh text seem to get duplicated and overlapped on itself. I am "hardcoding" the datasource's data here, I don't want to use the transport ajax.
I am trying to manually update the data in the setOptions pull method, instead of letting Kendo update it via ajax. The actual data update works. There are no Javascript errors and I get the same result in Chrome and Firefox.
First time works:
After moving to another view, then back to this view, then pulling down:
My view code is:
<div id="subitem-view" data-role="view" data-show="showSubItems">
<div data-role="header">
<div data-role="navbar">
</div>
</div>
<ul id="subItemList" class="itemList">
</ul>
<script id="subItemTemplate" type="text/x-kendo-template">
#:Name#
</script>
</div>
Javascript:
function showSubItems(e) {
var subItems = new kendo.data.DataSource({
data: [
{ Name : "Test1" },
{ Name : "Test2" }
]
});
e.view.element.find("#subItemList").kendoMobileListView({
dataSource: subItems,
pullToRefresh: true,
template: kendo.template($("#subItemTemplate").html())
});
if (typeof (e.view.scroller.pull) == "undefined") {
e.view.scroller.setOptions({
pull: function () {
console.log("pull event...");
subItems.data([
{ Name : "Test1 Updated" },
{ Name : "Test2 Updated" }
]);
setTimeout(function () { e.view.scroller.pullHandled(); }, 400);
}
});
}
}
You're initializing your Kendo UI Mobile ListView on every View show which leads to unpredictable results, like recreating the pull to refresh labels. You should do it in the Init event only.