In component data is fetched only when i make some changes in it - node.js

I am trying to add markers on map, using a view and component
In view where i call an api and then i pass that data to component using v:bind but console.log in that component shows an empty array, when i make some changes in that component the page reloads and data is fetched following is my code in view and then component.
//Script for View
<script>
import Maps from '../components/Maps.vue';
import LoadingOverlay from '../components/loading.vue';
import MessageHelper from '../helpers/messageHelper';
import RepositoryFactory from '../repositories/RepositoryFactory';
const Catalogs = RepositoryFactory.get('catalogs');
export default {
components: {
Maps,
LoadingOverlay,
},
data() {
return {
zones_and_locations: [],
loadingConfig: {
isLoading: true,
cancellable: true,
onCancelMessage: this.onCancelMessage(),
isFullPage: true,
},
};
},
created() {
this.fetchPlacesData();
},
methods: {
async fetchPlacesData() {
const { data } = await Catalogs.getZoneLocations();
if (data.output === null) {
this.nullDataException();
} else {
const places = [];
data.output.forEach((value, index) => {
places.push(value);
});
this.zones_and_locations = places;
this.loadingConfig.isLoading = false;
}
},
onCancelMessage() {
return MessageHelper.getLoadingCancelMessage();
},
nullDataException() {
this.exceptionMessage = 'Data from API is not available.';
console.log(this.exceptionMessage);
},
},
};
</script>
//Script For Map.vue
<script>
import MarkerClusterer from '#google/markerclusterer';
import GoogleMapsHelper from '../helpers/GoogleMapsHelper';
export default {
name: 'GoogleMap',
props: ['zones_and_locations'],
data() {
return {
country: '',
markers: [],
map: '',
};
},
created() {
this.loadGoogleMaps();
},
methods: {
markerClusterer(map, markers) {
return new MarkerClusterer(
map,
markers,
{ imagePath: 'https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m' },
);
},
async loadGoogleMaps() {
try {
const google = await GoogleMapsHelper();
const geoCoder = new google.maps.Geocoder();
const map = new google.maps.Map(document.getElementById('map'), {
zoom: 7,
mapTypeControl: false,
fullscreenControl: false,
});
geoCoder.geocode({ address: 'Singapore' }, (results, status) => {
if (status !== 'OK' || !results[0]) {
throw new Error(status);
}
// set Center of Map
map.setCenter(results[0].geometry.location);
map.fitBounds(results[0].geometry.viewport);
});
this.map = map;
let zones = [];
Array.prototype.forEach.call(this.zones_and_locations, child => {
var obj = {
lat: parseFloat(child.lat),
lng: parseFloat(child.lng),
}
var position = {
position: obj,
};
zones.push(position);
});
if (zones.length > 0) {
const markers = zones.map(x => new google.maps.Marker({ ...x, map }));
this.markerClusterer(map, markers);
}
} catch (error) {
console.error(error);
}
},
},
};
</script>
//Template for View
<template>
<div>
<!-- START::Loading Overlay -->
<LoadingOverlay
v-bind:loadingConfig="loadingConfig">
</LoadingOverlay><!-- END::Loading Overlay -->
<Maps v-bind:zones_and_locations="zones_and_locations"
></Maps>
</div>
</template>
//Template for Component
<template>
<div>
<div id="map" class="google-map"></div>
</div>
</template>

Data from the parent component are loaded asynchronously, so the created lifecycle hook inside the component is executed before the actual data comes, when they're still set as an empty array and are not reactive to change.
You can fix this by setting a watch inside the component, like this:
methods: {
...
},
watch: {
zones_and_locations (newVal, oldVal) {
this.loadGoogleMaps();
}
}
orelse you can set a reference to the child component and invoke its method when data comes:
<!-- main view -->
<Maps
v-bind:zones_and_locations="zones_and_locations"
ref="myMap"
></Maps>
async fetchPlacesData() {
const { data } = await Catalogs.getZoneLocations();
if (data.output === null) {
this.nullDataException();
} else {
const places = [];
data.output.forEach((value, index) => {
places.push(value);
});
this.zones_and_locations = places;
// using $refs
this.$refs.myMap.loadGoogleMaps();
this.loadingConfig.isLoading = false;
}
},

Related

How to make reusable pagination with react and redux?

