I am developing an instantsearch using nuxt. Googlemaps receives the first set on results on page load and renders the markers without any issues - however when searching the new results do not update on the map? Is there a way i can push the new set of results through to the map to render the markers?
I assume this will be the beforeMount hook and pass the results to updateMap?
pages/index.vue
<template>
<div class="root">
<div class="container">
<ais-instant-search-ssr
:search-client="searchClient"
index-name="dive_buddy"
>
<ais-configure :hits-per-page.camel="7">
<ais-search-box placeholder="Search here…" class="searchbox" />
<ais-stats />
<div class="h-screen">
<ais-hits>
<template #default="{ items }">
<div class="grid grid-cols-2 gap-2">
<div v-if="items.length">
<div
v-for="item in items"
:key="item.objectID"
class="my-2 p-2 border"
>
<SiteRow
:site="item"
#mouseover.native="highlightMarker(item.objectID, true)"
#mouseout.native="highlightMarker(item.objectID, false)"
/>
</div>
</div>
<div v-else>No results found</div>
<Map
:items="items"
:lat="items[0].lat"
:lng="items[0].lng"
/>
</div>
</template>
</ais-hits>
<ais-pagination />
</div>
</ais-configure>
</ais-instant-search-ssr>
</div>
</div>
</template>
<script>
import {
AisInstantSearchSsr,
AisConfigure,
AisHits,
AisSearchBox,
AisStats,
AisPagination,
createServerRootMixin,
} from 'vue-instantsearch'
import algoliasearch from 'algoliasearch/lite'
import _renderToString from 'vue-server-renderer/basic'
function renderToString(app) {
return new Promise((resolve, reject) => {
_renderToString(app, (err, res) => {
if (err) reject(err)
resolve(res)
})
})
}
const searchClient = algoliasearch(
'xxx', // AppID
'xxxxxxxx' // APIKey
)
export default {
components: {
AisInstantSearchSsr,
AisConfigure,
AisHits,
AisSearchBox,
AisStats,
AisPagination,
},
mixins: [
createServerRootMixin({
searchClient,
indexName: 'dive_buddy',
}),
],
data() {
return {
searchClient,
}
},
head() {
return {
link: [
{
rel: 'stylesheet',
href: 'https://cdn.jsdelivr.net/npm/instantsearch.css#7.4.5/themes/satellite-min.css',
},
],
}
},
serverPrefetch() {
return this.instantsearch
.findResultsState({
component: this,
renderToString,
})
.then((algoliaState) => {
this.$ssrContext.nuxt.algoliaState = algoliaState
})
},
beforeMount() {
const results =
(this.$nuxt.context && this.$nuxt.context.nuxtState.algoliaState) ||
window.__NUXT__.algoliaState
this.instantsearch.hydrate(results)
// Remove the SSR state so it can't be applied again by mistake
delete this.$nuxt.context.nuxtState.algoliaState
delete window.__NUXT__.algoliaState
},
methods: {
highlightMarker(id, isHighlighted) {
document
.getElementsByClassName(`id-${id}`)[0]
?.classList?.toggle('marker-highlight', isHighlighted)
},
},
}
</script>
components/Map.vue
<template>
<div ref="map" class="h-screen"></div>
</template>
<script>
export default {
props: {
items: {
type: Array,
required: true,
},
lat: {
type: Number,
required: true,
},
lng: {
type: Number,
required: true,
},
},
mounted() {
this.updateMap()
},
methods: {
// run showMap function in maps.client.js
async updateMap() {
await this.$maps.showMap(
this.$refs.map,
this.lat,
this.lng,
this.getMarkers()
)
},
// pass through markers to googlemaps
getMarkers() {
return this.items.map((item) => {
return {
...item,
}
})
},
},
}
</script>
plugins/maps.client.js
export default function(context, inject){
let isLoaded = false
let waiting = []
addScript()
inject('maps', {
showMap,
})
function addScript(){
const script = document.createElement('script')
script.src = "https://maps.googleapis.com/maps/api/js?key=xxxx&libraries=places&callback=initGoogleMaps"
script.async = true
window.initGoogleMaps = initGoogleMaps
document.head.appendChild(script)
}
function initGoogleMaps(){
isLoaded = true
waiting.forEach((item) => {
if(typeof item.fn === 'function'){
item.fn(...item.arguments)
}
})
waiting = []
}
function showMap(canvas, lat, lng, markers){
if(!isLoaded){
waiting.push({
fn: showMap,
arguments,
})
return
}
const mapOptions = {
zoom: 18,
center: new window.google.maps.LatLng(lat, lng),
disableDefaultUI: true,
zoomControl: true,
styles:[{
featureType: 'poi.business',
elementType: 'labels.icon',
stylers:[{ visibility: 'off' }]
}]
}
const map = new window.google.maps.Map(canvas, mapOptions)
if(!markers){
const position = new window.google.maps.LatLng(lat, lng)
const marker = new window.google.maps.Marker({
position,
clickable: false,
})
marker.setMap(map)
return
}
const bounds = new window.google.maps.LatLngBounds()
let index = 1
markers.forEach((item) => {
const position = new window.google.maps.LatLng(item.lat, item.lng)
const marker = new window.google.maps.Marker({
position,
label: {
text: `${index}`,
color: 'black',
className: `marker id-${item.objectID}`
},
icon: 'https://maps.gstatic.com/mapfiles/transparent.png',
clickable: false,
})
marker.setMap(map)
bounds.extend(position)
index++
})
map.fitBounds(bounds)
}
}
EDIT
As per #Bryan's suggestion - I have previously tried to follow algolia's documentation using vue-googlemaps plugin but this does not work on Nuxt as the map does not load as well as these errors -
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'g')
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'h')
Although this isnt a solution to the above - I have found a workaround using a different library: vue2-google-maps as this works with Nuxt (see docs).
pages/index.vue
<template>
<div class="root">
<div class="container">
<ais-instant-search-ssr
:search-client="searchClient"
index-name="dive_centres"
>
<ais-configure :hits-per-page.camel="7">
<ais-search-box placeholder="Search here…" class="searchbox" />
<ais-stats />
<div class="h-screen">
<ais-hits>
<template #default="{ items }">
<div class="grid grid-cols-2 gap-2">
<div v-if="items.length">
<div
v-for="item in items"
:key="item.objectID"
class="my-2 p-2 border"
>
<SiteRow
:site="item"
#mouseover.native="highlightMarker(item.objectID, true)"
#mouseout.native="highlightMarker(item.objectID, false)"
/>
</div>
</div>
<div v-else>No results found</div>
<Map />
</div>
</template>
</ais-hits>
<ais-pagination />
</div>
</ais-configure>
</ais-instant-search-ssr>
</div>
</div>
</template>
<script>
import {
AisInstantSearchSsr,
AisConfigure,
AisHits,
AisSearchBox,
AisStats,
AisPagination,
createServerRootMixin,
} from 'vue-instantsearch'
import algoliasearch from 'algoliasearch/lite'
import _renderToString from 'vue-server-renderer/basic'
function renderToString(app) {
return new Promise((resolve, reject) => {
_renderToString(app, (err, res) => {
if (err) reject(err)
resolve(res)
})
})
}
const searchClient = algoliasearch(
'xxx', // AppID
'xxxxxxxx' // APIKey
)
export default {
components: {
AisInstantSearchSsr,
AisConfigure,
AisHits,
AisSearchBox,
AisStats,
AisPagination,
},
mixins: [
createServerRootMixin({
searchClient,
indexName: 'dive_sites',
}),
],
head() {
return {
link: [
{
rel: 'stylesheet',
href: 'https://cdn.jsdelivr.net/npm/instantsearch.css#7.4.5/themes/satellite-min.css',
},
],
}
},
serverPrefetch() {
return this.instantsearch
.findResultsState({
component: this,
renderToString,
})
.then((algoliaState) => {
this.$ssrContext.nuxt.algoliaState = algoliaState
})
},
beforeMount() {
const results =
(this.$nuxt.context && this.$nuxt.context.nuxtState.algoliaState) ||
window.__NUXT__.algoliaState
this.instantsearch.hydrate(results)
// Remove the SSR state so it can't be applied again by mistake
delete this.$nuxt.context.nuxtState.algoliaState
delete window.__NUXT__.algoliaState
},
methods: {
highlightMarker(id, isHighlighted) {
console.log(id)
document
.getElementsByClassName(`id-${id}`)[0]
?.classList?.toggle('marker-highlight', isHighlighted)
},
},
}
</script>
components/Map.vue
<template>
<div v-if="state" class="ais-GeoSearch">
<GmapMap
:center="center"
:zoom="zoom"
map-type-id="terrain"
class="h-full w-full"
:options="options"
>
<GmapMarker
v-for="item in state.items"
:key="item.objectID"
:position="item._geoloc"
:clickable="false"
:draggable="true"
:icon="{ url: require('~/static/images/dive-site-small.svg') }"
/>
</GmapMap>
</div>
</template>
<script>
import { createWidgetMixin } from 'vue-instantsearch'
import { connectGeoSearch } from 'instantsearch.js/es/connectors'
export default {
mixins: [createWidgetMixin({ connector: connectGeoSearch })],
data() {
return {
zoom: 4,
options: {
disableDefaultUI: true,
},
}
},
computed: {
center() {
return this.state.items.length !== 0
? this.state.items[0]._geoloc
: { lat: 48.864716, lng: 2.349014 }
},
},
}
</script>
I created a todo list by using react. I get some problem that I want to create checkbox but my checkbox it does not work and I cannot solve :( I don't know what's wrong with that.
I set the data for each task and then I need to change the completed of some task, but it cannot click and change the completed task
This is my code
class App extends React.Component {
constructor() {
super()
this.state = {
todos: todoData,
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(id) {
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if(todo.id === id) {
todo.completed = !todo.completed
// console.log(todo.completed)
}
return todo
})
return {
todos: updatedTodos
}
})
}
render() {
const todoItem = this.state.todos.map(item => <TodoItem key={item.id} item={item}
handleChange={this.handleChange}/>)
return (
<div>
<h1 className="header">My Todo Lists</h1>
{todoItem}
</div>
)
}
}
function TodoItem(props) {
let textItem = props.item.completed === true ?
<del>{props.item.text}</del> : props.item.text
return (
<div className="list">
<input
type="checkbox"
checked={props.item.completed}
onChange={() => props.handleChange(props.item.id)}
/>
<p className="item">{textItem}</p>
</div>
)
}
And this is my data
const todoData = [
{
id: 1,
text: "Practice coding",
completed: false
},
{
id: 2,
text: "Grocery shopping",
completed: true
},
{
id: 3,
text: "Wash the dishes",
completed: true
},
{
id: 4,
text: "Take out the trash",
completed: false
},
{
id: 5,
text: "Teach my brother homework",
completed: false
}
]
Thank you for helping :)
Looks like on your handleChange you are mutating the existing state on your map transformation. you must return a new state instead.
Replace your handleChange with the following code:
handleChange(id) {
this.setState((prevState) => {
const updatedTodos = prevState.todos.map((todo) => {
return {
...todo,
completed: todo.id === id ? !todo.completed : todo.completed
};
});
return {
todos: updatedTodos
};
});
}
How to close autocomplete dropdown when clicked on any outside area? Currently i am calling my custom autocomplete component(child presentation component) 3 times with different labels from another vue page(parent) called departmentDetail.vue.
Example:
departmentDetail.vue
<b-field label="Custom Business Unit ">
<AutoComplete :method="getAsyncDataBusinessUnit" title='businessUnit' :autocompleteData="dataBusinessUnit" viewname='DepartmentDetail'>
</AutoComplete>
</b-field>
<b-field label="Custom Managers">
<AutoComplete :method="getAsyncData" title='manager' :autocompleteData="dataManager" viewname='DepartmentDetail'>
</AutoComplete>
</b-field>
<b-field label="Custom Location">
<AutoComplete :method=" getAsyncDataLocation" title='location' :autocompleteData="dataLocation" viewname='DepartmentDetail'>
</AutoComplete>
</b-field>
AutoComplete.vue (Custom component created by me)
<template>
<div class="autocomplete">
<input style="font-size: 12pt; height: 36px; width:1800px; " type="text" v-model="objectData[title]" #focus="getAsyncDataBusinessUnit" #input="getAsyncDataBusinessUnit"/>
<ul v-show="isFetching" >
<li v-for="(dataBusinessUnit, i) in dataBusinessUnit" :key="i" #click="setResult(dataBusinessUnit)" >
<template v-if="title!='manager'">
<div class="container">
<p>
<b>ID:</b>
{{dataBusinessUnit.id}}
</p>
<p>
<b>Description:</b>
{{dataBusinessUnit.description}}
</p>
</div>
</template>
<template v-else>
<div class="container">
<p>
<b>ID:</b>
{{dataBusinessUnit.id}}
</p>
<p>
<b>First Name:</b>
{{dataBusinessUnit.firstName}}
</p>
<p>
<b>Last Name:</b>
{{dataBusinessUnit.lastName}}
</p>
</div>
</template>
</li>
</ul>
</div>
</template>
<script>
import { viewMixin } from "../viewMixin.js";
import schemaData from '../store/schema';
import debounce from "lodash/debounce";
import api from "../store/api";
const ViewName = "AutoComplete";
var passedview;
export default {
name: "AutoComplete",
props: {
method: {
type: Function
},
title: String,
viewname:String,
autocompleteData: {
type: Array,
required: true
}
},
data() {
return {
// results: [],
dataBusinessUnit: [],
results: [],
isFetching: false
// vignesh: this.objectData[this.title]
};
},
computed: {
viewData() {
return this.$store.getters.getViewData('DepartmentDetail')
},
objectData() {
return this.$store.getters.getApiData(this.viewData.api_id).data
},
sessionData() {
return this.$store.getters.getSessionData()
},
isLoading() {
return this.$store.getters.getApiData(this.viewData.api_id).isLoading
},
newRecord() {
return this.$route.params.id === null;
},
getTitle() {
return this.title
}
},
mounted() {
},
methods: {
setResult(result) {
this.updateValue(result.id,this.title);
// localStorage.setItem(this.title,result.id );
this.isFetching = false;
},
updateValue(newValue, fieldName) {
var val;
var schema = schemaData[this.viewData.schema];
if(typeof schema!=='undefined' && schema['properties'][fieldName]['type'] == 'date'){
val = this.formatDate(newValue);
} else {
val = newValue;
}
this.$store.dispatch('updateDataObjectField', {
key: this.viewData.api_id,
field: fieldName,
value: val
});
},
getAsyncDataBusinessUnit: debounce(function(name) {
console.log('getAsyncDataBusinessUnit you typed'+name.target.value);
if (!name.target.value.length) {
// this.results = [];
// this.dataBusinessUnit = [...this.results];
// this.isFetching = false;
// return;
}
// this.isFetching = true;
api
.getSearchData(this.sessionData.key,`/businessunit/`,{ filter: `{id}like'%${name.target.value}%'` })
.then(response => {
this.results = [];
if (!response.length)
{
console.log('inside if ')
this.isFetching = false;
}
else{
console.log('inside else')
response.forEach(item => {
this.results.push(item);
});
// this.dataBusinessUnit=this.results
this.dataBusinessUnit = [...this.results];
this.isFetching = true;
}
console.log('length of dataBusinessUnit is '+(this.dataBusinessUnit).length)
console.log('contents of dataBusinessUnit array '+JSON.stringify(this.dataBusinessUnit))
})
.catch(error => {
//this.dataBusinessUnit = [];
throw error;
})
.finally(() => {
// this.isFetching = true;
});
}, 500),
},
components: {
}
};
</script>
Screenshot of the Department screen
And why is it that when the page loads sometimes the values dont show up in these input fields? But upon focus or if i type anything then sudddenly the value shows up?Any idea why this is happening?
About your last question:
"And why is it that when the page loads sometimes the values dont show up in these input fields? But upon focus or if i type anything then sudddenly the value shows up?Any idea why this is happening?"
It looks like you are usesing API requests on computed props.
Computed props are pre renderd values. If your API works async then the computed is renderd "empty" before the full request is resolved.
you could try data props and set them with the API setter in Mounted() or Created().
EDIT:
It could look something like this:
data() {
return {
// results: [],
dataBusinessUnit: [],
results: [],
viewData: [],
objectData:[],
sessionData:[],
isLoading: [],
isFetching: false
// vignesh: this.objectData[this.title]
};
},
computed: {
newRecord() {
return this.$route.params.id === null;
},
getTitle() {
return this.title
}
},
mounted() {
this.viewData = this.$store.getters.getViewData('DepartmentDetail');
this.objectData = this.$store.getters.getApiData(this.viewData.api_id).data;
this.sessionData = this.$store.getters.getSessionData();
this.isLoading = this.$store.getters.getApiData(this.viewData.api_id).isLoading;
},
I'm trying to get the value from days_free from an array within an object returned from a rest api. However, I get the error: Uncaught TypeError: this.state.user.map is not a function
The data is structured like so:
{
"available": true,
"offers": [
{
"days_free": 30,
"description": "Woo!"
}
]
}
I'm trying to map the array within the object and get the value with
const daysFree = this.state.user.map((daysFree, i) => {
return (
<span>{daysFree.days_free}</span>
)
})
From my component:
class CancelOffer extends React.Component {
constructor (props) {
super(props)
this.state = {
user: []
}
this.processData = this.processData.bind(this)
}
componentDidMount () {
this.fetchContent(this.processData)
}
fetchContent (cb) {
superagent
.get('/api/data')
.then(cb)
}
processData (data) {
this.setState({
user: data.body
})
}
render () {
const content = this.props.config.contentStrings
const daysFree = this.state.user.map((daysFree, i) => {
return (
<span>{daysFree.days_free}</span>
)
})
return (
<div className='offer'>
<h2 className='offer-heading md'>Heading</h2>
<p className='offer-subpara'>text {daysFree} </p>
<div className='footer-links'>
<a href='/member' className='btn btn--primary btn--lg'>accept</a>
<a href='/' className='cancel-link'>cancel</a>
</div>
</div>
)
}
}
export default CancelOffer
data.body is an object, if you want to loop over the offers, you need to do
processData (data) {
this.setState({
user: data.body.offers
})
}
I have json data passed as props from the Redux store, the json has this format:
{
"label1.label2.label3": {
"option1":[
],
"option2": "value",
"option3": [
],
...
}
...
}
I need to split the label part and create a nested menu from the data as follows:
label1
label2
label3
option1
value of option1
option2
value of option2
option3
...
I couldn't figure out how to map into the data, my component code looks like this:
class MyDropdown extends React.Component {
componentDidMount(){
//A component dispatches ACTIONS
this.props.dispatch(fetchDataWithRedux());
}
render(){
const {error, loading, elements} = this.props;
if (loading){
return <div>Loading...</div>;
}
if (error){
return <div> Error! {error.message}</div>;
}
console.log(this.props.elements); //outputs "undefined"
return(
<div>
{this.props.elements && this.props.elements.map(this.createMenu)}
</div>
);
}
createMenu() {
return this.props.elements.map((item) => {
return(
);
});
}
}
const mapStateToProps = state => ({
elements: state.items,
loading: state.loading,
error: state.error
});
export default connect(mapStateToProps)(MyDropdown);
I'm getting real close with this. It's pretty tricky to get the nesting right though.
const data = {
"label1.label2.label3": {
"option1":"value1",
"option2": "value2",
"option3": "value3"
},
"label1.label2.label3.label4": {
"option1":"value1",
"option2": "value2"
},
}
class App extends React.Component {
createMenu = () => {
const options = Object.keys(data).reduce( (options, key, i) => {
const d = data[key];
let levels = key.split('.').map( (label, i) => {
return {
label: label,
options: []
};
});
levels[levels.length -1].options = Object.keys(d).map( option => {
return {
label: option,
value: d[option]
}
});
options.push(levels);
return options;
}, []);
const mapOptions = o => {
let menu = [];
o.forEach( (option, i) => {
if(!option.options.length) {
// Nest next option
menu.push(
<li>
{option.label}
</li>
);
}else {
// display options
menu.push(
<li>
{option.label}
{option.options.map( nested => {
return <li>{nested.label}: {nested.value}</li>
})}
</li>
);
}
});
return menu;
};
return options.map( option => {
return mapOptions(option);
});
};
render() {
return (
<div>
{this.createMenu()}
</div>
)
}
}
ReactDOM.render(<App/>, document.getElementById('root'));
li li {
margin-left: 15px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>