VueJS: How to use Google places API with multiple refs - javascript

I'm using the Google places API for my work and everything works fine. However, I have a unique scenario where I need to have more than one delivery address, hence I'll need to make Google place API works that way.
Here is my current code:
<template>
<div>
<div v-for="(search, index) in searchInput" :key="index">
<input type="text" ref="search" v-model="search.city">
</div>
<button type="button" #click="addMore">Add more</button>
</div>
</template>
<script>
export default {
name: "App",
data: () => ({
location: null,
searchInput: [
{
city: ""
}
]
}),
mounted() {
window.checkAndAttachMapScript(this.initLocationSearch);
},
methods: {
initLocationSearch() {
let autocomplete = new window.google.maps.places.Autocomplete(
this.$refs.search
);
autocomplete.addListener("place_changed", function() {
let place = autocomplete.getPlace();
if (place && place.address_components) {
console.log(place.address_components);
}
});
},
addMore() {
this.searchInput.push({ city: "" });
}
}
};
</script>
I get this error from the API: InvalidValueError: not an instance of HTMLInputElement and b.ownerDocument is undefined
How do I make the refs unique for the individual item that I add and then pass it to the initLocationSearch method? Please, any help would be appreciated.
Her is a Codesandbox demo

Given the details you provided for the complete scenario, here is how I would do it.
I would use a simple counter to keep track of the number of inputs and set its initial value to 1 and also use it to display the inputs.
<div v-for="(n, index) in this.counter" :key="index">
<input type="text" ref="search">
</div>
Then bind the Autocomplete widget to the last added input, based on the counter.
autocomplete = new window.google.maps.places.Autocomplete(
this.$refs.search[this.counter - 1]
);
And use your addMore() method to increment the counter.
addMore() {
this.counter++;
}
Use updated() for whenever a new input is added to the DOM to trigger the initLocationSearch() method.
updated() {
this.initLocationSearch();
},
On place_changed event, update the list of selected cities based on the various input values.
this.selectedCities = this.$refs["search"].map(item => {
return { city: item.value };
});
This way you always have an up to date list of cities, whatever input is changed. Of course you would need to handle emptying an input and probably other edge cases.
Complete code:
<template>
<div>
<div v-for="(n, index) in this.counter" :key="index">
<input type="text" ref="search">
</div>
<button type="button" #click="addMore">Add more</button>
<hr>Selected cities:
<ul>
<li v-for="(item, index) in this.selectedCities" :key="index">{{ item.city }}</li>
</ul>
</div>
</template>
<script>
export default {
name: "App",
data: () => ({
selectedCities: [],
counter: 1
}),
mounted() {
window.checkAndAttachMapScript(this.initLocationSearch);
},
updated() {
this.initLocationSearch();
},
methods: {
setSelectedCities() {
this.selectedCities = this.$refs["search"].map(item => {
return { city: item.value };
});
},
initLocationSearch() {
let self = this,
autocomplete = new window.google.maps.places.Autocomplete(
this.$refs.search[this.counter - 1],
{
fields: ["address_components"] // Reduce API costs by specifying fields
}
);
autocomplete.addListener("place_changed", function() {
let place = autocomplete.getPlace();
if (place && place.address_components) {
console.log(place);
self.setSelectedCities();
}
});
},
addMore() {
this.counter++;
}
}
};
</script>
CodeSandbox demo

You can dynamically bind to ref. Something like :ref="'search' + index"
<template>
<div>
<div v-for="(search, index) in searchInput" :key="index">
<input type="text" :ref="'search' + index" v-model="search.city">
</div>
<button type="button" #click="addMore">Add more</button>
</div>
</template>
Then in addMore, before creating a pushing on a new object to searchInput, grab the last index in searchInput. You can then get the newly created ref and pass that to initLocationSearch.
initLocationSearch(inputRef) {
let autocomplete = new window.google.maps.places.Autocomplete(
inputRef
);
autocomplete.addListener("place_changed", function() {
let place = autocomplete.getPlace();
if (place && place.address_components) {
console.log(place.address_components);
}
});
},
addMore() {
const lastInputRef = this.$refs['search' + this.searchInput.length - 1];
this.initLocationSearch(lastInputRef);
this.searchInput.push({ city: "" });
}

Related

Vue: Input Manual Autocomplete component

