I have this code here,
it basically fetch from firestore in the setup(), then display the information with the Categoria component. It also should update the Categoria components when the <span> is pressed. However, something don't work. My snippet successfully update the database but does not reload the component... any ideas?
<template>
<div class="header">
<span class="mdi mdi-home icona" />
<h1>Modifica menĂ¹</h1>
</div>
<Categoria
v-model="categorie"
v-for="categoria in categorie"
v-bind:key="categoria"
:Nome="categoria.nome"
/>
<div class="contenitore_aggiungi">
<span #click="crea()" class="mdi mdi-plus aggiungi" />
</div>
</template>
<script>
import Categoria from "#/components/edit/Categoria-edit.vue";
import { useRoute } from "vue-router";
import { creaCategoria, ottieniCategorie } from "#/firebase";
export default {
name: "Modifica",
components: { Categoria },
async setup() {
const route = useRoute();
let idRistorante = route.params.id;
let categorie = await ottieniCategorie(idRistorante);
console.log(categorie);
return { idRistorante, categorie };
},
methods: {
crea() {
let nuovaCategoria = "Nuova categoria";
creaCategoria(this.idRistorante, nuovaCategoria);
this.categorie.push({ nome: nuovaCategoria });
console.log(this.categorie);
},
},
};
</script>
Thanks for your answers!
You need to declare categorie as a reactive property. Also you can write methods in setup() itself instead of methods:
import { ref } from 'vue'
export default {
setup() {
const route = useRoute();
let idRistorante = route.params.id;
const categorie = ref({}) // <-- add default value of properties
const getData = async () => {
const data = await ottieniCategorie(idRistorante);
categorie.value = data
}
getData() // or void getData()
const crea = () => {
let nuovaCategoria = "Nuova categoria";
categorie.value.push({ nome: nuovaCategoria });
console.log(categorie.value);
},
return { idRistorante, categorie, crea };
}
}
Make sure the default value of categorie is set in ref(). If it's an array set it to ref([]).
Related
I'm new to Vue, javascript & Web development. Using Vue, I tried to recreate the moviedb app(from Brad's 50 JS Projects in 50 Days course).
I'm getting stuck and can't get the data out of a scope.
I've successfully retrieved data & destructured it.
But how can I get those values out of that scope (out of setMovies function) and use it in the Vue file (html template)?
Here's my code:
I've made the api_key private
<h1>MovieDesk</h1>
<div class="hero">
<!-- Search -->
<div class="search">
<form #submit.prevent="handleSearch">
<input type="text" placeholder="Search here..." />
<button #click="handleSearch">Search</button>
</form>
</div>
</div>
<!-- Movies -->
<div v-if="searchOn">
<SearchedMovies />
</div>
<div v-else>
<MovieList/>
</div>
</template>
<script>
// imports-------------------
import { ref } from "#vue/reactivity";
import MovieList from "../components/MovieList.vue";
import SearchedMovies from "../components/SearchedMovies.vue";
import { onMounted } from "#vue/runtime-core";
export default {
components: { MovieList, SearchedMovies },
setup() {
const searchOn = ref(false);
const api_url = ref(
"https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=api_key&page=1"
);
const movies = ref([])
// getting the data ------------------------------
onMounted(() => {
fetch(api_url.value)
.then((res) => res.json())
.then((data) => {
console.log(data);
setMovies(data.results);
});
});
function setMovies(movies) {
movies.forEach((movie) => {
const { title, poster_path, vote_average, overview } = movie;
});
}
return { searchOn, setMovies };
},
};
</script> ```
In your setMovies function, You can set the response in the movies variable and then return that variable from your setup.
function setMovies(apiResponse) {
movies.value = apiResponse
}
return { movies };
Live Demo :
const { ref, onMounted } = Vue;
const App = {
setup() {
const movies = ref([])
onMounted(() => {
const apiResponse = [{
id: 1,
name: 'Movie 1'
}, {
id: 2,
name: 'Movie 2'
}, {
id: 3,
name: 'Movie 3'
}];
setMovies(apiResponse);
})
function setMovies(res) {
movies.value = res;
}
return {
movies
};
}
};
Vue.createApp(App).mount("#app");
<script src="https://unpkg.com/vue#next"></script>
<div id="app">
<pre>{{ movies }}</pre>
</div>
Add 'movies' to the return statement at the bottom of your code, then you should be able to render it.
So I've got this page which puts out all of the profiles on my database and what I want to do is when someone clicks on an individual profile they go in to a profile page which displays the rest of the data for the corresponding id they have just clicked on but I don't seem to be able to get it to work.
Below is my js file to get individual profiles.
import { projectFirestore } from "../Firebase/Config";
import { ref } from "vue"
const getPBasic = (id) => {
const PBasic = ref(null)
const error = ref(null)
const load = async () => {
try{
let res = await projectFirestore.collection('Basic').doc(id).get()
PBasic.value = {...res.data(), id: res.id}
console.log(PBasic.value)
}
catch (err){
error.value = err.message
console.log(error.value)
}
}
return { PBasic, error, load}
}
export default getPBasic
And this is what the vue page which I want the data to appear on after they have clicked on a profile from the previous page.
<script>
import getPBasic from "../Composables/getPBasic";
const {PBasic, error, load} = getPBasic(route.params.id);
load();
export default {
name: "Slider",
data() {
return {
images: [
"/src/assets/sample-1.jpg",
"/src/assets/sample-2.jpg",
"/src/assets/sample-3.jpg",
"/src/assets/sample-4.jpg"
],
currentIndex: 0
};
},
methods: {
next: function() {
this.currentIndex += 1;
},
prev: function() {
this.currentIndex -= 1;
}
},
computed: {
currentImg: function() {
return this.images[Math.abs(this.currentIndex) % this.images.length];
}
}
};
</script>
<template>
<div v-if="error">{{ error }}</div>
<div v-if="PBasic" class="PBasic">
<br><br>
<p>{{ PBasic.Name }} </p>
<p>{{ PBaic.Age }} </p>
</div>
<div v-else>
<spinner/>
</div>
Thats what I've got so far I just shortened it for here so it didn't go on and on for too long, if anyone has any ideas I would greatly appreciate it, Thanks.
You can run your getPBasic function in page load to fetch the data you need.
One possible solution is, Run your getPBasic function in a lifecycle hook that fire on page render like mounted() hook or onMounted() in vue3 script setup.
And as we can see your getPBasic function is a synchronous function so use await to get the return value properly.
One possible code might look like this,
<script>
import getPBasic from "../Composables/getPBasic";
const {PBasic, error, load} = getPBasic(route.params.id);
export default {
name: "Slider",
mounted(){
let {PBasic,error,load} = await getPBasic();
this.PBasic = PBasic;
this.error = error;
this.load = load;
},
data() {
return {
PBasic:{},
error: null,
load: false,
images: [
"/src/assets/sample-1.jpg",
"/src/assets/sample-2.jpg",
"/src/assets/sample-3.jpg",
"/src/assets/sample-4.jpg"
],
currentIndex: 0
};
},
methods: {
next: function() {
this.currentIndex += 1;
},
prev: function() {
this.currentIndex -= 1;
}
},
computed: {
currentImg: function() {
return this.images[Math.abs(this.currentIndex) % this.images.length];
}
}
};
</script>
<template>
<div v-if="error">{{ error }}</div>
<div v-if="PBasic" class="PBasic">
<br><br>
<p>{{ PBasic.Name }} </p>
<p>{{ PBaic.Age }} </p>
</div>
<div v-else>
<spinner/>
</div>
I'm using Vuejs and Quasar on my project, I'm trying to do an autocomplete select to select some value, when the user tries to type some text, an API is sent to the server to retrieve. all the values contains that text, heres the code :
<template>
<div class="autocomplete">
<q-select
filled
label="Search"
v-model="searchTerm"
use-input
use-chips
multiple
#filter="filterFn"
#input-value="inputValue"
#filter-abort="abortFilterFn"
:options="searchResult"
style="width: 395px"
:option-label=""
option-value="text"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey"> No results </q-item-section>
</q-item>
</template>
</q-select>
<div>
<back-button #click="go" data-test="btn-back" forwardicon="arrowright" />
</div>
<script>
import { defineComponent, ref, computed } from "vue";
import { apiProductService } from "../../models/";
import apiEndPoints from "../../models/api";
import BackButton from "../ui/";
import { useRouter } from "vue-router";
import { useSearchStore } from "../../store/";
import _ from "lodash";
import { debounce } from "quasar";
export default defineComponent({
name: "search-bar",
components: { BackButton },
setup(props, context) {
const router = useRouter();
let searchTerm = ref(null);
let searchResult = ref([]);
const search = useSearchStore();
function filterFn(val, update, abort) {
if (searchResult.value.length > 0) {
update();
return;
}
abortFilterFn();
document.addEventListener('keypress', function(e) {
true
});
}
function abortFilterFn() {
// console.log('delayed filter aborted')
}
const inputValue = computed(() => debounce(suggestions, 0).bind(this));
function suggestions(val) {
if (val) {
getSearchData(val);
document.addEventListener('keypress', function(e) {
true
});
} else {
searchResult.value = [];
}
}
const getSearchData = async (val) => {
await apiProductService(
apiEndPoints.GetSearchSuggestions.method,
apiEndPoints.GetSearchSuggestions.url,
{
q: val,
top: 5,
suggester: "sg",
}
).then((response) => {
searchResult.value = response?.data?.suggestions || [];
});
};
}
return {
searchTerm,
searchResult,
filterFn,
inputValue
};
},
});
the problem with code is when I try to type for example the term "tes", I can see the data retrieved for the db, when I clic on the desired value, the type text is savec on the multi selected component like:
how can I remove the type text please ?
Use ref and use updateInputValue for clear input.
https://codepen.io/Pratik__007/pen/RwMWbwK
I'm building a simple note-taking app and I'm trying to add new note at the end of the list of notes, and then see the added note immediately. Unfortunately I'm only able to do it by refreshing the page. Is there an easier way?
I know that changing state would usually help, but I have two separate components and I don't know how to connect them in any way.
So in the NewNoteForm component I have this submit action:
doSubmit = async () => {
await saveNote(this.state.data);
};
And then in the main component I simply pass the NewNoteForm component.
Here's the whole NewNoteForm component:
import React from "react";
import Joi from "joi-browser";
import Form from "./common/form";
import { getNote, saveNote } from "../services/noteService";
import { getFolders } from "../services/folderService";
class NewNoteForm extends Form {
//extends Form to get validation and handling
state = {
data: {
title: "default title",
content: "jasjdhajhdjshdjahjahdjh",
folderId: "5d6131ad65ee332060bfd9ea"
},
folders: [],
errors: {}
};
schema = {
_id: Joi.string(),
title: Joi.string().label("Title"),
content: Joi.string()
.required()
.label("Note"),
folderId: Joi.string()
.required()
.label("Folder")
};
async populateFolders() {
const { data: folders } = await getFolders();
this.setState({ folders });
}
async populateNote() {
try {
const noteId = this.props.match.params.id;
if (noteId === "new") return;
const { data: note } = await getNote(noteId);
this.setState({ data: this.mapToViewModel(note) });
} catch (ex) {
if (ex.response && ex.response.status === 404)
this.props.history.replace("/not-found");
}
}
async componentDidMount() {
await this.populateFolders();
await this.populateNote();
}
mapToViewModel(note) {
return {
_id: note._id,
title: note.title,
content: note.content,
folderId: note.folder._id
};
}
scrollToBottom = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth" });
}
doSubmit = async () => {
await saveNote(this.state.data);
};
render() {
return (
<div>
<h1>Add new note</h1>
<form onSubmit={this.handleSubmit}>
{this.renderSelect("folderId", "Folder", this.state.folders)}
{this.renderInput("title", "Title")}
{this.renderInput("content", "Content")}
{this.renderButton("Add")}
</form>
</div>
);
}
}
export default NewNoteForm;
And here's the whole main component:
import React, { Component } from "react";
import { getNotes, deleteNote } from "../services/noteService";
import ListGroup from "./common/listGroup";
import { getFolders } from "../services/folderService";
import { toast } from "react-toastify";
import SingleNote from "./singleNote";
import NewNoteForm from "./newNoteForm";
class Notes extends Component {
state = {
notes: [], //I initialize them here so they are not undefined while componentDidMount is rendering them, otherwise I'd get a runtime error
folders: [],
selectedFolder: null
};
async componentDidMount() {
const { data } = await getFolders();
const folders = [{ _id: "", name: "All notes" }, ...data];
const { data: notes } = await getNotes();
this.setState({ notes, folders });
}
handleDelete = async note => {
const originalNotes = this.state.notes;
const notes = originalNotes.filter(n => n._id !== note._id);
this.setState({ notes });
try {
await deleteNote(note._id);
} catch (ex) {
if (ex.response && ex.response.status === 404)
toast.error("This note has already been deleted.");
this.setState({ notes: originalNotes });
}
};
handleFolderSelect = folder => {
this.setState({ selectedFolder: folder }); //here I say that this is a selected folder
};
render() {
const { selectedFolder, notes } = this.state;
const filteredNotes =
selectedFolder && selectedFolder._id //if the selected folder is truthy I get all the notes with this folder id, otherwise I get all the notes
? notes.filter(n => n.folder._id === selectedFolder._id)
: notes;
return (
<div className="row m-0">
<div className="col-3">
<ListGroup
items={this.state.folders}
selectedItem={this.state.selectedFolder} //here I say that this is a selected folder
onItemSelect={this.handleFolderSelect}
/>
</div>
<div className="col">
<SingleNote
filteredNotes={filteredNotes}
onDelete={this.handleDelete}
/>
<NewNoteForm />
</div>
</div>
);
}
}
export default Notes;
How can I connect these two components so that the data shows smoothly after submitting?
You can use a callback-like pattern to communicate between a child component and its parent (which is the 3rd strategy in #FrankerZ's link)
src: https://medium.com/#thejasonfile/callback-functions-in-react-e822ebede766)
Essentially you pass in a function into the child component (in the main/parent component = "Notes": <NewNoteForm onNewNoteCreated={this.onNewNoteCreated} />
where onNewNoteCreated can accept something like the new note (raw data or the response from the service) as a parameter and saves it into the parent's local state which is in turn consumed by any interested child components, i.e. ListGroup).
Sample onNewNoteCreated implementation:
onNewNoteCreated = (newNote) => {
this.setState({
notes: [...this.state.notes, newNote],
});
}
Sample use in NewNoteForm component:
doSubmit/handleSubmit = async (event) => {
event.preventDefault();
event.stopPropagation();
const newNote = await saveNote(this.state.data);
this.props.onNewNoteCreated(newNote);
}
You probably want to stop the refresh of the page on form submit with event.preventDefault() and event.stopPropagation() inside your submit handler (What's the difference between event.stopPropagation and event.preventDefault?).
Slight issue here which I think is relatively simple to solve but I can't quite get my head around. I'm quite new to React. I've decided to make a small sample app which just takes the input from two fields, saves them to Firebase and outputs those values on the page. It works completely fine in terms of submitting data and retrieving it, but when I click the submit button to add the data to Firebase it seems to duplicate the data stored in the state and render them twice:
Parent Component:
import React, { Component, Fragment } from 'react';
import firebase from '../../config/firebase';
import QuestFormField from './QuestFormField/QuestFormField';
import QuestFormSelection from './QuestFormSelection/QuestFormSelection';
import classes from './QuestForm.css';
class QuestForm extends Component {
state = {
value: '',
points: 0,
items: []
}
questHandler = e => {
this.setState({
value: e.target.value,
});
}
pointsHandler = e => {
this.setState({
points: e.target.value,
});
}
submitHandler = e => {
e.preventDefault();
const itemsRef = firebase.database().ref('quest');
const items = {
quest: this.state.value,
points: this.state.points
}
itemsRef.push(items);
this.setState({
value: '',
points: 0
});
}
render () {
return (
<Fragment>
<form className={classes.Form} onSubmit={this.submitHandler}>
<QuestFormField val='Quest' inputType='text' name='quest' value={this.state.value} changed={this.questHandler} />
<QuestFormField val='Points' inputType='number' name='points' value={this.state.points} changed={this.pointsHandler} />
<button>Away! To Firebase!</button>
</form>
<QuestFormSelection />
</Fragment>
);
}
}
export default QuestForm;
Child Component (Form Fields)
import React from 'react';
import classes from './QuestFormField.css';
const QuestFormField = (props) => (
<div className={classes.Container}>
<label htmlFor={props.name}>{props.val}</label>
<input type={props.inputType} name={props.name} onChange={props.changed}/>
</div>
);
export default QuestFormField;
Child Component B (Data Retriever/Displayer)
import React, { Component, Fragment } from 'react';
import firebase from '../../../config/firebase';
import classes from './QuestFormSelection.css';
class QuestFormSelection extends Component {
state = {
quests: []
}
componentDidMount() {
const database = firebase.database();
const quests = [];
database.ref('quest').on('value', (snapshot) => {
snapshot.forEach((childSnapshot) => {
quests.push({
id: childSnapshot.key,
quest: childSnapshot.val().quest,
points: childSnapshot.val().points,
});
});
console.log(quests);
this.setState(() => {
return {
quests: quests
}
});
console.log(this.state.quests);
});
}
render () {
return (
<section className='display-item'>
<div className="wrapper">
{this.state.quests.map(quest => (
<div key={quest.key}>
<p>{quest.quest}</p>
<p>{quest.points}</p>
</div>
))}
</div>
</section>
)
}
}
export default QuestFormSelection;
Example of behaviour here:
https://i.gyazo.com/c70972f8b260838b1673d360d1bec9cc.mp4
Any pointers would help :)
I haven't used firebase myself, but it looks like the code below is setting up a listener to "quest" changes which will execute each time a change occurs, but you defined const quests = [] outside of the db change handler. This means that on the second change, you will push everything in the snapshot to the same quests array that may have already had previous snapshots added to it. I believe you can fix this by moving the quests variable inside the listener function as shown below.
componentDidMount() {
const database = firebase.database();
database.ref('quest').on('value', (snapshot) => {
const quests = [];
snapshot.forEach((childSnapshot) => {
quests.push({
id: childSnapshot.key,
quest: childSnapshot.val().quest,
points: childSnapshot.val().points,
});
});
console.log(quests);
this.setState(() => {
return {
quests: quests
}
});
console.log(this.state.quests);
});
}