How to create a translator with dictionaries using Composition API - vuejs3

I'm trying to understand how to use Composition API. In this simple app I'm trying to implement a reactive translator with multiple dictionaries. Basically, what I want to do is to click on a button and change a language on a page. Right now clicking it does nothing.
Here's where I'm stuck, please check it on flems.io
index.html
<div id="app">
<div>Hello: {{ Hello }}</div>
<div>World: {{ World }}</div>
<button #click="changeLangButtonClickHandler">Change lang</button>
</div>
App.js
const App = {
setup () {
const { dictionary, changeLanguage, language } = useTranslator()
return { ...dictionary.value, changeLanguage, language }
},
methods: {
changeLangButtonClickHandler () {
const newLanguage = this.language === 'en' ? 'ru' : 'en'
this.changeLanguage(newLanguage)
}
}
}
Vue.createApp(App).mount('#app')
translator.js
const language = Vue.ref('en')
const dictionaries = Vue.ref({
ru: {
Hello: 'Привет',
World: 'Мир'
},
en: {
Hello: 'Hello',
World: 'World'
}
})
function useTranslator () {
const dictionary = Vue.computed(() => {
return dictionaries.value[language.value]
})
function changeLanguage (lang) {
language.value = lang
}
return { dictionary, changeLanguage, language }
}

The problem is that if you're using return { ...dictionary.value, changeLanguage, language } the ...dictionary.value will cease being reactive. That's why switching to ...dictionary and distionary.Hello works.
on a side note..., you don't even need a useTranslator function, you can just export the functions and variables like this
// private
const dictionaries = Vue.ref({
ru: {
Hello: "Привет",
World: "Мир"
},
en: {
Hello: "Hello",
World: "World"
}
});
// public
export const language = Vue.ref("en");
export const dictionary = Vue.computed(() => {
return dictionaries.value[language.value];
});
export const changeLanguage = lang => {
language.value = lang;
};
The useSomethingSomething() function is helpful if you want to pass aspects of the component or a variable to the function. Or have a stylistic preference.

Here's a working version.
I replaced <div>Hello: {{ Hello }}</div> with <div>Hello: {{ dictionary.Hello }}</div> in index.html
And also replaced { ...dictionary.value, changeLanguage, language } with return { dictionary, changeLanguage, language } in setup function
The issue had something to do with how reactivity system works

Related

How to use pinia getter in setup ? (with a wrapped defineStore and async fetching)

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

Accessing a variable name in vue 3 like a class component does

I have only use Vue 2 with class components. Example:
#Options({
components: {
HelloWorld,
},
})
export default class Home extends Vue {}
However now that it is deprecated I am having issues access variables. I am not sure why I cannot use this anymore; I do not quite understand how the template will be aware of the variables and how the void methods can manipulate them.
</button>
{{name}}-{{age}}
env: {{ mode }} - My name: {{ myName}}
</div>
</template>
<script lang="ts">
import {api} from "#/lib/api";
export default {
name: "Home",
data() {
return {
name: String,
age: Number,
mode: process.env.NODE_ENV,
myName: process.env.VUE_APP_TITLE
}
},
methods: {
submit(): void {
api.getTest().then(
response => {
const testResponse = JSON.stringify(response)
this.name = JSON.parse(testResponse).name
this.age = parseInt(JSON.parse(testResponse).age)
}).catch(response => {
console.log("Error while getting the response", response)
})
},
counter(age: number): void {
age = age + 1
}
}
}
</script>
--- update 1 ----
I received some excellent advice from a poster, suggesting I ref or reactive.
Vue 3 is built with typescript which is why class components were decided to be deprecated. However I am not able to use my gRPC generated type objects, or at least I do not know how at this moment
IDE
axios
export const api = {
async getTest() {
try{
return await grpcClient.get<TestResponse>("/v1/test")
.then(res => {
console.log(url.baseUrl)
return res.data
})
}catch (err) {
console.log("error" + err);
}
},
}
So vue3 way of defining component is a bit different than v2 - more like native JS. Here's quick example how you component could look like in vue3. Instead of methods, just create function. Instead of data use reactive or ref.
import { reactive, computed } from 'vue'
import { api } from '#/lib/api'
export default {
setup() {
const state = reactive({
name: '',
age: 0,
mode: process.env.NODE_ENV,
myName: process.env.VUE_APP_TITLE
})
const submit = async () => {
try {
const response = await api.getTest()
state.name = response.name
state.age = response.age
} catch (error) {
console.log('Error while getting the response:', error)
}
}
const counter = (age) => {
state.age = age + 1
}
return {
...state,
submit
}
}
}