I have a vue-cli project, that has a component named 'AutoCompleteList.vue' that manually handled for searching experience and this component has some buttons that will be fill out the input.
It listens an array as its item list. so when this array has some items, it will be automatically shown; and when I empty this array, it will be automatically hidden.
I defined an oninput event method for my input, that fetches data from server, and fill the array. so the autocomplete list, will not be shown while the user doesn't try to enter something into the input.
I also like to hide the autocomplete list when the user blurs the input (onblur).
but there is a really big problem! when the user chooses one of items (buttons) on the autocomplete list, JS-engine first blurs the input (onblur runs) and then, tries to run onclick method in autocomplete list. but its too late, because the autocomplete list has hidden and there is nothing to do. so the input will not fill out...
here is my code:
src/views/LoginView.vue:
<template>
<InputGroup
label="Your School Name"
inputId="schoolName"
:onInput="schoolNameOnInput"
autoComplete="off"
:onFocus="onFocus"
:onBlur="onBlur"
:vModel="schoolName"
#update:vModel="newValue => schoolName = newValue"
/>
<AutoCompleteList
:items="autoCompleteItems"
:choose="autoCompleteOnChoose"
v-show="autoCompleteItems.length > 0"
:positionY="autoCompletePositionY"
:positionX="autoCompletePositionX"
/>
</template>
<script>
import InputGroup from '../components/InputGroup'
import AutoCompleteList from '../components/AutoCompleteList'
export default {
name: 'LoginView',
components: {
InputGroup,
AutoCompleteList
},
props: [],
data: () => ({
autoCompleteItems: [],
autoCompletePositionY: 0,
autoCompletePositionX: 0,
schoolName: ""
}),
methods: {
async schoolNameOnInput(e) {
const data = await (await fetch(`http://[::1]:8888/schools/${e.target.value}`)).json();
this.autoCompleteItems = data;
},
autoCompleteOnChoose(value, name) {
OO("#schoolName").val(name);
this.schoolName = name;
},
onFocus(e) {
const position = e.target.getBoundingClientRect();
this.autoCompletePositionX = innerWidth - position.right;
this.autoCompletePositionY = position.top + e.target.offsetHeight + 20;
},
onBlur(e) {
// this.autoCompleteItems = [];
// PROBLEM! =================================================================
}
}
}
</script>
src/components/AutoCompleteList.vue:
<template>
<div class="autocomplete-list" :style="'top: ' + this.positionY + 'px; right: ' + this.positionX + 'px;'">
<ul>
<li v-for="(item, index) in items" :key="index">
<button #click="choose(item.value, item.name)" type="button">{{ item.name }}</button>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'AutoCompleteList',
props: {
items: Array,
positionX: Number,
positionY: Number,
choose: Function
},
data: () => ({
})
}
</script>
src/components/InputGroup.vue:
<template>
<div class="input-group mb-3">
<label class="input-group-text" :for="inputId ?? ''">{{ label }}</label>
<input
:type="type ?? 'text'"
:class="['form-control', ltr && 'ltr']"
:id="inputId ?? ''"
#input="$event => { $emit('update:vModel', $event.target.value); onInput($event); }"
:autocomplete="autoComplete ?? 'off'"
#focus="onFocus"
#blur="onBlur"
:value="vModel"
/>
</div>
</template>
<script>
export default {
name: 'input-group',
props: {
label: String,
ltr: Boolean,
type: String,
inputId: String,
groupingId: String,
onInput: Function,
autoComplete: String,
onFocus: Function,
onBlur: Function,
vModel: String
},
emits: [
'update:vModel'
],
data: () => ({
}),
methods: {
}
}
</script>
Notes on LoginView.vue:
autoCompletePositionX and autoCompletePositionY are used to find the best position to show the autocomplete list; will be changed in onFocus method of the input (inputGroup)
OO("#schoolName").val(name) is used to change the value of the input, works like jQuery (but not exactly)
the [::1]:8888 is my server that used to fetch the search results
If there was any unclear code, ask me in the comment
I need to fix this. any idea?
Thank you, #yoduh
I got the answer.
I knew there should be some differences between when the user focus out the input normally, and when he tries to click on buttons.
the key, was the FocusEvent.relatedTarget property. It should be defined in onblur method. here is its full tutorial.
I defined a property named isFocus and I change it in onBlur method, only when I sure that the focus is not on the dropdown menu, by checking the relatedTarget

Vue child component not re-rendering on data change

