I don't understand what's going on
componentDidMount() {
console.log('componentDidMount');
//const self = this;
let _id = this.props.match.params.id.toUpperCase();
if (_id != this.state.id.toUpperCase()) {
axios.get('/data/pricemultifull?fsyms=' + _id + '&tsyms=USD')
.then(response => {
// let _currentcoin = { ...resp.data.RAW.BTC.USD, ticker: _id };
this.setState({ id: _id }); //this == undefined
});
}
}
I can get a response back but this is always undefined and I'm unable to setState. I'm using an arrow function which I thought was scope 'this' to the component level. I can fix it by making a new var and setting 'this' before I make the request. I know that this should be working though. What am I missing?
My entire component
import React, { Component } from 'react';
import axios from '../../axios';
class CoinViewer extends Component {
state = {
coin: {},
hasLoaded: false,
id: ''
}
componentDidMount() {
console.log('componentDidMount');
//const self = this;
let _id = this.props.match.params.id.toUpperCase();
if (_id != this.state.id.toUpperCase()) {
axios.get('/data/pricemultifull?fsyms=' + _id + '&tsyms=USD')
.then( resp => {
// let _currentcoin = { ...resp.data.RAW.BTC.USD, ticker: _id };
this.setState({ id: _id });
});
}
}
componentWillMount() {
}
componentWillUpdate() {
}
componentDidUpdate() {
}
getCompleteCoinData(_id) {
}
render() {
return (
<div>
CoinViewer Component: {this.state.id} sads
</div>
)
}
}
export default CoinViewer
Solution 1: arrow functions..
requestSuccess = (resp) => {
// let _currentcoin = { ...resp.data.RAW.BTC.USD, ticker: _id };
this.setState({ id: _id });
}
componentDidMount() {
console.log('componentDidMount');
//const self = this;
let _id = this.props.match.params.id.toUpperCase();
if (_id != this.state.id.toUpperCase()) {
axios.get('/data/pricemultifull?fsyms=' + _id + '&tsyms=USD')
.then(this.requestSuccess);
}
}
Solution 2: binding
componentDidMount() {
console.log('componentDidMount');
//const self = this;
let _id = this.props.match.params.id.toUpperCase();
if (_id != this.state.id.toUpperCase()) {
axios.get('/data/pricemultifull?fsyms=' + _id + '&tsyms=USD')
.then((resp) => {
// let _currentcoin = { ...resp.data.RAW.BTC.USD, ticker: _id };
this.setState({ id: _id });
}.bind(this));
}
}
:Edit
Wow, the below is kinda true, but the real issue is you didn't initialize state. https://reactjs.org/docs/react-component.html#constructor
constructor() {
super();
this.state = {
coin: {},
hasLoaded: false,
id: ''
}
}
You could use lexical scoping and fix like this, this is a popular pattern to protect this.
Basically, when you use promises or functions from other libraries/ APIs you do not know what they have set their context inside the callback functions to.
In order to use the context you want, you keep the context you need saved in a variable within scope and reference it there _this, rather than by pointing to the context this. I'd recommend reading 'you dont know js' to understand this concept further.
componentDidMount() {
console.log('componentDidMount');
const _this = this;
let _id = _this.props.match.params.id.toUpperCase();
if ( _id != _this.state.id.toUpperCase() ) {
axios.get('/data/pricemultifull?fsyms=' + _id + '&tsyms=USD')
.then(response => {
_this.setState({ id: _id }); //this == undefined
});
}
}
When working with React.js, chances are you have faced a problem how
to access this from inside the promise.There is more than one solution to resolve this reference inside the
promise. The old approach would be setting the self = this
reference While this would work, the recommended solution, which is
more inline with ES6, would be to use an arrow function here:
class Component extends React.Component {
componentDidMount() {
let component = this;
axios.get('http://…').then(function(data) {
component.setState( { name: data.blah } );
});
}
}
The arrow syntax, as stated above, is a much smarter way to allow use
of this to make reference to React.Component classes, as we can see
below:
class Component extends React.Component {
componentDidMount() {
axios.get('http://…').then(data => {
this.setState( { name: data.blah } );
});
}
}
Please note, instead of using
function(data) { //body },
we used data => { //body }, and in this case this reference won’t get the promise instance back.
Related
I'm new to VueJs and currently trying to load some data only once and make it globally available to all vue components. What would be the best way to achieve this?
I'm a little bit stuck because the global variables occasionally seem to become null and I can't figure out why.
In my main.js I make three global Vue instance variables:
let globalData = new Vue({
data: {
$serviceDiscoveryUrl: 'http://localhost:40000/api/v1',
$serviceCollection: null,
$clientConfiguration: null
}
});
Vue.mixin({
computed: {
$serviceDiscoveryUrl: {
get: function () { return globalData.$data.$serviceDiscoveryUrl },
set: function (newUrl) { globalData.$data.$serviceDiscoveryUrl = newUrl; }
},
$serviceCollection: {
get: function () { return globalData.$data.$serviceCollection },
set: function (newCollection) { globalData.$data.$serviceCollection = newCollection; }
},
$clientConfiguration: {
get: function () { return globalData.$data.$clientConfiguration },
set: function (newConfiguration) { globalData.$data.$clientConfiguration = newConfiguration; }
}
}
})
and in my App.vue component I load all the data:
<script>
export default {
name: 'app',
data: function () {
return {
isLoading: true,
isError: false
};
},
methods: {
loadAllData: function () {
this.$axios.get(this.$serviceDiscoveryUrl)
.then(
response => {
this.$serviceCollection = response.data;
let configurationService = this.$serviceCollection.services.find(obj => obj.key == "ProcessConfigurationService");
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
}
);
this.isLoading = false;
})
}
},
created: function m() {
this.loadAllData();
}
}
</script>
But when I try to access the $clientConfiguration it seems to be null from time to time and I can't figure out why. For example when I try to build the navigation sidebar:
beforeMount: function () {
let $ = JQuery;
let clients = [];
if (this.$clientConfiguration === null)
console.error("client config is <null>");
$.each(this.$clientConfiguration, function (key, clientValue) {
let processes = [];
$.each(clientValue.processConfigurations, function (k, processValue) {
processes.push(
{
name: processValue.name,
url: '/process/' + processValue.id,
icon: 'fal fa-project-diagram'
});
});
clients.push(
{
name: clientValue.name,
url: '/client/' + clientValue.id,
icon: 'fal fa-building',
children: processes
});
});
this.nav.find(obj => obj.name == 'Processes').children = clients;
The most likely cause is that the null is just the initial value. Loading the data is asynchronous so you'll need to wait for loading to finish before trying to create any components that rely on that data.
You have an isLoading flag, which I would guess is your attempt to wait for loading to complete before showing any components (maybe via a suitable v-if). However, it currently only waits for the first request and not the second. So this:
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
}
);
this.isLoading = false;
would need to be:
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
this.isLoading = false;
}
);
If it isn't that initial value that's the problem then you need to figure out what is setting it to null. That should be prety easy, just put a debugger statement in your setter:
$clientConfiguration: {
get: function () { return globalData.$data.$clientConfiguration },
set: function (newConfiguration) {
if (!newConfiguration) {
debugger;
}
globalData.$data.$clientConfiguration = newConfiguration;
}
}
Beyond the problem with the null, if you're using Vue 2.6+ I would suggest taking a look at Vue.observable, which is a simpler way of creating a reactive object than creating a new Vue instance.
Personally I would probably implement all of this by putting a reactive object on Vue.prototype rather than using a global mixin. That assumes that you even need the object to be reactive, if you don't then this is all somewhat more complicated than it needs to be.
I am doing a task where I need to wire up a search field to a simple JS application that displays a few items and the user can search through and filter them.
There are three classes - App, ProductsPanel and Search. Both Search and ProductsPanel are being initialised inside the App class.
The ProductsPanel class holds an array with 10 products.
I want to call a method of ProductsPanel from inside Search that filters through the products. How can I do that?
I've tried using this.productsPanel = new productsPanel() inside the constructor of the first class, but that brings up a new instance which doesn't have the array of all of the products.
Here's the App class:
class App {
constructor() {
this.modules = {
search: {
type: Search,
instance: null
},
filter: {
type: Filter,
instance: null
},
productsPanel: {
type: ProductsPanel,
instance: null
},
shoppingCart: {
type: ShoppingCart,
instance: null
}
};
}
init() {
const placeholders = document.querySelectorAll("#root [data-module]");
for (let i = 0; i < placeholders.length; i++) {
const root = placeholders[i];
const id = root.dataset.module;
const module = this.modules[id];
if (module.instance) {
throw new Error(`module ${id} has already been started`);
}
module.instance = new module.type(root);
module.instance.init();
// console.info(`${id} is running...`);
}
}
}
app = new App();
app.init();
And here are the Search:
export default class Search {
constructor(root) {
this.input = root.querySelector("#search-input");
}
// addEventListener is an anonymous function that encapsulates code that sends paramaters to handleSearch() which actually handles the event
init() {
this.input.addEventListener("input", () => {
this.handleSearch();
});
}
handleSearch() {
const query = this.input.value;
app.modules.productsPanel.instance.performSearch(query);
}
}
And ProductsPanel classes:
export default class ProductsPanel {
constructor(root) {
this.view = new ProductsPanelView(root, this);
this.products = [];
}
init() {
this.products = new ProductsService().products;
this.products.forEach(x => this.view.addProduct(x));
}
performSearch(query) {
query = query.toLowerCase();
this.products.forEach(p => {
if (query === p.name) {
this.view.showProduct(p.id);
} else {
this.view.hideProduct(p.id);
}
});
}
addToCart(id) {
const product = this.products.filter(p => p.id === id)[0];
if (product) {
app.modules.shoppingCart.instance.addProduct(product);
}
}
}
I want to call ProductsPanel's performSearch method but on the instance created by the App class. I have no clue on how I can do that.
Try below custom event handler class
class CustomEventEmitter {
constructor() {
this.eventsObj = {};
}
emit(eName, data) {
const event = this.eventsObj[eName];
if( event ) {
event.forEach(fn => {
fn.call(null, data);
});
}
}
subscribe(eName, fn) {
if(!this.eventsObj[eName]) {
this.eventsObj[eName] = [];
}
this.eventsObj[eName].push(fn);
return () => {
this.eventsObj[eName] = this.events[eName].filter(eventFn => fn !== eventFn);
}
}
}
How to use?
create the object of CustomEventEmitter class
let eventEmitter = new CustomEventEmitter()
Subscribe an event
emitter.subscribe('event: do-action', data => {
console.log(data.message);
});
call the event
emitter.emit('event: do-action',{message: 'My Custom Event handling'});
Hope this helps!
I'm stuck at a crossroads with a component I am working on.
I have the following component "RecentUpdates"
Within it I am passing props down to a few other components, as you can see from the top of the file.
My problem is when adding a new post, I can not figure out how to get the correct update object array back and i also can not figure out the correct 'Vue way' to update the data prop that is being passed down to the "PostList" component.
<template>
<div>
<PostFilter v-on:selectedCategory="getSelectedPosts" v-on:showAllPosts="showAllPosts" :user="user" :categories="categories"/>
<PostList v-if="recent_posts[0]" :categories="categories" :posts="recent_posts[0]" :user="user"/>
<Pagination v-on:getPreviousPage="getPreviousPage" v-on:getNextPage="getNextPage"/>
</div>
</template>
<script>
import PostList from './PostList';
import PostFilter from './PostFilter';
import Pagination from './Pagination';
import EventBus from '../event-bus';
export default {
name: 'RecentUpdates',
data: () => ({
errors: [],
recent_posts: [],
}),
props: ['categories', 'user'],
components: {
PostList,
PostFilter,
Pagination
},
created() {
if (this.user.meta.selected_categories[0] == 0) {
this.showAllPosts();
}
// do not call here, not working as expected
// is switching selected category to an incorrect one
// this.updateList();
this.getSelectedCategory();
},
watch: {
recent_posts: function(newValue) {
EventBus.$on('addPost', function(newPost) {
console.log(newPost);
this.$forceUpdate();
//this.recent_posts.push(newPost);
//this.$set(this.recent_posts, newPost, newPost);
// this.$nextTick(function () {
// this.recent_posts.push(newPost);
// });
});
console.log(this.recent_posts[0]);
// this.$nextTick(function () {
// console.log(this.recent_posts[0]) // => 'updated'
// });
// if (this.user.meta.selected_categories[0] == 0) {
// EventBus.$on('addPost', this.showAllPosts);
// } else {
// EventBus.$on('addPost', this.getSelectedCategory);
// }
//this.updateList();
}
},
methods: {
// updateList() {
// if (this.user.meta.selected_categories[0] == 0) {
// EventBus.$on('addPost', this.showAllPosts);
// //EventBus.$emit('newPost');
// } else {
// EventBus.$on('addPost', this.getSelectedCategory);
// //EventBus.$emit('newPost');
// }
// },
getSelectedCategory() {
let categoryId = this.user.meta.selected_categories[0];
this.getSelectedPosts(categoryId);
},
showAllPosts() {
axios.get('/wp-json/wp/v2/posts?_embed=true&status=[publish,resolved,unresolved]',
{headers: {'X-WP-Nonce': portal.nonce}})
.then(response => {
this.recent_posts = [];
//this.recent_posts = response.data;
//console.log(response.data);
this.recent_posts.push(response.data);
console.log(this.recent_posts[0]);
})
.catch(e => {
this.errors.push(e);
});
},
getSelectedPosts(categoryId) {
axios.get('/wp-json/wp/v2/posts?_embed=true&status=[publish,resolved,unresolved]&categories=' + categoryId,
{headers: {'X-WP-Nonce': portal.nonce}})
.then(response => {
this.recent_posts = [];
//console.log(response.data);
this.recent_posts.push(response.data);
console.log(this.recent_posts[0]);
})
.catch(e => {
this.errors.push(e);
});
},
/**
* Pagination methods
*
*/
getPreviousPage(page) {
axios.get('/wp-json/wp/v2/posts?_embed=true&status=[publish,resolved,unresolved]&page=' + page,
{headers: {'X-WP-Nonce': portal.nonce}})
.then(response => {
this.recent_posts = response.data;
})
.catch(e => {
this.errors.push(e);
});
},
getNextPage(page) {
axios.get('/wp-json/wp/v2/posts?_embed=true&status=[publish,resolved,unresolved]&page=' + page,
{headers: {'X-WP-Nonce': portal.nonce}})
.then(response => {
this.recent_posts = response.data;
})
.catch(e => {
this.errors.push(e);
});
}
},
}
</script>
<style>
</style>
So there are a number of issues I see reading through your code.
You have a recent_posts data property, which is an array. When you make your ajax call to get the posts you push the response which is also an array into the recent_posts array. Why? Why not just set recent_posts = response.data? Then you won't have to be passing recent_posts[0] around.
You're setting up your EventBus handler inside a watcher. This is really unusual. Typically you would set up a handler inside created or mounted.
this inside the EventBus handler likely refers to the EventBus and not your Vue. Ideally, you would set the handler to be a method on the component, which is already bound to the Vue. Something like EventBus.$on("addPost", this.addPost).
Once you've done all that, adding a new post should be as simple as this.recent_posts.push(newPost).
Here is what I might recommend.
export default {
name: 'RecentUpdates',
data(){
return {
errors: [],
recent_posts: []
}
},
props: ['categories', 'user'],
components: {
PostList,
PostFilter,
Pagination
},
created() {
if (this.user.meta.selected_categories[0] == 0) {
this.showAllPosts();
}
this.getSelectedCategory();
EventBus.$on("addPost", this.addPost)
},
beforeDestroy(){
EventBus.$off("addPost", this.addPost)
},
methods: {
getPosts(url){
axios.get(url, {headers: {'X-WP-Nonce': portal.nonce}})
.then(response => this.recent_posts = response.data)
.catch(e => this.errors.push(e))
},
showAllPosts() {
const url = '/wp-json/wp/v2/posts?_embed=true&status=[publish,resolved,unresolved]';
this.getPosts(url);
},
getSelectedPosts(categoryId) {
const url = '/wp-json/wp/v2/posts?_embed=true&status=[publish,resolved,unresolved]&categories=' + categoryId;
this.getPosts(url);
},
addPost(newPost){
this.recent_posts.push(newPost)
},
... //other methods
},
}
Try using kebab-case in your event listeners instead of camelCase:
Example: v-on:selectedCategory="getSelectedPosts" should be v-on:selected-category="getSelectedPosts".
Example: v-on:showAllPosts="showAllPosts" should be v-on:show-all-posts="showAllPosts" or even using the shortcut #show-all-posts="showAllPosts".
UPDATE: If you can provide the code of the other components so we can have a clearer vision of your problem, But you only want to track changes that happens on an object or an array in vue.js you need to deep watch them.
your watcher should be :
watch: {
recent_posts: {
deep: true,
handler: function( oldValue, newValue) {
console.log( "recent_posts has changed" );
// A post has been added, updated or even deleted
}
}
}
Im trying to pass a simple list from a parent to a set of child components and i cant pass down a function. i have something line for line that is the exact same bloody thing that works but this does not.
Does anyone see what is going wrong?
This is the function in the parent that i cant reach (the console.log is not firing):
setClassImage(name, image){
console.log('inside setClassImage');
var TitleArray = this.state.TitleArray;
TitleArray[0] = name;
var ImageArray = this.state.ImageArray;
ImageArray[0] = image;
this.setState({
characterModal: false,
equipmentModal: false,
TitleArray: TitleArray,
ImageArray: ImageArray
});
}
Setting the list in the parent:
let listClasses;
if(this.state.classResults.length!=0){
listClasses = this.state.classResults.map((item,i) => {
return (
<ListClass key={i} classes={item} setClassImage={this.setClassImage.bind(this)}/>
);
});
}
in the render of the parent:
<SelectorBox>
<ListBox>
{listClasses}
<br/>
</ListBox>
<SelectorButton onClick={(e)=>this.cancelChange(e)}>No Class</SelectorButton>
</SelectorBox>
In the child:
class ListClass extends Component{
constructor(props){
super(props);
this.state={
}
}
getImage(e){
e.preventDefault();
var self = this;
axios.post('http://localhost:5000/post2/characterimage', {
search: this.props.classes,
})
.then((response)=>{
console.log('response from the pixabay call ', response.data);
self.props.setClassImage(self.props.classes, response.data);
})
.catch(()=>{
console.log('hello axios error');
});
}
render(){
return(
<div>
<ItemContainer onClick={(e)=>this.getImage(e)}>
<p>
{this.props.classes}
</p>
</ItemContainer>
</div>
)
}
}
I can see console.log('response from the pixabay call ', response.data); and it fires correctly from data on the backend. But the function is not called! The weirdly named containers are purely for styling, so just assume they all act like divs.
The arrow function in your .then() call will automatically bind this to the proper context, so you don't need to do the var self = this; However you do need to bind your getImage function in your constructor. OR
Update your function definition to:
....
getImage = (e) => {
e.preventDefault();
axios.post('http://localhost:5000/post2/characterimage', {
search: this.props.classes,
})
.then((response) => {
console.log('response from the pixabay call ', response.data);
this.props.setClassImage(this.props.classes, response.data);
})
.catch(()=>{
console.log('hello axios error');
});
}
....
}
Otherwise, bind getImage in your constructor:
constructor(props){
super(props);
this.state = {};
this.getImage = this.getImage.bind(this);
}
I am building a React app using altjs as my Flux implementation. When I try to create/delete an item from the front end, no matter what I pass as a parameter to the create/delete function, it always ends up as passing the entire state.
For example : I'm trying to delete an item with id=1. I click delete on that item and pass just the id to the delete function in the component. That function calls the delete service again passing the id. Once that gets to the store layer, it has the entire state of the component and not just the id that is passed.
I'm still fairly new to React/Flux and not sure what I'm doing wrong or why this is happening.
Main component delete function :
deleteItem = (id) => {
console.log(id) //logs out 56b36c34ad9c927508c9d14f
QuestionStore.deleteQuestion(id);
}
At this point id is still just the id.
QuestionStore :
import alt from '../core/alt';
import QuestionActions from '../actions/QuestionActions';
import QuestionSource from '../sources/QuestionSource';
class QuestionStore {
constructor() {
this.bindActions(QuestionActions);
this.exportAsync(QuestionSource);
this.loaded = false;
this.modalIsOpen = false;
this.data = [];
this.question = {
"text": '',
"tag": [],
"answer": [],
"company": [],
"createdAt": ''
};
this.error = null;
this.questionAdded = null;
this.questionDeleted = null;
}
onGetQuestions(data) {
if (data === false) {
this.onFailed();
} else {
this.data = data;
this.loaded = true;
}
}
onCreateQuestion(response) {
if (response === false) {
this.onFailed();
} else {
this.questionAdded = response;
}
}
onDeleteQuestion(response) {
if (response === false) {
this.onFailed();
} else {
this.questionDeleted = response;
}
}
onFailed(err) {
this.loaded = true;
this.error = "Data unavailable";
}
}
export default alt.createStore(QuestionStore, 'QuestionStore');
QuestionSource :
import Api from '../services/QuestionApi';
import QuestionActions from '../actions/QuestionActions';
let QuestionSource = {
fetchData() {
return {
async remote(state) {
return Api.getQuestions()
},
success: QuestionActions.getQuestions
}
},
createQuestion(question) {
return {
async remote(question) {
return Api.createQuestion(question)
},
success: QuestionActions.createQuestion
}
},
deleteQuestion(id) {
//id here is an object of the entire state of QuestionStore
return {
async remote(id) {
return Api.deleteQuestion(id)
},
success: QuestionActions.deleteQuestion
}
}
};
export default QuestionSource;
Once it hits this point, id is now the entire state of the component even though only the id is passed.
The first parameter that is bound to the action is the state of the store (part of the result of the exportAsync call. So all parameters shift one to the right, and the first parameter you call the function with in turn becomes the second parameter. See below code example:
deleteQuestion(state, id) {
//state here is an object of the entire state of QuestionStore
//id will be the first argument you provide to the function.
return {
async remote(id) {
return Api.deleteQuestion(id)
},
success: QuestionActions.deleteQuestion
}
}
Documentation from alt.js about handling async operations.