Tableau Vuejs getWorkBook() "Cannot read property get_workbook of null" - vue.js

I am trying to follow the getData example found on the tableau javascript tutorial (https://github.com/tableau/js-api-samples/blob/master/getDataBasic.html) , but for vue js, however, I am unable to get it to work. I am able to render the tableau object, but when it comes to getting the underlying data or even trying to get the workbook name, I get the error: "Cannot read property get_workbook of null". Below is my code:
<template>
<div class="container" style="margin-top: 90px;">
<div id="vizContainer2"></div>
</div>
</template>
<script>
export default {
name: 'TableauHolder',
methods: {
getUnderlyingData(){
const containerDiv = document.getElementById("vizContainer2")
let url = "http://public.tableau.com/views/RegionalSampleWorkbook/Storms"
let options = {
hideTabs: true,
hideToolbar: true,
onFirstInteractive: () => {
}
}
this.viz = new window.tableau.Viz(containerDiv, url, options)
let sheet = this.viz.getWorkbook().getActiveSheet().getWorksheets().get("Storm Map Sheet")
console.log(sheet)
},
},
mounted () {
window.addEventListener('load', () => {
this.getUnderlyingData();
})
}
}
</script>
Placing getWorBbook() in onFirstInteractive successfully gets me the workbook name (as shown below), but I am not sure where to go from there in terms rendering the data.
<template>
<div class="container" style="margin-top: 90px;">
<div id="vizContainer2"></div>
</div>
</template>
<script>
export default {
name: 'TableauHolder',
methods: {
getUnderlyingData(){
const containerDiv = document.getElementById("vizContainer2")
let url = "http://public.tableau.com/views/RegionalSampleWorkbook/Storms"
let options = {
hideTabs: true,
hideToolbar: true,
onFirstInteractive: () => {
let sheet = this.viz.getWorkbook()
console.log(sheet)
}
}
this.viz = new window.tableau.Viz(containerDiv, url, options)
},
},
mounted () {
window.addEventListener('load', () => {
this.getUnderlyingData();
})
}
}
</script>

I realized that the JavaScript API is asynchronous and therefore the let sheet line is executed before while executing the API. Therefore, something like setTimeout will make the line execute after the API has been executed. See below incase anyone was having similar issues:
<template>
<div class="container" style="margin-top: 90px;">
<div id="vizContainer2"></div>
</div>
</template>
<script>
export default {
name: 'TableauHolder',
methods: {
getUnderlyingData(){
const containerDiv = document.getElementById("vizContainer2")
let url = "http://public.tableau.com/views/RegionalSampleWorkbook/Storms"
let options = {
hideTabs: true,
hideToolbar: true,
onFirstInteractive: () => {
}
}
this.viz = new window.tableau.Viz(containerDiv, url, options)
setTimeout(() => {
let sheet = this.viz.getWorkbook().getActiveSheet();
console.log(sheet);
}, 3000);
},
},
mounted () {
window.addEventListener('load', () => {
this.getUnderlyingData();
})
}
}
</script>

Related

Nuxt.js Hackernews API update posts without loading page every minute

I have a nuxt.js project: https://github.com/AzizxonZufarov/newsnuxt2
I need to update posts from API every minute without loading the page:
https://github.com/AzizxonZufarov/newsnuxt2/blob/main/pages/index.vue
How can I do that?
Please help to end the code, I have already written some code for this functionality.
Also I have this button for Force updating. It doesn't work too. It adds posts to previous posts. It is not what I want I need to force update posts when I click it.
This is what I have so far
<template>
<div>
<button class="btn" #click="refresh">Force update</button>
<div class="grid grid-cols-4 gap-5">
<div v-for="s in stories" :key="s">
<StoryCard :story="s" />
</div>
</div>
</div>
</template>
<script>
definePageMeta({
layout: 'stories',
})
export default {
data() {
return {
err: '',
stories: [],
}
},
mounted() {
this.reNew()
},
created() {
/* setInterval(() => {
alert()
stories = []
this.reNew()
}, 60000) */
},
methods: {
refresh() {
stories = []
this.reNew()
},
async reNew() {
await $fetch(
'https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty'
).then((response) => {
const results = response.slice(0, 10)
results.forEach((id) => {
$fetch(
'https://hacker-news.firebaseio.com/v0/item/' +
id +
'.json?print=pretty'
)
.then((response) => {
this.stories.push(response)
})
.catch((err) => {
this.err = err
})
})
})
},
},
}
</script>
<style scoped>
.router-link-exact-active {
color: #12b488;
}
</style>
This is how you efficiently use Nuxt3 with the useLazyAsyncData hook and a setInterval of 60s to fetch the data periodically. On top of using async/await rather than .then.
The refreshData function is also a manual refresh of the data if you need to fetch it again.
We're using useIntervalFn, so please do not forget to install #vueuse/core.
<template>
<div>
<button class="btn" #click="refreshData">Fetch the data manually</button>
<p v-if="error">An error happened: {{ error }}</p>
<div v-else-if="stories" class="grid grid-cols-4 gap-5">
<div v-for="s in stories" :key="s.id">
<p>{{ s.id }}: {{ s.title }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { useIntervalFn } from '#vueuse/core' // VueUse helper, install it
const stories = ref(null)
const { pending, data: fetchedStories, error, refresh } = useLazyAsyncData('topstories', () => $fetch('https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty'))
useIntervalFn(() => {
console.log('refreshing the data again')
refresh() // will call the 'topstories' endpoint, just above
}, 60000) // every 60 000 milliseconds
const responseSubset = computed(() => {
return fetchedStories.value?.slice(0, 10) // optional chaining important here
})
watch(responseSubset, async (newV) => {
if (newV.length) { // not mandatory but in case responseSubset goes null again
stories.value = await Promise.all(responseSubset.value.map(async (id) => await $fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json?print=pretty`)))
}
})
function refreshData() { refreshNuxtData('topstories') }
</script>

Watch, Compare & post updated form data to API using Axios in Vue 3

I need help to complete my code.
This is what have done.
I am fetching options from API, so I have defined the initial state as
empty.
Once I have a response from API, I update the state of options.
My form is displayed once I have a response from API.
Now using v-bind I am binding the form.
Where I need help.
I need to watch for the changes in form. If the values of form elements are different from the state of the API response, I would like to enable the submit button.
When the save button is clicked, I need to filter the options that were changed & submit that form data to my pinia action called updateOptions.
Note: API handles post data in this way. Example: enable_quick_view: true
Thank you in advance.
options.js pinia store
import { defineStore } from 'pinia'
import Axios from 'axios';
import axios from 'axios';
const BASE_API_URL = adfy_wp_locolizer.api_url;
export const useOptionsStore = defineStore({
id: 'Options',
state: () => ({
allData: {},
options: {
enable_quick_view: null, // boolean
quick_view_btn_label: "", // string
quick_view_btn_position: "", // string
},
newOptions: {}, // If required, holds the new options to be saved.
message: "", // Holds the message to be displayed to the user.
isLoading: true,
isSaving: false,
needSave: false,
errors: [],
}),
getters: {
// ⚡️ Return state of the options.
loading: (state) => {
return state.isLoading;
},
},
actions: {
// ⚡️ Use Axios to get options from api.
fetchOptions() {
Axios.get(BASE_API_URL + 'get_options')
.then(res => {
this.alldata = res.data.settings;
let settings = res.data.settings_values;
/*
* Set options state.
*/
this.options.enable_quick_view = JSON.parse(
settings.enable_quick_view
);
this.options.quick_view_btn_label =
settings.quick_view_btn_label;
this.options.quick_view_btn_position = settings.quick_view_btn_position;
/*
* End!
*/
this.isLoading = false;
})
.catch(err => {
this.errors = err;
console.log(err);
})
.finally(() => {
// Do nothing for now.
});
},
// ⚡️ Update options using Axios.
updateOptions() {
this.isSaving = true;
axios.post(BASE_API_URL + 'update_options', payload)
.then(res => {
this.needSave = false;
this.isSaving = false;
this.message = "Options saved successfully!";
})
.catch(err => {
this.errors = err;
console.log(err);
this.message = "Error saving options!";
})
}
},
});
Option.vue component
<script setup>
import { onMounted, watch } from "vue";
import { storeToRefs } from "pinia";
import { Check, Close } from "#element-plus/icons-vue";
import Loading from "../Loading.vue";
import { useOptionsStore } from "../../stores/options";
let store = useOptionsStore();
let { needSave, loading, options, newOptions } = storeToRefs(store);
watch(
options,
(state) => {
console.log(state);
// Assign the option to the newOptions.
},
{ deep: true, immediate: false }
);
onMounted(() => {
store.fetchOptions();
});
</script>
<template>
<Loading v-if="loading" />
<form
v-else
id="ui-settings-form"
class="ui-form"
#submit="store.updateOptions()"
>
<h3 class="option-box-title">General</h3>
<div class="ui-options">
<div class="ui-option-columns option-box">
<div class="ui-col left">
<div class="label">
<p class="option-label">Enable quick view</p>
<p class="option-description">
Once enabled, it will be visible in product catalog.
</p>
</div>
</div>
<div class="ui-col right">
<div class="input">
<el-switch
v-model="options.enable_quick_view"
size="large"
inline-prompt
:active-icon="Check"
:inactive-icon="Close"
/>
</div>
</div>
</div>
</div>
<!-- // ui-options -->
<div class="ui-options">
<div class="ui-option-columns option-box">
<div class="ui-col left">
<div class="label">
<p class="option-label">Button label</p>
</div>
</div>
<div class="ui-col right">
<div class="input">
<el-input
v-model="options.quick_view_btn_label"
size="large"
placeholder="Quick view"
/>
</div>
</div>
</div>
</div>
<!-- // ui-options -->
<button type="submit" class="ui-button" :disabled="needSave == true">
Save
</button>
</form>
</template>
<style lang="css" scoped>
.el-checkbox {
--el-checkbox-font-weight: normal;
}
.el-select-dropdown__item.selected {
font-weight: normal;
}
</style>
In the watch function you can compare the new and old values. But you shuld change it to:
watch(options, (newValue, oldValue) => {
console.log(oldValue, newValue);
// compare objects
}, {deep: true, immediate: false};
Now you can compare the old with the new object. I think search on google can help you with that.
Hope this helps.

data in Vue instance doesn't get updated after axios post response

I am writing a code piece to submit the html form data on a POST REST API. Using Vue.js and axios for that.
My Vue.js code is like this -
const app = new Vue({
el: "#main-div",
data() { return {
name: 'Please enter the name',
showEdit: true,
showResponse: true,
responseText: null
}
},
methods: {
savePerson: function () {
this.showEdit = false;
axios
.post('/api/person', {
name: this.name
})
.then(function (response) {
this.responseText = response.data.name+ ' added successfully.';
console.log(response);
console.log(response.data.name+ ' added successfully.');
})
.catch(function (error) {
this.responseText = error.message;
console.log(error);
});
}
}
}
)
And html -
<div id="main-div">
<h2> Fill out the details to create a Person</h2>
<div v-if="showEdit">
<form >
<div>
Name: <input v-bind:value = 'name' type="text" v-on:focus="name= ''" />
</div>
<div>
<button v-on:click="savePerson">Save</button>
</div>
</form>
</div>
<div v-if="showResponse">
<div><p>{{ responseText }}</p></div>
<div>
<button v-on:click="showEdit = true">Add one more person</button>
</div>
</div>
This code doesn't update responseText. That I can check in Vue plugin in browser.
Any idea what is not correct in my example?
You need to use an arrow function in the callback or else the function injects its own this context:
.then((response) => {
...
})
.catch((error) => {
...
})
Or you could use async/await:
async savePerson() {
this.showEdit = false;
try {
const response = await axios.post('/api/person', {
name: this.name
})
this.responseText = response.data.name+ ' added successfully.';
} catch(error) {
this.responseText = error.message;
}
}
to bind data with the input field you need to use v-model in the HTML and try to use the arrow function in the API call.

How do I render data from Firebase in vue at page load

I am trying to load data from firebase at page load.
The console logs correctly as follows: {-Ltl2osulqmFnKIRoT5Q: {…}, -LtnKKxEWkEH7DbV7VB-: {…}}.
I can't however get the data rendered. fbData only shows []
This is what I have: (I know that I have to use a v-for loop for rendering, but below is only to see if anything is rendered at all, which in my case isn't)
<template>
<div id="main">
<div id="cardFront">{{fbData}}</div>
</div>
</template>
<script>
module.exports = {
data () {
return {
fbData:[],
}
},
created() {
var ref = firebase.database().ref("Users/MK01111000/cards")
ref.once("value")
.then(function(snapshot) {
this.fbData = snapshot.val()
console.log(this.fbData)
})
}
}
What I am looking for is a way to render my data at page load.
<div id="main">
<div id="cardFront">{{preRenderedData}}</div>
</div>
</template>
<script>
module.exports = {
data () {
return {
preRenderedData: null,
fbData:[],
}
},
created() {
var ref = firebase.database().ref("Users/MK01111000/cards")
ref.once("value")
.then(function(snapshot) {
this.fbData = snapshot.val()
})
},
beforeMount() {
this.preRenderedData = this.fbData
}
}

Fetch data in component on initiation using parameters from Vuex store

I am new to Vue and am trying to build a simple movie app, fetching data from an API and rendering the results. I want to have an incremental search feature. I have an input field in my navbar and when the user types, I want to redirect from the dashboard view to the search results view. I am unsure of how to pass the query params from the navbar to the search results view.
Here is my App.vue component
<template>
<div id="app">
<Navbar></Navbar>
<router-view/>
</div>
</template>
<script>
import Navbar from './components/Navbar.vue'
export default {
name: 'App',
components: {
Navbar
},
}
</script>
And here is my navbar component where I have the input field
<template>
<nav class="navbar">
<h1 class="logo" v-on:click="goToHome">Movie App</h1>
<input class="search-input" v-on:keyup="showResults" v-model="query" type="text" placeholder="Search..."/>
</nav>
</template>
<script>
import router from '../router/index'
export default {
data: function () {
return {
query: this.query
}
},
methods: {
goToHome () {
router.push({name: 'Dashboard'})
},
showResults () {
//here on each key press I want to narrow my results in the SearchedMovies component
}
}
}
</script>
If I use router.push to the SearchedMovies component then I am only able to pass the query as a parameter once. I thought about using Vuex to store the query and then access it from the SearchedMovies component, but surely there is a better way of doing it?
I also read about using $emit but since my parent contains all the routes, I'm not sure how to go about this.
You don't need to redirect user anywhere. I've made a small demo to show how one might do it. I used this navbar component as you described and emit an event from it:
const movies = {
data: [
{
id: 0,
title: 'Eraserhead',
},
{
id: 1,
title: 'Erazerhead',
},
{
id: 2,
title: 'Videodrome',
},
{
id: 3,
title: 'Videobrome',
},
{
id: 4,
title: 'Cube',
},
]
};
Vue.component('navbar', {
template: '<input v-model="filter" #input="onInput" placeholder="search">',
data() {
return {
filter: '',
};
},
methods: {
onInput() {
this.$emit('filter', this.filter);
}
}
});
// this is just a request imitation.
// just waiting for a second until we get a response
// from the datasample
function request(title) {
return new Promise((fulfill) => {
toReturn = movies.data.filter(movie => movie.title.toLowerCase().indexOf(title.toLowerCase()) !== -1)
setTimeout(() => fulfill(toReturn), 1000);
});
}
new Vue({
el: '#app',
data: {
movies: undefined,
loading: false,
filter: '',
lastValue: '',
},
methods: {
filterList(payload) {
// a timeout to prevent
// instant request on every input interaction
this.lastValue = payload;
setTimeout(() => this.makeRequest(), 1000);
},
makeRequest() {
if (this.loading) {
return;
}
this.loading = true;
request(this.lastValue).then((response) => {
this.movies = response;
this.loading = false;
});
}
},
mounted() {
this.makeRequest('');
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<navbar v-on:filter="filterList"></navbar>
<ul v-if="!loading">
<li v-for="movie in movies" :key="movie.id">{{ movie.title }}</li>
</ul>
<p v-else>Loading...</p>
</div>
Also jsfiddle: https://jsfiddle.net/oniondomes/rsyys3rp/
If you have any problem to understand the code above let me know.
EDIT: Fixed some bugs and added a couple of comments
EDIT2(after the comment below):
Here's what you can do. Every time user inputs something inside a navbar you call a function:
// template
<navbar v-on:input-inside-nav-bar="atInputInsideNavBar"></navbar>
// script
methods: {
atInputInsideNavBar(userInput) {
this.$router.push({
path: '/filtred-items',
params: {
value: userInput
}
})
}
}
Then inside you 'searched movies' page component you can access this value so:
this.$route.params.value // returns userInput from root component