Trying to update a child component on parent state change. When I change the date state in Day.vue I can see that the computed function is working but the <EntryList> component is not updating with the new date I pass in.
Day.vue
<template>
<div id="daily">
<div id="nav">
<div class="nav">
<button #click="changeDay(-1)" class="arrow">←</button>
<h5 class="dayTitle" style="background-color: initial">
{{ dayTitle }}
</h5>
<button #click="changeDay(1)" class="arrow">→</button>
</div>
</div>
<div class="dayDiv">
<div class="details">
<!-- We use v-bind to attach state item to component--->
<EntryList v-bind:listDate="date" />
</div>
</div>
</div>
</template>
<script>
import EntryList from "./EntryList";
export default {
components: {
EntryList,
},
data() {
return {
date: "",
};
},
created() {
this.date = new Date();
},
methods: {
// Change Date here
changeDay(amount) {
let changeDate = new Date(this.date); // had to do this because computed couldn't see that it was updating
changeDate.setDate(this.date.getDate() + amount);
this.date = changeDate;
console.log("Date Changed");
},
},
computed: {
dayTitle() {
let options = { weekday: "short" };
return `${this.date.toLocaleString(
undefined,
options
)} ${this.date.toLocaleDateString()}`;
},
},
};
</script>
EntryList.vue
<template>
<div>
<ul>
<Entry v-for="entry in entries" v-bind:entry="entry" :key="entry.id" />
</ul>
<button :value="dateStamp" class="add">+</button>
</div>
</template>
<style lang="scss">
</style>
<script>
import Entry from "./Entry";
export default {
components: {
Entry,
},
// Eventually pass in props for styling component
props: {
listDate: {
type: Date,
required: true,
},
},
data() {
return {
entries: [],
};
},
created() {
// We do this to get the entries for the date
console.log("Render List");
let dateStamp = this.listDate.toLocaleDateString();
chrome.storage.sync.get([dateStamp], (result) => {
this.entries = Object.values(result[dateStamp]);
});
},
computed: {
dateStamp() {
return this.listDate.toLocaleDateString();
},
},
};
</script>
your entries array fills once on created event. You should move logic from created function, to computed property or use watcher
like this
watch: {
listDate (newValue) {
console.log("Render List");
let dateStamp = this.listDate.toLocaleDateString();
chrome.storage.sync.get([dateStamp], (result) => {
this.entries = Object.values(result[dateStamp]);
});
}
}

Vue - passing data via Event.$emit