vue3 file upload get value undefined

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.

Vue 3 compoition API computed function

Trying to switch my code to the new composition API that comes with Vue 3 but I cant get it to work.
export default {
props: {
classProp: {type: String},
error: {type: String},
},
setup(){
// move to here (this is not working)
computed(() => {
const classObject = () => {
return ['form__control', this.classProp,
{
'form__invalid': this.error
}
]
}
})
},
computed: {
classObject: function () {
return ['form__control', this.classProp,
{
'form__invalid': this.error
}
]
}
},
}
skip "computed" all together
you need to use "ref" or "reactive". these are modules:
<script>
import { ref } from 'vue'
setup(){
const whateverObject = ref({ prop: "whatever initial value" });
whateverObject.value.prop= "if you change something within setup you need to access it trough .value";
return { whateverObject } // expose it to the template by returning it
}
</script>
if you want to use classes you import them like in this example of my own:
import { APIBroker } from '~/helpers/APIbroker'
const api = new APIBroker({})
Now "api" can be used inside setup() or wherever

Redux state and component property undefined until ajax resolves

My component get some properties via props with the function:
const mapStateToProps = state => {
const { entities: { keywords } } = state
const {locale} = state
return {
keywords: keywords[locale]
}
}
I got state keywords using ajax, in the same component:
componentDidMount() {
this.props.loadKeywords()
}
My component gets rendered twice. First, before the ajax resolves, so in my render method I got undefined:
render() {
const { keywords } = this.props.keywords
...
Which is the proper way to solve it? I changed componentDidMount to componentWillMount without success.
Right now, based on the real-world example, I have initialized keywords state with an empty object:
function entities(state = { users: {}, repos: {}, keywords: {} }, action) {
if (action.response && action.response.entities) {
return merge({}, state, action.response.entities)
}
return state
}
My reducer:
import { combineReducers } from 'redux'
import { routerReducer as router } from 'react-router-redux'
import merge from 'lodash/merge'
import locale from './modules/locale'
import errorMessage from './modules/error'
import searchText from './modules/searchText'
// Updates an entity cache in response to any action with response.entities.
function entities(state = { users: {}, repos: {}, keywords: {} }, action) {
if (action.response && action.response.entities) {
return merge({}, state, action.response.entities)
}
return state
}
export default combineReducers({
locale,
router,
searchText,
errorMessage,
entities
})
My action:
import { CALL_API, Schemas } from '../middleware/api'
import isEmpty from 'lodash/isEmpty'
export const KEYWORDS_REQUEST = 'KEYWORDS_REQUEST'
export const KEYWORDS_SUCCESS = 'KEYWORDS_SUCCESS'
export const KEYWORDS_FAILURE = 'KEYWORDS_FAILURE'
// Fetches all keywords for pictos
// Relies on the custom API middleware defined in ../middleware/api.js.
function fetchKeywords() {
return {
[CALL_API]: {
types: [ KEYWORDS_REQUEST, KEYWORDS_SUCCESS, KEYWORDS_FAILURE ],
endpoint: 'users/56deee9a85cd6a05c58af61a',
schema: Schemas.KEYWORDS
}
}
}
// Fetches all keywords for pictograms from our API unless it is cached.
// Relies on Redux Thunk middleware.
export function loadKeywords() {
return (dispatch, getState) => {
const keywords = getState().entities.keywords
if (!isEmpty(keywords)) {
return null
}
return dispatch(fetchKeywords())
}
}
All based on the Real world redux example
My Solution
Given initial state to keywords entity. I'm getting json like this through ajax:
{'locale': 'en', 'keywords': ['keyword1', 'keyword2']}
However as I use normalizr with locale as id, for caching results, my initial state is as I describe in the reducer:
function entities(state = { users: {}, repos: {}, keywords: { 'en': { 'keywords': [] } } }, action) {
if (action.response && action.response.entities) {
return merge({}, state, action.response.entities)
}
return state
}
What I don't like is the initial if we have several languages, also remembering to modify it if we add another language, for example fr. In this
keywords: { 'en': { 'keywords': [] } }
should be:
keywords: { 'en': { 'keywords': [] }, 'fr': { 'keywords': [] } }
This line looks problematic:
const { keywords } = this.props.keywords
It's the equivalent of:
var keywords = this.props.keywords.keywords;
I doubt that's what you intended.
Another thing worth checking is keywords[locale] in your mapStateToProps() which will probably initially resolve to undefined. Make sure your component can handle that, or give it a sensible default.

Resources