I am having an issue using an Interface as a Vue3 Prop, the goal is to use the interface as a type check on the prop: (We are using Nuxt3 so alot of the imports are not listed below)
// IsProduct.ts
export interface IsProduct {
_id: string;
productName: string;
icon: string
}
// ProductDisplay.vue
<script lang="ts" setup>
import { IsProduct } from "./IsProduct";
interface Props {
product: IsProduct | string | null;
}
const {
product = null,
} = defineProps<Props>();
let productObj: IsProduct = ref(null);
let productRef: string = ref(null);
let displayText: string = ref("");
watch(
() => product,
() => {
initialize();
},
{ immediate: true }
);
function initialize() {
productRef = null;
productObj = null;
if (!product) return;
if (typeof product === "object") {
productRef = product._id;
productObj = product;
}
if (typeof product === "string") {
productRef = product;
productObj = productByRef[product]; //<-- productByRef goes to Pinia store
}
}
</script>
<template>
<span>
<span v-if="productObj">{{productObj.productName}} <fa-icon :icon="productObj.icon"></span>
</span>
</template>
// ProductList.vue
<script lang="ts" setup>
const { products } = useProductStore() // <-- list of products from Pinia store
</script>
<template>
<div>
<-- THIS WORKS because the _id is a string, no Vue warnings -->
<ProductDisplay :product="p._id" v-for="p of products" :key="p._id" />
<-- HAS VUE WARNINGS because the p is the full Object-->
<ProductDisplay :product="p" v-for="p of products" :key="p._id" />
</div>
</template>
Is there a way to have Vue understand that when a full Object is passed in that it uses the IsProduct interface?
I found a 'work around' for the example that I posted above:
// IsProduct.ts
export interface IsProduct {
_id: string;
productName: string;
icon: string
}
// ProductDisplay.vue
<script lang="ts" setup>
...
interface Props {
product: {_id: string} | string | null; <-- Change the Interface to an Object
}
...
function initialize() {
productRef = null;
productObj = null;
if (!product) return;
if (typeof product === "object") {
productRef = product._id;
productObj = product as IsProduct; <-- cast the product prop as IsProduct
}
}
...
</script>
Since the ProductDisplay.vue component is looking for either a Product Object or the string _id of a Product, in the Props I just defined an Object with _id of string since that is required. This tells the Vue compiler, 'hey this is either an object or a string' and there are no warnings in the devtools console.
Later in the code I check if this is an Object and if so, then cast the Object to the 'type of' IsProduct. This removes the Typescript warnings in VSCode.
Related
I don't succeed to use a Pinia getter properly in the setup part.
It's working when I use the getter directly in the part, but not in the part.
Here is my (simplified) pinia store :
export const useCharactersStore = () => {
const charactersStoreBuilder = defineStore("characters", {
state: (): State => ({
characters: {},
}),
getters: {
getCharacter(state) {
return (characterId: string) => state.characters[characterId];
},
},
actions: {
async fetchCharacters() {
this.characters = {
1: "Character 1",
2: "Character 2",
};
},
},
});
const charactersStore = charactersStoreBuilder();
charactersStore.fetchCharacters(); // async fetching
return charactersStore;
And here is my usage :
<script setup>
import { useCharactersStore } from "#/stores/characters";
import { storeToRefs } from "pinia";
const characterId = 1;
const characterStore = useCharactersStore();
// Various alternatives :
// Don't work
const { getCharacter } = storeToRefs(characterStore);
const character = getCharacter.value(characterId);
// Don't work
const character = characterStore.getCharacter(characterId);
// Don't work
const character = characterStore.getCharacter(characterId);
// Work
const getCharacter = characterStore.getCharacter;
</setup>
<template>
<pre>
<!-- Don't work -->
{{ character }}
<!-- Work -->
{{ getCharacter(characterId) }}
</pre>
</template>
I've also tried with character as a ref, with its value updated with a watch, and various things. I'm absolutely lost :/
Your question is ambiguous. I do not know what you mean by.
It's working when I use the getter directly in the part, but not in the part.
However, I made a slight modification to your code. For me, this works.
// CharactersStore.ts
import { defineStore } from 'pinia'
export const useCharactersStore = () => {
const charactersStoreBuilder = defineStore('CharactersStore', {
state: () => {
return {
characters: {} as Record<number | string, string | number>,
}
},
actions: {
// async is irrelevant here. You can use setTimeout to simulate async here.
async fetchCharacters() {
this.characters = {
1: 'Character 1',
2: 'Character 2',
}
},
},
getters: {
getCharacterByID: (state) => (characterId: string | number) =>
state.characters[characterId],
},
})
const charactersStore = charactersStoreBuilder()
charactersStore.fetchCharacters() // async fetching
return charactersStore
}
The Test page I used.
<script setup lang="ts">
import { storeToRefs } from 'pinia'
const c = useCharactersStore()
const characterId = 1 // even though it works, It is not reactive.
const character = c.getCharacterByID(characterId)
const { getCharacterByID: getCharacterWithStoreRef } = storeToRefs(c)
</script>
<template>
<div>
<h1>Test page</h1>
<div>
<pre>
Get character by Id : {{ c.getCharacterByID(characterId) }}
character {{ character }}
getCharacterWithStoreRef {{ getCharacterWithStoreRef(characterId) }}
</pre>
</div>
</div>
</template>
Result
Test the solution provided
Feel free to let me know if it works for you. Happy coding
Additional information
Read more about Passing arguments to getters in Pinia
I've seen some examples of a button to copy data with a tooltip using vuetify2.
Out project is currently using vuetify3 version..
I've followed vuetify docs and here is my current component code:
<template>
<VTooltip :model-value="data.showTooltip">
<template v-slot:activator="{ props }">
<VBtn
icon
:color="properties.buttonColor"
:size="properties.size"
:elevation="elevation"
#click="copyData"
v-on:hover="data.showTooltip = false"
v-bind="props"
>
<VIcon>mdi-content-copy</VIcon>
</VBtn>
</template>
Copied to clipboard
</VTooltip>
</template>
<script setup lang="ts">
export interface CopyButtonProps {
size?: string;
elevation?: number | undefined;
data: string;
tooltipLocation?: string | undefined;
tooltipTimeoutMs?: number;
buttonColor?: string;
}
export interface CopyButtonData {
showTooltip: boolean;
}
const properties = withDefaults(defineProps<CopyButtonProps>(), {
size: "small",
elevation: 1,
tooltipLocation: "end",
buttonColor: "white transparent",
tooltipTimeoutMs: 1000,
});
const data = reactive<CopyButtonData>({
showTooltip: false,
});
function showTooltip() {
data.showTooltip = true;
setTimeout(() => {
data.showTooltip = false;
}, properties.tooltipTimeoutMs);
}
async function copyData(isTooltipActive: boolean) {
if (!properties.data) return;
await navigator.clipboard.writeText(properties.data);
showTooltip();
return true;
}
</script>
The code above makes the tooltip opens on click, but is also opening on hover, and I don't want this behavior. Do you have any suggestions to solve it?
:open-on-hover="false"
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.
catalogueSelection in Store Image
I have the data I require in state (catalogueSelection: searchTextResult & categoryCheckboxResult) and need to pass the string 'SearchTextResult' into one component and the Array 'categoryCheckboxResult' into another.
When I try to retrieve the required values I am retrieving the whole store. I have looked at numerous websites and entries here but getting very confused now.
Model:
export class SearchTextResult {
searchTextResult: string;
}
export class CategoryCheckboxResult {
categoryCheckboxResult:Array<CategoryCheckboxResult>;
}
Actions:
import { Action } from '#ngrx/store';
import { SearchTextResult, CategoryCheckboxResult } from 'app/#core/services/products/products.model';
export enum UserCatalogueSelectionTypes {
AddSearchTextResult = '[SearchTextResult] AddResult',
AddCategoryCheckboxResult = '[CategoryCheckboxResult] AddResult',
GetSearchTextResult = '[SearchTextResult] GetResult',
}
export class AddSearchTextResult implements Action {
readonly type = UserCatalogueSelectionTypes.AddSearchTextResult;
constructor(public payload: SearchTextResult){
}
}
export class AddCategoryCheckboxResult implements Action {
readonly type = UserCatalogueSelectionTypes.AddCategoryCheckboxResult;
constructor(public payload: CategoryCheckboxResult){
}
}
export class GetSearchTextResult implements Action {
readonly type = UserCatalogueSelectionTypes.GetSearchTextResult;
}
export type UserCatalogueSelectionUnion =
| AddSearchTextResult
| AddCategoryCheckboxResult
| GetSearchTextResult
Reducers:
import { SearchTextResult, CategoryCheckboxResult} from "app/#core/services/products/products.model";
import { UserCatalogueSelectionTypes, UserCatalogueSelectionUnion} from "../actions/products.actions";
export interface UserCatalogueSelectionState {
searchTextResult: SearchTextResult | null;
categoryCheckboxResult: CategoryCheckboxResult | null;
}
export const initialState: UserCatalogueSelectionState = {
searchTextResult: null,
categoryCheckboxResult: null,
}
export function reducer(state:UserCatalogueSelectionState = initialState, action: UserCatalogueSelectionUnion ): UserCatalogueSelectionState{
switch (action.type) {
case UserCatalogueSelectionTypes.AddSearchTextResult:
return {
...state,
searchTextResult: action.payload,
};
case UserCatalogueSelectionTypes.AddCategoryCheckboxResult:
return {
...state,
categoryCheckboxResult: action.payload,
};
case UserCatalogueSelectionTypes.GetSearchTextResult: {
return state;
}
default: {
return state;
}
}
}
Selectors:
import { createSelector,createFeatureSelector } from "#ngrx/store";
import {UserCatalogueSelectionState} from '../../store/reducer/products.reducer';
export const fetchSearchTextResults = createFeatureSelector<UserCatalogueSelectionState>("searchTextResult");
export const fetchSearchTextResult = createSelector (
fetchSearchTextResults,
(state:UserCatalogueSelectionState) => state.searchTextResult.searchTextResult
);
export const fetchCatalogueCheckBoxResults = createFeatureSelector<UserCatalogueSelectionState>("catalogueCheckboxResult");
export const fetchCatalogueCheckBoxResult = createSelector (
fetchCatalogueCheckBoxResults,
(state: UserCatalogueSelectionState) => state.categoryCheckboxResult.categoryCheckboxResult
);
My Component 1
Observable:
public searchTextResult: Observable<String>;
Contructor: (part of)
private store: Store<fromCatalogueSelection.UserCatalogueSelectionState>
Code Snippet: (asking for the data)
this.searchTextResult = this.store.select('SearchTextResult');
console.log('TESTING SEARCH TEXT: ', this.searchTextResult);
Console:
TESTING SEARCH TEXT: StoreĀ {_isScalar: false, actionsObserver: ActionsSubject, reducerManager: >ReducerManager, source: Store, operator: DistinctUntilChangedOperator}
My Component 2
Observable
searchTextResult$: Observable<CatalogueSelectionActions.GetSearchTextResult>;
Code Snippet: (asking for the data)
this.searchTextResult$ = this.store.select('GetSearchTextResult');
console.log('TESTING SEARCH TEXT: ', this.searchTextResult$);
Console:
TESTING SEARCH TEXT: StoreĀ {_isScalar: false, actionsObserver: ActionsSubject, reducerManager: > ReducerManager, source: Store, operator: DistinctUntilChangedOperator}
I've given up on the Selectors for the moment. Any help much appreciated as I'm going a round in circles.
You are almost there, the value from the console log is the observable object, everytime you select something from the store, you will get the value wrapped within an observable. You just need to subscribe to it:
this.searchTextResult$ = this.store.select('GetSearchTextResult');
this.searchTextResult$.subscribe((yourData) => console.log(yourData));
Also, since you are working with selectors, use them, you don't have to write the state/selector name:
selector
...
export const fetchCatalogueCheckBoxResult = createSelector (
fetchCatalogueCheckBoxResults,
(state: UserCatalogueSelectionState) =>
state.categoryCheckboxResult.categoryCheckboxResult
);
component
import * as YourSelectors from './store/something/selectors/yourthing.selectors'
...
...
this.searchTextResult$ = this.store
.select(YourSelectors.fetchCatalogueCheckBoxResult)
.subscribe(console.log);
Additionally, try to subscribe using the async pipe delegating that to your template html so you don't have to deal with the subscription in the code, for example:
component
...
export class Component {
searchTextResult$!: Observable<any> // your data type here
...
...
this.searchTextResult$ = this.store
.select(YourSelectors.fetchCatalogueCheckBoxResult)
}
html
<ng-container *ngIf="(searchTextResult$ | async) as result">
<p>Your result value: {{ result }}</p>
</ng-container>
I am trying to upload a file with vue, but the issue I am stuck with is this,
I can't access this.imageFile.value after selecting the photo.
The data I returned from setup but undefined.
Under normal conditions, imageFile.value works in setup.
<template>
<img v-show="imageUrl" :src="imageUrl" alt="">
<input #change="handleImageSelected,getdata" ref="fileInput" type="file" accept="image/*">
</template>
<script>
import {UseImageUpload} from "./use/UseImageUpload";
export default {
data() {
return {
Hello: null
}
},
methods: {
getdata() {
this.Hello = this.imageFile.value
}
},
setup() {
let {imageFile, imageUrl, handleImageSelected} = UseImageUpload();
return {
imageFile,
handleImageSelected,
imageUrl,
}
}
}
</script>
UseImageUpload.js
import {ref, watch} from "vue";
export function UseImageUpload() {
//image
let imageFile = ref("");
let imageUrl = ref("");
function handleImageSelected(event) {
if (event.target.files.length === 0) {
imageFile.value = "";
imageUrl.value = "";
return;
}
imageFile.value = event.target.files[0];
}
watch(imageFile, (imageFile) => {
if (!(imageFile instanceof File)) {
return;
}
let fileReader = new FileReader();
fileReader.readAsDataURL(imageFile);
fileReader.addEventListener("load", () => {
imageUrl.value = fileReader.result;
});
});
return {
imageFile,
imageUrl,
handleImageSelected,
};
}
First of all please try not to mix Options and Composition API - I know it might work but it is not necessary and in the most cases just an anti-pattern.
Composition API is there to replace the Options API or rather to give an alternative. They are just not supposed to work together or to be used together.
So this would improve your code:
setup() {
const Hello = ref(null);
const {imageFile, imageUrl, handleImageSelected} = UseImageUpload();
function getdata() {
Hello = imageFile.value
}
return {
imageFile,
handleImageSelected,
imageUrl,
getdata,
}
This should also fix your issue.