I have a question about passing data through Event.$emit. I have a parent that has a click event (appSelected) like this
<template>
<v-app>
<v-container>
<v-select :items="results" item-text="name" item-value="id" v-model="selectedOption" #change="appSelected"
:results="results"
label="Choose Application"
dense
></v-select>
</v-container>
</v-app>
</template>
In that component I have a method that emits the data. This is the id - this.selectedOption
appSelected() {
//alert("This is the id" + this.selectedOption);
Event.$emit('selected', this.selectedOption);
In my child component I would like to pass and use the value but can't seem to get it to work. I have a v-if on the top level div that if it sees a true it will show all below. That works great, but I need the id that's being passed.
This works for showing the div, but need to pass and use the id also. How can I pass the and use the id?
Event.$on('selected', (data) => this.appselected = true);
<template>
<div v-if="appselected">
Choose the Client Source
<input type="text" placeholder="Source client" v-model="query"
v-on:keyup="autoComplete"
v-on-clickaway="away"
#keydown.esc="clearText" class="form-control">
<span class="instructiontext"> Search for id, name or coid</span>
<div class="panel-footer" v-if="results.length">
<ul class="list-group">
<li class="list-group-item" v-for="result in results">
{{ result.name + "-" + result.oid }}
</li>
</ul>
</div>
</div>
</template>
<script>
import axios from 'axios'
import { directive as onClickaway } from 'vue-clickaway';
export default{
directives: {
onClickaway: onClickaway,
},
data(){
return {
add_id: '',
onSelecton: '',
input_disabled: false,
selected: '',
query: '',
results: [],
appselected: false
}
},
methods: {
getClient(name) {
this.query = name;
this.results = [];
},
clearText(){
this.query = '';
},
away: function() {
// Added for future use
},
autoComplete(){
this.results = [];
if(this.query.length > 2){
axios.get('/getclientdata',{params: {query: this.query}}).then(response => {
this.results = response.data;
});
}
}
},
created() {
Event.$on('selected', (data) => this.appselected = true);
console.log(this.appselected);
}
}
</script>
Thanks for your help in advance!
Change your listener to do something with the emitted value. I see the parent already has a selected variable that you maybe intend to use for this purpose:
Event.$on('selected', (selectedOption) => {
this.selected = selectedOption;
this.appselected = true;
});
The selectedOption is the value emitted from the child.

How to call a method in a Vue component from programmatically inserted component

I'm trying to call a method from a child component which is programatically inserted.
Here is my code.
MultipleFileUploader.vue
<template>
<div class="form-group" id="multiple-file-uploader">
<div>
<multiple-file-uploader-part
:name="uploadername" :index="1"
#remove="deleteUploader" #fileselected="fileSelected($event)">
</multiple-file-uploader-part>
</div>
</div>
</template>
<script>
import MultipleFileUploaderPart from './MultipleFileUploaderPart.vue';
let index_count = 1;
export default {
components: {
'multiple-file-uploader-part':MultipleFileUploaderPart,
},
props: {
uploadername: {
type: String,
default: 'files',
}
},
data() {
return {
next_id:1,
}
},
methods: {
fileSelected: function (target) {
var UploaderPart = Vue.extend(MultipleFileUploaderPart);
new UploaderPart().$on('fileselected','fileSelected')
.$mount('#multiple-file-uploader');
},
deleteUploader: function (idToRemove) {
this.uploaders = this.uploaders.filter(
uploaders_id => {
return uploaders_id.id !== idToRemove;
}
)
}
},
}
</script>
<style scoped>
</style>
MultipleFileUploaderPart.vue
<template>
<div v-bind:id="name + '['+index+']'">
<div class="input-group margin">
{{index}}
<input type="file" accept="application/pdf,image/jpeg,image/png"
v-bind:name="name + '['+index+']'"
v-on:change="fileSelectedMethod($event.target)">
<div class="input-group-btn">
<button #click="removeClicked"
class="btn btn-danger btn-sm"
v-if="index != 1"
type="button">
Delete{{index}}
</button>
</div>
</div>
<p v-if="size_error" style="color: red">File size must be less than 2MB</p>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
},
index: {
type: Number,
},
},
data() {
return {
size: '',
size_error: false,
}
},
methods: {
removeClicked: function () {
document.getElementById(this.name+'[' + this.index + ']' ).remove();
this.$emit('remove', this.index);
},
fileSelectedMethod: function (target) {
this.size = target.files[0].size;
if (this.size < 2000000) {
this.size_error = false;
this.$emit('fileselected', target);
} else {
target.value = null;
this.size_error = true;
console.log(target.files);
}
}
}
}
</script>
<style scoped>
I'm trying to achieve is that when a file input is filled with a file, a MultipleFileUploaderPart is created. And when the file input element in this component is filled, another MultipleFileUploaderPart is inserted.
I'd like to call MultipleFileUploader 's fileSelected method from newly inserted components so that I can create another component.
I also want to remove a MultipleFileUploaderPart component when the delete button is clicked.
How can I achieve this? or is there a better way?
EDIT:
This is what I originally had.
MultipleFileUploader.vue
<template>
<div class="form-group">
<div>
<multiple-file-uploader-part
v-for="uploader in uploaders"
:name="uploadername" :index="uploader.id"
#remove="deleteUploader" #fileselected="fileSelected($event)">
slot
</multiple-file-uploader-part>
</div>
</div>
</template>
<script>
import MultipleFileUploaderPart from "./MultipleFileUploaderPart";
let index_count = 1;
export default {
//name: "MultipleFileUploader",
components: {MultipleFileUploaderPart},
props: {
uploadername: {
type: String,
default: 'files',
}
},
data() {
return {
uploaders: [
{
id: index_count++,
},
]
}
},
methods: {
fileSelected: function (target) {
if(target.value){
this.uploaders.push({
id: index_count++,
})
}
},
deleteUploader: function (idToRemove) {
this.uploaders = this.uploaders.filter(
uploaders_id => {
return uploaders_id.id !== idToRemove;
}
)
}
},
}
</script>
MultipleFileUploaderPart.vue
<template>
<div class="input-group margin">
{{index}}
<input type="file" accept="application/pdf,image/jpeg,image/png"
v-bind:name="name + '['+index+']'"
v-on:change="fileSelectedMethod($event.target)">
<div class="input-group-btn">
<button #click="$emit('remove',index)"
class="btn btn-danger btn-sm"
v-if="index != 1"
type="button">
Delete{{index}}
</button>
</div>
<br>
<p v-if="size_error" style="color: red">File size must be less than 2MB</p>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
},
index: {
type: Number,
},
},
data() {
return {
size: '',
size_error: false,
}
},
methods: {
checkFileSize: function () {
},
fileSelectedMethod: function (target) {
console.log(target);
console.log(target.files);
this.size = target.files[0].size;
console.log(this.size);
if (this.size < 2000000) {
this.size_error = false;
this.$emit('fileselected', target);
} else {
target.value = null;
this.size_error = true;
console.log(target.files);
}
}
}
}
</script>
And this happens. please click
When I click 'Delete'Button, correct child coponent is deleted but the file in the input form stays there. that's why I'm seeking for another approach.
Declare uploaders as an array of objects that contain all needed props for creation of MultipleFileUploaderPart.
Use v-for on MultipleFileUploaderPart in the main MultipleFileUploader to reactively generate MultipleFileUploaderPart components
Use $emit from MultipleFileUploaderPart to MultipleFileUploader to emit creation and deletion events so that MultipleFileUploader can add or remove elements in the uploaders array.
Please don't delete or create elements from DOM directly, let the VueJs do this work.