On my application i'm using Reduxjs/toolkit for state management and TypeScript for type safety.
My backend were wrote in Node.js with MongoDB.
I have implemented pagination for one slice/component, and i know that is not the best solution and i want to improve it and make reusable for other slices.
Could you help me with that? Give me some hints?
Below is my current pagination implementation:
// CategorySlice
interface InitialState {
categories: ICategory[];
isFetching: boolean;
errorMessage: string | null;
// to refactor
currentPage: number;
itemsPerPage: number;
totalResults: number;
}
const initialState: InitialState = {
categories: [],
isFetching: false,
errorMessage: '',
// to refactor
currentPage: 1,
itemsPerPage: 9,
totalResults: 0,
};
export const fetchCategories = createAsyncThunk<
{ data: ICategory[]; totalResults: number },
number
>('category/fetchCategories', async (currentPage, { rejectWithValue }) => {
try {
const accessToken = getToken();
if (!accessToken) rejectWithValue('Invalid token');
const config = {
headers: { Authorization: `Bearer ${accessToken}` },
};
const response: IApiResponse<ICategoryToConvert[]> = await api.get(
`/categories?page=${currentPage}&limit=9&isPrivate[ne]=true`,
config
);
const data = response.data.data;
const convertedData = data.map(e => {
return {
id: e._id,
name: e.name,
image: e.image,
};
});
return {
totalResults: response.data.totalResults,
data: convertedData,
};
} catch (error) {
removeToken();
return rejectWithValue(error);
}
});
export const categorySlice = createSlice({
name: 'category',
initialState,
reducers: {
setNextPage(state, { payload }) {
state.currentPage = payload;
},
},
extraReducers: builder => {
builder.addCase(fetchCategories.pending, state => {
state.isFetching = true;
state.errorMessage = null;
});
builder.addCase(fetchCategories.fulfilled, (state, action) => {
state.categories = action.payload.data;
state.isFetching = false;
state.totalResults = action.payload.totalResults;
});
builder.addCase(fetchCategories.rejected, state => {
state.isFetching = false;
state.errorMessage = 'Problem with fetching categories 🐱‍👤';
});
},
});
// Category Page
const CategoryPage = () => {
const dispatch = useAppDispatch();
const { currentPage } = useAppSelector(state => state.category);
useEffect(() => {
dispatch(fetchCategories(currentPage));
}, [dispatch, currentPage]);
return (
<ContainerWrapper>
<CategoryList />
</ContainerWrapper>
);
};
export default CategoryPage;
Inside CategoryPage
I'm passing those properties from state selector.
<Pagination
currentPage={currentPage}
itemsPerPage={itemsPerPage}
paginate={(n: number) => dispatch(categoryActions.setNextPage(n))}
totalItems={totalResults}
/>
And finally PaginationComponent
interface IProps {
itemsPerPage: number;
totalItems: number;
paginate: (numb: number) => void;
currentPage: number;
}
const Pagination = ({ itemsPerPage, totalItems, paginate, currentPage }: IProps) => {
const numberOfPages = [];
for (let i = 1; i <= Math.ceil(totalItems / itemsPerPage); i++) {
numberOfPages.push(i);
}
return (
<nav className={styles['pagination']}>
<ul className={styles['pagination__list']}>
{numberOfPages.map(number => {
return (
<li
key={number}
className={`${styles['pagination__item']} ${
currentPage === number && styles['pagination__item--active']
}`}
onClick={() => paginate(number)}
>
<div className={styles['pagination__link']}>{number}</div>
</li>
);
})}
</ul>
</nav>
);
};
export default Pagination;

custom element error in jointjs when use vue-cli

I got an error when use my element in vue-cli.
I do not why joint.shapes do not contain my element!
test.vue:
// Cannot read property 'Component' of undefined"
<script>
const joint = require('jointjs');
joint.dia.Element.define('example.Component', {
attrs: {
...
},
}, {
markup: ...,
}, {
createComponent: function() {
return new this({
attrs:{
...
}
});
}
});
export default {
name: "FlowCanvas",
mounted() {
var graph=new joint.dia.Graph;
var paper = new joint.dia.Paper({
...
});
var el1=joint.shapes.example.Component.createComponent();
el1.position(100,100);
},
}
</script>

Mediafilepicker video file selection(display video on the app)

