I want to make an increment on firestore database entry, as I press the everything works fine: I can create database to the server, and the aollcation of the names are correct, until I create an increment on Add Category Infomation.
this is my firebase console so far, it add two entry separately.
enter image description here
enter image description here
This is the complete code below:
<template>
<div class="CategoyAddAll">
<from #sumbit.prevent="addCategory">
<div class="grid gird-cols-2 gap-2">
<div><label>Category Name</label></div>
<div><input ref="category_enter" type="text" required/></div>
</div>
</from>
<button #click="createCategory" class="btn btn-info" >Add Category Information </button>
<br>
<br>
</div>
</template>
<script>
import { db, auth, fv } from "#/firebase.js";
import { create } from "domain";
import { collection, addDoc, FieldValue, DocumentReference } from "firebase/firestore";
export default{
name: 'CategoyAddAll',
setup() {},
data(){
return{
all_category: [],
client:{
category_fullname: null,
category_counter: null,
}
}
},
components: {},
methods:{
//
async createCategory(){
console.log("[CategoryAddAll] create new Category.");
const db_id = firebase.firestore();
const get_id = db_id.collection('all_categorys').doc();
const category_id = get_id.id;
console.log("[CategoryAddAll] id.");
const increment = firebase.firestore.FieldValue.increment(1); //always "1"
console.log("[CategoryAddAll] id2.");
const ref = collection(db, 'all_categorys');
const ref2 = db_id.collection('all_categorys'); //ignore this
//get_id.update({ reads: increment });
//https://fireship.io/snippets/firestore-increment-tips/
console.log("[CategoryAddAll] update incremented. " + this.$refs.category_enter.value);
const obj_ref ={
category_fullname : this.$refs.category_enter.value,
}
const doc_ref = await addDoc(ref, obj_ref);
const batch = db_id.batch();
batch.set(get_id, {category_increment: increment}, { merge: true }); //incrment in seperate value, always 1
batch.commit();
//get_id.update({ category_increment: increment });
},
}
}
</script>
I install Firebase correclt and it runs and able to add data, how come the "const increment = firebase.firestore.FieldValue.increment(1)" won't work? any help would be helpful? thank you.
I want to be like this below:
{
category_fullname: test_FULLname
category_increment : 11 //12 13 so on.
}
The category_increment should be based on how many entry I do. Lat's say if I have 10 category inside the database, the next entry of category_increment should be 11, even if there this an element delte, it will also become 12 and so on...
Related
I have a use case with the pinia in vue 3 that I want to dynamically add new entries to the pinia store using the store actions. for example if I have a state called firstName and if I call a the action of the store it should add new state called lastName in the state as well. Here is what I have tried
import { defineStore } from "pinia";
export const useAdvanceListingsFilterStore = defineStore(
"advance-listing-filters",
{
state: () => {
return {
firstName: "jhon",
};
},
actions: {
setLastName(payload) {
return {
...this.state,
lastName: payload,
};
},
},
}
);
The new state should include the fistName and lastName fields.
The simplest way would by add lastName: null to your state, but I guess it is not what you are trying to achieve.
I have tried to add new items to the state internally using state.$path (see Mutating the state), but the new lastName item was still not accessible outside the state using state.lastName. So I haven't found a way to achieve your goal directly. But there is another way to do it.
You can use a nested object to achieve your goal.
See the playground below.
I should also state, that adding state items dynamically makes your application less predictable.
So, I would rather rethink the design and flow of data.
const { ref, createApp, defineComponent } = Vue
const { createPinia, defineStore, storeToRefs } = Pinia
const useAlertsStore = defineStore("advance-listing-filters",
{
state: () => {
return {
user: { firstName: "John" }
};
},
actions: {
setLastName(payload) {
this.$state.user.lastName = payload;
}
}
}
)
const App = {
setup() {
const store = useAlertsStore()
const { user } = storeToRefs(store)
const lastName = ref('Doe')
return {
store,
user,
lastName
}
}
}
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
<div id="app">
<label>Name: </label> <b>{{store.user.firstName}} {{store.user.lastName}}</b><br /><br />
<label>First Name:</label> <input :value="user.firstName" disabled /><br /><br />
<label>Last Name:</label> <input v-model="lastName" /><br /><br />
<button #click="store.setLastName(lastName)">Set Last Name</button>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/vue-demi#0.13.11/lib/index.iife.js"></script>
<script src="https://unpkg.com/pinia#2.0.30/dist/pinia.iife.js"></script>
hope you're well!
I have a Vue 3 app using Pinia + Vuetify 3. I've defined a "client" store and a component that, upon render, will call a store action that calls my backend API and sets my client state (JSON) with the result.
clientStore.js:
export const useClientStore = defineStore('clients', {
state: () => ({
//Loading state and client(s)
loading: false,
clients: [],
client: {}
}),
getters: {
//Get all clients
getClients(state) {
return state.clients
},
//Get one client
getClient(state) {
return state.client
}
},
actions: {
//Get one client
async fetchClient(clientId) {
try {
this.loading = true
const data = await axiosConfig.get('/clients/' + clientId)
this.client = data.data
this.loading = false
} catch (error) {
this.loading = false
console.log("Error fetching client: " + clientId)
},
//snipped
I have a computed property that returns the client from the store and render them as follows:
Component.vue:
<template>
<div class="text-center py-5">
<div class="text-h4 font-weight-bold">{{ client.name }}</div>
</div>
<div class="d-flex justify-space-between">
<div class="text-h5">Description</div>
<v-btn #click="dialog = true" prepend-icon="mdi-cog" color="primary">Edit</v-btn>
</div>
<v-textarea class="py-5" :value="client.description" readonly auto-grow outlined>{{ client.description
}}</v-textarea>
<updateClient v-model="dialog" />
</template>
<script setup>
import updateClient from '#/components/clients/updateClient.vue'
import { useClientStore } from '#/store/clients'
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router'
const store = useClientStore()
const route = useRoute()
const dialog = ref(false)
const client = computed(() => {
return store.client
})
onMounted(() => {
store.fetchClient(route.params.clientId)
})
</script>
My aim is to make an "EDIT" component - a popup dialog - that takes the client state values and pre-populate them in my text fields and upon changing the values, submit and PATCH the client in the backend.
updateClient.vue
<template>
<v-dialog max-width="500">
<v-card class="pa-5">
<v-card-title>Edit client</v-card-title>
<v-text-field label="Name" v-model="client.name"></v-text-field>
<v-textarea label="Description" v-model="client.description"></v-textarea>
<v-btn block outlined color="primary" #click="updateClient">Update Client</v-btn>
</v-card>
</v-dialog>
</template>
<script setup>
import { useClientStore } from '#/store/clients'
import {computed} from 'vue'
const store = useClientStore()
const client = computed(() => {
return store.client
})
</script>
Problem is when I edit the pre-populated values in the fields, it changes the values outside the dialog as seen in the video and stay changed even after closing the pop-up. Ideally I'd like the values in my Component.vue to be static and have my state values unaltered. How can this be solved?
Thanks!
When you bind client.name to a text field in "Edit component", you directly change values stored in pinia. This, by design, changes values in your "View component".
A simple answer is... just create a copy of the object.
Now, I know, I know... there is a reason why you used computed properties in both places. Because you're waiting on the server to return the initial values.
The easiest way to solve this is to create a copy of the client object in pinia store. Then, just use copy of the object for text field binding in "Edit component".
state: () => ({
//Loading state and client(s)
loading: false,
clients: [],
client: {},
clientEdit: {} // Make changes to this object instead
})
In api response
actions: {
//Get one client
async fetchClient(clientId) {
try {
this.loading = true
const data = await axiosConfig.get('/clients/' + clientId)
this.client = data.data
this.clientEdit = { ...this.client } // Copy client object
this.loading = false
} catch (error) {
this.loading = false
console.log("Error fetching client: " + clientId)
},
}
const Fetch = ({nationalize}) => {
return (
<div>
<h1>Nationalize</h1>
<h5>
<h4>You're {nationalize.name} and here's your results</h4>
{nationalize.country.map((i)=>{
return(
<div key={i.country_id}>
<h5>{(i.probability)*100}% {i.country_id} </h5>
</div>
)
})}
</h5>
</div>
);
}
export const getStaticProps = async (ctx) => {
const res = await fetch('https://api.nationalize.io?name=joe')
const nationalize = await res.json()
return {
props:{
nationalize,
}
}
}
export default Fetch;
So this is my NextJs page fetch API from nationalize.io, the API takes a name as a query. The code works just fine but I wanted to take the name query from an input field instead of being set manually. In this example it is Joe 'https://api.nationalize.io?name=joe', any suggestion? thank you
You use getStaticProps which pre-builds the page before sending it to your browser, so all data inside getStaticProps has to be static as the name says, and not being taken from any user input or query params.
If you'd like someone to change the name, you can either render it on the server using getServerSideProps and taking the name from the params, or fetch it on client-side via a user input as you mentioned:
import { useState } from "react";
const Fetch = () => {
const [name, setName] = useState("");
const [nationalize, setNationalize] = useState();
const submitName = async (name) => {
const res = await fetch("https://api.nationalize.io?name=" + name);
const nationalizeJson = await res.json();
setNationalize(nationalizeJson);
};
return (
<div>
<h1>Nationalize</h1>
{nationalize && (
<>
<h4>You're {nationalize.name} and here's your results</h4>
{nationalize.country.map((i) => {
return (
<div key={i.country_id}>
<h5>
{i.probability * 100}% {i.country_id}{" "}
</h5>
</div>
);
})}
</>
)}
<div>
<input
id="input"
type="text"
placeholder="Enter your name"
value={name}
onChange={(e) => setName(e.target.value)}
style={{ padding: "10px" }}
/>
<button
onClick={() => {
if (!name) return;
submitName(name);
}}
>
Submit
</button>
</div>
</div>
);
};
export default Fetch;
Here's how it works:
Let user enter his name with a controlled text input (https://reactjs.org/docs/forms.html#controlled-components) and store the value in the state name
When a user clicks the "Submit" button, call the api as you did in getStaticProps but take the name from the state name, and store the returned JSON from the api in the state nationalize.
In the JSX part, check if nationalize is defined, and only then display the information fetched from the api.
This is obviously just a demo and could use some optimization like styling or a loading indicator.
I am using Nuxt RC8 combined with Firestore.
My goal is to make the firestore request SSR and then combine it with Firestore's onSnapshot to get realtime updates after hydration is done.
I have created this composable useAssets:
import { computed, ref } from 'vue';
import { Asset, RandomAPI, RandomDatabase } from '#random/api';
/**
* Asset basic composable
* #param dbClient Database client
* #param options Extra options, like live data binding
*/
export function useAssets(dbClient: RandomDatabase) {
const assets = ref([]);
const unsubscribe = ref(null);
const searchQuery = ref('');
const randomAPI = RandomAPI.getInstance();
async function fetchAssets(options?: { live: boolean }): Promise<void> {
if (options?.live) {
try {
const query = randomAPI.fetchAssetsLive(dbClient, (_assets) => {
assets.value = _assets as Asset<any>[];
});
unsubscribe.value = query;
} catch (error) {
throw Error(`Error reading assets: ${error}`);
}
} else {
const query = await randomAPI.fetchAssetsStatic(dbClient);
assets.value = query;
}
}
const filteredAssets = computed(() => {
return searchQuery.value
? assets.value.filter((asset) =>
asset.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
: assets.value;
});
function reverseAssets(): void {
const newArray = [...assets.value];
assets.value = newArray.reverse();
}
return {
assets,
fetchAssets,
filteredAssets,
searchQuery,
reverseAssets,
unsubscribe,
};
}
The randomAPI.fetchAssetsLive comes from the firestore queries file:
export function fetchAssetsLive({
db,
callback,
options,
}: {
db: Firestore;
callback: (
assets: Asset<Timestamp>[] | QueryDocumentSnapshot<Asset<Timestamp>>[]
) => void;
options?: { fullDocs: boolean };
}): Unsubscribe {
const assetCollection = collection(db, 'assets') as CollectionReference<
Asset<Timestamp>
>;
if (options?.fullDocs) {
return onSnapshot(assetCollection, (querySnapshot) =>
callback(querySnapshot.docs)
);
}
// Return unsubscribe
return onSnapshot(assetCollection, (querySnapshot) =>
callback(querySnapshot.docs.map((doc) => doc.data()))
);
}
And then the component:
<template>
<div>
<h1>Welcome to Random!</h1>
<Button #click="reverseAssets">Reverse order</Button>
<ClientOnly>
<!-- <Input name="search" label="Search for an asset" v-model="searchQuery" /> -->
</ClientOnly>
<ul>
<li class="list-item" v-for="asset in assets" :key="asset.name">
Asset Name: {{ asset.name }} Type: {{ asset.type }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { Button, Input } from '#random/ui';
import { useNuxtApp, useAsyncData } from '#app';
const { $randomFirebase, $firestore, $getDocs, $collection } = useNuxtApp();
const { fetchAssets, filteredAssets, searchQuery, reverseAssets, assets } =
useAssets($randomFirebase);
// const a = process.client ? filteredAssets : assets;
onMounted(() => {
// console.log(searchQuery.value);
// fetchAssets({ live: true });
});
watch(
assets,
(val) => {
console.log('watcher: ', val);
},
{ deep: true, immediate: true }
);
// TODO: make SSR work
await useAsyncData(async () => {
await fetchAssets();
});
</script>
Why is it only loading via SSR and then assets.value goes []? Refreshing the page retrieves renders the items correctly but then once hydration comes in, it's gone.
Querying both, in onMounted and useAsyncData, makes it send correctly via SSR the values, makes it work client-side too but there is still a hydration missmatch, even being the values the same. And visually you only see the ones from the client-side request, not the SSR.
Is there a better approach? What am I not understanding?
I don't want to use firebase-admin as the SSR query maker because I want to use roles in the future (together with Firebase Auth via sessions).
I solved the hydration issue in two ways:
By displaying in the template only specific information, since JS objects are not ordered by default so there could be different order between the SSR query and the CS query.
By ordering by a field name in the query.
By making sure that the serverData is displayed until first load of the onsnapshot is there, so theres is not a mismatch this way: [data] -> [] -> [data]. For now I control it in the template in a very cheap way but it was for testing purposes:
<li class="list-item" v-for="asset in (isServer || (!isServer && !assets.length) ? serverData : assets)" :key="asset.name">
Asset Name: {{ asset.name }} Type: {{ asset.type }}
</li>
By using /server/api/assets.ts file with this:
import { getDocs, collection, query, orderBy, CollectionReference, Timestamp, Query } from 'firebase/firestore';
import { Asset } from '#random/api/dist';
import { firestore } from '../utils/firebase';
export default defineEventHandler(async (event) => {
const assetCollection = collection(firestore, 'assets');
let fullQuery: CollectionReference<Asset<Timestamp>> | Query<Asset<Timestamp>>;
try {
// #ts-ignore
fullQuery = query(assetCollection, orderBy('name'));
} catch (e) {
console.error(e)
// #ts-ignore
fullQuery = assetCollection;
}
const ref = await getDocs(fullQuery);
return ref.docs.map((doc) => doc.data());
});
And then in the component, executing:
const { data: assets } = useFetch('/api/assets');
onMounted(async () => {
fetchAssets({ live: true });
});
Still, if I try via useAsyncData it does not work correctly.
I'm working with next.js and firebase.
I'm doing authentication with google and with phone number.
The problem I have happens when authenticating with phone number, when the recaptcha is rendered, I can't click it and move forward to the next steps in the auth flow, because a transparent container also is rendered and overlaps completly ui.
I don't know why this happens, I followed every step of the google guide:
Phone authentication
My page component is the following...
In the page component I'm rendering a different form, one if it's time to write the verification code sent via SMS and another which is the first in being showed, to write and send the phone number.
import AppHead from '../../components/head'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from "react-i18next"
import Select from '../../components/select'
import Nav from '../../components/nav'
import PhoneNumberInput from '../../components/phone-input'
import Dialog from '../../components/dialog'
import Footer from '../../components/footer'
import { isPossiblePhoneNumber } from 'react-phone-number-input'
export default function Home({ M, authService, libService }) {
const verificationCodeInput = useRef(null)
const [phoneNumber, setPhoneNumber] = useState("")
const [shouldVerify, setShouldVerify] = useState(false)
const [openDialog, setOpenDialog] = useState(false)
const [valid, setValid] = useState(2)
const [confirmationCode, setConfirmationCode] = useState('')
const { t, i18n } = useTranslation('common')
let verificationValidClass = ""
const onChangePhoneNumber = (value) => {
setPhoneNumber(value)
};
const onClickLoginWithPhone = async (evt) => {
evt.preventDefault()
try {
if (isPossiblePhoneNumber(phoneNumber)) {
const confirmationResult = await authService.signInWithPhoneNumber(phoneNumber, window.recaptchaVerifier)
window.confirmationResult = confirmationResult
setShouldVerify(true)
} else {
setOpenDialog(true)
}
} catch (error) {
console.log(error)
}
};
const phoneNumberForm = (
<form className="home__formLogin">
<PhoneNumberInput onChangePhoneNumber={onChangePhoneNumber} phoneNumber={phoneNumber} labelText={t("forms.auth.phoneFieldLabel")} />
<button data-target="phoneLoginModal" className="btn-large home__login-phone modal-trigger" onClick={onClickLoginWithPhone}>
<span className="material-icons home__sendIcon" aria-hidden="true">
send
</span>
{t("forms.auth.sendPhone")}
</button>
<div id="recaptcha-container">
</div>
</form>
)
if (valid === 0) {
verificationValidClass = "invalid"
} else if (valid === 1) {
verificationValidClass = "valid"
}
useEffect(() => {
const elems = document.querySelectorAll('.modal');
const instances = M.Modal.init(elems);
window.recaptchaVerifier = authService.getVerifier('recaptcha-container', (response) => setShouldVerify(true), () => console.log("captcha-expired"))
}, []);
return (
<>
<main className="main">
<div className="main__layout home">
<Nav top={true} content={pickLanguage} contentLg={pickLanguageLG} />
<AppHead title={t("pages.login_phone.meta-title")} description={t("metaDescription")} />
<header className="home__header">
<h1>{shouldVerify ? t("pages.home.titleCode") : "LOGIN"}</h1>
<p>{t("pages.login_phone.subtitle_1")} <br /> {t("pages.login_phone.subtitle_2")}</p>
</header>
<section className="home__login-options">
<h1 className="home__formTitle">{shouldVerify ? t("pages.home.code") : t("pages.home.loginHeading")}</h1>
{shouldVerify ? verificationCodeForm : phoneNumberForm}
<Dialog open={openDialog} onConfirm={onClickConfirmDialog} heading={t("components.dialog.wrongPhoneNumber")} confirmText={t("components.dialog.wrongPhoneNumberConfirm")}>
{t("components.dialog.wrongPhoneNumberContent")}
</Dialog>
</section>
</div>
</main>
<Footer />
</>
)
}
In the useEffect callback I create the verifier object and then in onClickLoginWithPhone I submit the phone number.
The function call authService.signInWithPhoneNumber(phoneNumber, window.recaptchaVerifier) is implemented as shown below:
export default function AuthService(authProvider,firebase,firebaseApp) {
return Object.freeze({
signInWithPhoneNumber,
getVerifier,
getCredentials
})
function getVerifier(id,solvedCallback,expiredCallback){
return new firebase.auth.RecaptchaVerifier("recaptcha-container",{
'size': 'normal',
'callback': solvedCallback,
'expired-callback': expiredCallback
});
}
//used when user signs in with phone number and need to introduce a verification code sended via SMS
async function getCredentials(verificationId,verificationCode){
const credential = firebase.auth.PhoneAuthProvider.credential(verificationId,verificationCode);
const userCredential = await firebase.auth(firebaseApp).signInWithCredential(credential);
return userCredential
}
async function signInWithPhoneNumber(phoneNumber, phoneVerifier) {
const confirmationResult = await firebase.auth(firebaseApp).signInWithPhoneNumber(`${phoneNumber}`, phoneVerifier)
return confirmationResult
}
}
The container that is displayed is the following:
The part highlighted in blue is the full element which appears when I submit the phone number, and the part highlighted in red is the div which spans all the viewport and dont let me click the captcha widget
I really don't know why this thing is displayed, I'm not manipulating the DOM when submit the phone number, so I guess this problem is related to using next with firebase or with firebase itself.
Note: t("<json-object-field>"), t is just a function which reads an specific json file (a different one depending on the language the page is being showed) which contains all the strings translated to an specific language and this function returns a translated string individually.
The captcha widget is rendered but the container overlaps it, the container is transparent.