How do you target a button, which is in another component, with a method in App.vue?

I'm making a basic To Do app, where I have an input field and upon entering a task and pressing the "Enter" key the task appears in the list. Along with the task the TodoCard.vue component also generates a button, which I would like to use to delete the task.
I've added a #click="removeTodo" method to the button, but don't know where to place it, in the TodoCard component or in the App.vue file?
TodoCard component:
<template>
<div id="todo">
<h3>{{ todo.text }}</h3>
<button #click="removeTodo(todo)">Delete</button>
</div>
</template>
<script>
export default {
props: ['todo'],
methods: {
removeTodo: function (todo) {
this.todos.splice(this.todos.indexOf(todo), 1)
}
}
}
</script>
App.vue:
<template>
<div id="app">
<input class="new-todo"
placeholder="Enter a task and press enter."
v-model="newTodo"
#keyup.enter="addTodo">
<TodoCard v-for="(todo, key) in todos" :todo="todo" :key="key" />
</div>
</template>
<script>
import TodoCard from './components/TodoCard'
export default {
data () {
return {
todos: [],
newTodo: ''
}
},
components: {
TodoCard
},
methods: {
addTodo: function () {
// Store the input value in a variable
let inputValue = this.newTodo && this.newTodo.trim()
// Check to see if inputed value was entered
if (!inputValue) {
return
}
// Add the new task to the todos array
this.todos.push(
{
text: inputValue,
done: false
}
)
// Set input field to empty
this.newTodo = ''
}
}
}
</script>
Also is the code for deleting a task even correct?
You can send an event to your parent notifying the parent that the delete button is clicked in your child component.
You can check more of this in Vue's documentation.
And here's how your components should look like:
TodoCard.vue:
<template>
<div id="todo">
<h3>{{ todo.text }}</h3>
<button #click="removeTodo">Delete</button>
</div>
</template>
<script>
export default {
props: ['todo'],
methods: {
removeTodo: function (todo) {
this.$emit('remove')
}
}
}
</script>
App.vue:
<template>
<div id="app">
<input class="new-todo"
placeholder="Enter a task and press enter."
v-model="newTodo"
#keyup.enter="addTodo">
<TodoCard v-for="(todo, key) in todos" :todo="todo" :key="key" #remove="removeTodo(key)" />
</div>
</template>
<script>
import TodoCard from './components/TodoCard'
export default {
data () {
return {
todos: [],
newTodo: ''
}
},
components: {
TodoCard
},
methods: {
addTodo: function () {
// Store the input value in a variable
let inputValue = this.newTodo && this.newTodo.trim()
// Check to see if inputed value was entered
if (!inputValue) {
return
}
// Add the new task to the todos array
this.todos.push(
{
text: inputValue,
done: false
}
)
// Set input field to empty
this.newTodo = ''
}
},
removeTodo: function(key) {
this.todos.splice(key, 1);
}
}
</script>

Categories

Resources