I am using Mediafilepicker plugin to pick a video file from my device, but when I select the video it doesn't display on the app but once I press (pick a video) button for the second time its when the video shows
I searched for a similar problem but didn't find any
public pickvideo(){
let options: VideoPickerOptions = {
android: {
isCaptureMood: false,
isNeedCamera: true,
maxNumberFiles: 1,
isNeedFolderList: true,
maxDuration: 20,
},
};
let mediafilepicker = new Mediafilepicker();
mediafilepicker.openVideoPicker(options);
mediafilepicker.on("getFiles", res => {
let results = res.object.get('results');
this.videoSrc = results[0].file;
console.dir(results);
if (results) {
for (let i = 0; i < results.length; i++) {
let result = results[i];
console.dir(result);
let file = result.file;
console.log(file);
}
}
It doesn't show any errors
Use this code for pick a video from gallery and show in the app.
media-picker.component.ts:-
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from "#angular/core";
import { Page } from "tns-core-modules/ui/page";
import * as app from 'tns-core-modules/application';
import { Mediafilepicker, VideoPickerOptions } from 'nativescript-mediafilepicker';
declare const AVCaptureSessionPreset1920x1080, AVCaptureSessionPresetHigh;
#Component({
selector: "media-picker",
moduleId: module.id,
templateUrl: "./media-picker.component.html",
styleUrls: ["./media-picker.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MideaPickerComponent implements OnInit {
public videoFileUrl: Array<string> = [];
constructor(public page: Page,
private ref: ChangeDetectorRef) {
// Use the constructor to inject services.
// Get reference to object we want to animate with code
}
ngOnInit(): void {
}
//Open video gallery list
public openVideoGallery() {
let allowedVideoQualities = [];
if (app.ios) {
allowedVideoQualities = [AVCaptureSessionPreset1920x1080, AVCaptureSessionPresetHigh];
}
let options: VideoPickerOptions = {
android: {
isCaptureMood: false,
isNeedCamera: true,
maxNumberFiles: 2,
isNeedFolderList: true,
maxDuration: 20,
},
ios: {
isCaptureMood: false
}
};
let mediafilepicker = new Mediafilepicker();
mediafilepicker.openVideoPicker(options);
mediafilepicker.on("getFiles", (res) => {
let results = res.object.get('results');
if (results) {
this.videoFileUrl = [];
for (let i = 0; i < results.length; i++) {
let result = results[i];
let file = result.file;
this.videoFileUrl.push(file);
if (result.file && app.ios && !options.ios.isCaptureMood) {
let fileName = file.replace(/^.*[\/]/, '');
setTimeout(() => {
mediafilepicker.copyPHVideoToAppDirectory(result.urlAsset, fileName).then(res => {
console.dir(res);
}).catch(e => {
console.dir(e);
});
}, 1000);
} else if (result.file && app.ios) {
// or we will get our own recorded video :)
console.log(file);
}
}
}
});
mediafilepicker.on("error", (res) => {
let msg = res.object.get('msg');
console.log(msg);
});
mediafilepicker.on("cancel", (res) => {
let msg = res.object.get('msg');
console.log(msg);
});
setInterval(() => {
// require view to be updated
this.ref.markForCheck();
}, 500);
}
}
media-picker.component.html:-
<StackLayout row="0">
<Button height="50" (tap)="openVideoGallery()" text="Open Video Gallery">
</Button>
</StackLayout>
<StackLayout row="1">
<VideoPlayer *ngFor="let video of videoFileUrl" src="{{video}}" autoplay="true" height="300"></VideoPlayer>
</StackLayout>

How do I trigger a v-autocomplete "input" event with vue test utils?

I'm trying to trigger the "input" event of a Vuetify v-autocomplete component inside of a unit test. From what I can tell, the unit test is quite similar to the one the Vuetify guys are using, but for some reason the mock method is never called.
The factory method simply returns a component instance made via mount and adds a local store, vue, and vueLoading instance.
Would love a bit of insight here, already lost several hours to this and I'm starting to go a bit mad...
Vuetify version: 1.4.3
Component
<template>
<v-autocomplete
:items="options"
:value="selectedId"
:label="label || $t('user.filter')"
:dark="dark"
:class="fieldClass"
:menu-props="{ contentClass }"
:loading="$loading.isLoading('fetch users')"
item-text="email"
item-value="id"
name="search_by_user"
hide-details
single-line
clearable
#input="updateValue($event)"
#click.native="fetchUsers"
/>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
props: {
value: {
type: String,
required: false,
default: null
},
label: {
type: String,
required: false,
default: null
},
dark: {
type: Boolean,
required: false,
default: true
},
fieldClass: {
type: String,
required: false,
default: 'select-user-search'
},
contentClass: {
type: String,
required: false,
default: 'user-search'
},
blankItem: {
type: Object,
required: false,
default: null
}
},
data () {
return {
selectedId: this.value
}
},
computed: {
...mapGetters(['users']),
options () { return this.blankItem ? [this.blankItem].concat(this.users) : this.users }
},
created () {
if (this.value) this.fetchUsers()
},
methods: {
fetchUsers () {
if (this.users.length) return
this.$store.dispatch('FETCH_USERS', {
fields: ['id', 'email'],
filter: { active: 'true' }
})
},
updateValue (value) {
this.selectedId = value
this.$emit('input', value)
}
}
}
</script>
<style>
.select-user-search {
overflow: hidden;
padding-bottom: 1px;
}
.select-user-search .v-select__selections {
overflow: hidden;
}
.select-user-search .v-select__selection {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
Unit Test
import { mount, shallowMount, createLocalVue } from '#vue/test-utils'
import Vuex from 'vuex'
import VueLoading from 'vuex-loading'
import Vuetify from 'vuetify'
import UserSearch from 'manager/components/user/search.vue'
const factory = (values, shallow = true) => {
if (!shallow) {
return mount(UserSearch, { ...values })
}
return shallowMount(UserSearch, { ...values })
}
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(VueLoading)
localVue.use(Vuetify)
describe('UserSearch', () => {
let actions, getters, store, vueLoading
beforeEach(() => {
actions = {
FETCH_USERS: jest.fn()
}
getters = {
users: () => []
}
vueLoading = new VueLoading({ useVuex: true })
store = new Vuex.Store({
actions,
getters
})
})
it('calls "updateValue" method when input triggered', async () => {
const methods = {
updateValue: jest.fn()
}
const wrapper = factory({ methods, store, localVue, vueLoading }, false)
const input = wrapper.find('input')
input.element.value = 'value'
input.trigger('input')
await wrapper.vm.$nextTick()
expect(methods['updateValue']).toBeCalledWith('value')
})
})
I use the following variant
Component
<template>
<span> <!-- span (or the other HTML element) around <v-autocomplete> is important -->
<v-autocomplete
:items="options"
:value="selectedId"
:label="$t('user.filter')"
:menu-props="{ contentClass }"
:loading="false"
item-text="email"
item-value="id"
name="search_by_user"
hide-details
single-line
clearable
#input="updateValue($event)"
#click.native="fetchUsers"
/>
</span>
</template>
<script lang="ts">
import { Component } from 'vue-property-decorator';
import Vue from 'vue';
#Component
export default class AppAutocomplete extends Vue {
private options: any[] = [{email: 'box1#gmail.com', id: 1}];
private selectedId: string|undefined = undefined;
private contentClass = 'user-search';
private fetchUsers() {
}
public updateValue(event: any) {
}
}
</script>
Unit test
import { Wrapper, createLocalVue, mount } from '#vue/test-utils';
import Vue from 'vue';
import Vuetify from 'vuetify';
// #ts-ignore
import AppAutocomplete from '#/components/AppAutocomplete.vue';
describe('AppAutocomplete', () => {
Vue.use(Vuetify);
let wrapper: Wrapper<Vue>;
let vm: any;
beforeEach(() => {
wrapper = mount(AppAutocomplete, {
localVue: createLocalVue(),
vuetify: new Vuetify({
mocks: { $vuetify: { lang: { t: (val: string) => val } } },
}),
mocks: { $t: () => { /** */ } },
});
vm = wrapper.vm;
});
afterEach(() => wrapper.destroy());
it(`calls "updateValue" method when input triggered`, () => {
jest.spyOn(vm, 'updateValue');
const autocompleteElem = wrapper.find('.v-autocomplete');
autocompleteElem.vm.$emit('input', 'box1');
expect(vm.updateValue).toBeCalledTimes(1);
expect(vm.updateValue).toHaveBeenCalledWith('box1');
});
});
To surround v-autocomplete by some other HTML element is important. Without it a test fails.
Vuetify version: 2.1.12

Action creator is being dispatched but the new state is not showing in store

I have written a server and wired up React with Redux, also made use of container-componenent-seperation. The first action fetchSnus() is successfully dispatched, the selectors also seem to work, but for some reason all the actions that I call after the first rendering, such as fetchSnu() (that only fetches ONE single snu-object) e.g. is not accessible for me in the store, meaning: after mapping state and dispatch to props not accessible for me under this.props. I really don't understand why!
You can see the whole project here: https://github.com/julibi/shonagon
This is my container ReadSingleSnuContainer.js:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchSnus, fetchSnu, setToRead, createSnu, getSnusMatchingKeyword } from '../../actions/index';
import { unreadSnus, randomFirstSnu } from '../../selectors/index';
import ReadSingleSnu from './ReadSingleSnu';
class ReadSingleSnuContainer extends Component {
componentWillMount() {
this.props.fetchSnus();
}
render() {
return (
<ReadSingleSnu { ...this.props } />
);
}
}
const mapStateToProps = (state) => {
return {
snus: state.snus,
randomFirstSnu: randomFirstSnu(state),
candidate: state.candidate
};
}
const mapDispatchToProps = (dispatch) => {
return {
fetchSnus: () => dispatch(fetchSnus()),
getSnusMatchingKeyword,
fetchSnu,
setToRead,
createSnu
}
};
export default connect(mapStateToProps, mapDispatchToProps)(ReadSingleSnuContainer);
This is my component ReadSingleSnu.js:
import React, { Component } from 'react';
import style from './ReadSingleSnu.css'
import Typist from 'react-typist';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
export default class ReadSingleSnu extends Component {
constructor(props) {
super(props);
this.state = { showTitles: false };
}
renderRandomFirstSnu() {
const { randomFirstSnu } = this.props;
const { showTitles } = this.state;
// preselect a keyword
// dispatch the action that searches for other keywords (action I)
if(randomFirstSnu) {
return (
<div>
<h3><Typist cursor={ { show: false } }>{ randomFirstSnu.title }</Typist></h3>
<ReactCSSTransitionGroup
transitionName="snu"
transitionAppear={ true }
transitionAppearTimeout={ 1000 }
transitionEnter={ false }
transitionLeave={ false }
>
<p>{ randomFirstSnu.text }</p>
</ReactCSSTransitionGroup>
{ !showTitles ? (
<div>
<button>Some other</button>
<button onClick={ () => this.handleDoneReading(randomFirstSnu) }>Done reading, next</button>
</div>
) : (
<ReactCSSTransitionGroup
transitionName="keywords"
transitionAppear={ true }
transitionAppearTimeout={ 1000 }
transitionEnter={ false }
transitionLeave={ false }
>
<ul>{ randomFirstSnu.keywords.map((keyword, idx) =>
<li key={ idx }>
<button onClick={ () => this.fetchNextSnu(randomFirstSnu) }>
{ keyword }
</button>
</li>) }
</ul>
</ReactCSSTransitionGroup>
)
}
</div>
);
}
return <div>Loading ...</div>
}
handleDoneReading(snu) {
const { setToRead, getSnusMatchingKeyword } = this.props;
const id = snu._id;
if (snu.keywords.length > 0 && setToRead) {
// setToRead(id, snu);
this.setState({ showTitles: true });
const randomIndex = Math.floor(Math.random() * snu.keywords.length);
const randomKeyword = snu.keywords[randomIndex];
console.log('This is the randomKeyword :', randomKeyword);
getSnusMatchingKeyword(randomKeyword);
} else {
console.log('Here will soon be the select random next snu action :)');
}
}
render() {
console.log('ReadSingleSnu, this.props: ', this.props);
return (
<div className={style.App}>
<div>{ this.renderRandomFirstSnu() }</div>
</div>
);
}
}
This is my actions file:
import axios from 'axios';
export const FETCH_SNUS = 'FETCH_SNUS';
export const FETCH_SNU = 'FETCH_SNU';
export const SET_TO_READ = 'SET_TO_READ';
export const CREATE_SNU = 'CREATE_SNU';
export function getSnusMatchingKeyword(keyword) {
const request = axios.get(`/snus/keyword/${keyword}`);
return {
type: GET_SNUS_MATCHING_KEYWORD,
payload: request
};
}
export function fetchSnus() {
const request = axios.get('/snus');
return {
type: FETCH_SNUS,
payload: request
};
}
export function fetchSnu(id) {
const request = axios.get(`/snus/${id}`);
return {
type: FETCH_SNU,
payload: request
};
}
export function setToRead(id, snu) {
const request = axios.patch(`/snus/${id}`, { title: snu.title, text: snu.text, keywords: snu.keywords, read: true });
return {
type: SET_TO_READ,
payload: request
}
}
export function createSnu(object) {
const request = axios.post('/snus', object);
return {
type: CREATE_SNU,
payload: request
};
}
A Reducer:
import { FETCH_SNUS, FETCH_SNU, SET_TO_READ, CREATE_SNU } from '../actions/index';
export default function(state = [], action) {
switch(action.type) {
case FETCH_SNUS:
return [ ...state, ...action.payload.data ];
case FETCH_SNU:
return [ ...state, ...action.payload.data ];
case SET_TO_READ:
return [ ...state, ...action.payload.data ];
case CREATE_SNU:
return [...state, ...action.payload.data ];
default:
return state;
}
}
I tested all endpoints via Postman and they work. So that should not be the problem… Please help! I cannot find a solution to this problem.

Resources