I am trying to convert the following code form options to composition API in Vue using Typescript.
data() {
return {
items: [
'Car',
'Truck',
'Bike',
'Caravan'
],
activeItem: 0,
show: false,
};
},
methods: {
showDropdown() {
this.show = !this.show;
},
setActiveItem(item) {
this.activeItem = this.items.indexOf(item);
this.show = false;
}
},
computed: {
dropdownItems() {
return this.items.filter((item,i) => i !== this.activeItem)
}
}
This is what I have. I am still new to Vue and Typescript so i am using this example to learn more about composition API and Typescript.
setup() {
const activeItem = 0;
const show = ref(false);
const items = ref([
'Car',
'Truck',
'Bike',
'Caravan'
]);
function showDropdown(this: any) {
this.show = !this.show;
}
function setActiveItem(this: any, item: string) {
this.activeItem = this.items.indexOf(item);
this.show = false;
}
const dropdownItems = computed(function (this: any, item: string) {
return this.items.filter((item, i) => i !== this.activeItem);
});
return {
activeItem,
show,
items,
showDropdown,
setActiveItem,
dropdownItems,
};
},
The errors I am getting are for example in the setActiveItem method is 'this' implicitly has type 'any' because it does not have a type annotation. So when I pass this: any params it works but I don't know if this is the right way to do it?
Second problem is I can't get the computed method to work I don't know how to implement it correctly. Is there someone who can help me out with this?
The objective of composition API is to get rid of dynamic this that isn't an instance of a specific class but an object that aggregates properties from component definition and puts limitations on TypeScript typing.
Instead, variables defined in the scope of setup are supposed to be accessed directly:
const dropdownItems = computed((item: string) => {
return unref(items).filter((item, i) => i !== unref(activeItem));
});
activeItem is supposed to be a ref as well.
Computed method you just import import { ref, computed } from "#vue/reactivity";. With ref you need to use value. (Following code is without typescript)
import { ref, computed } from "#vue/reactivity";
setup() {
const activeItem = ref(0);
const show = ref(false);
const items = ref([
'Car',
'Truck',
'Bike',
'Caravan'
]);
const dropdownItems = computed(() => items.value.filter((item, i) => i !== activeItem.value));
const showDropdown = () => {
show.value = !show.value;
}
const setActiveItem = (item) => {
activeItem.value = items.value.indexOf(item);
show.value = false;
}
return {
activeItem,
show,
items,
showDropdown,
setActiveItem,
dropdownItems,
};
},
Related
i have this code inside a class, how could i keep the idea of it but updating to use in a function component? I'm trying to change but I can't keep the current proposal
validate = value => {
const {
formApi: { getValue },
name,
} = this.props;
const component = (props) => {
const validate = value => {
const {
formApi: { getValue },
name,
} = props
}
}
const Component = ({formApi: {getValue}, name})=> {
const validate = useCallback((value)=> {
}, [getValue, name]);
}
I am trying to solve a problem in my vuex store. I write two different actions in my store. One action is reactive and the other not. But I need the loadSlidesEach() in reactivity, so the data are updated. I cant find the mistake.
My store:
const store = new Vuex.Store({
state: {
loadedSlides: []
},
mutations: {
setSlides(state, slides) {
state.loadedSlides = slides
},
setSlidesPush(state, slide) {
state.loadedSlides.push(slide)
}
},
getters: {
loadedSlides(state) {
return state.loadedSlides
}
},
actions: {
loadSlides({ commit, getters, state }) {
firebase.database().ref('/slides/').on('value', snapshot => {
const slidesArray = []
const obj = snapshot.val()
for (const key in obj) {
slidesArray.push({ ...obj[key], id: key })
}
commit('setSlides', slidesArray)
})
},
loadSlidesEach({ commit, getters, state }, id) {
firebase.database().ref('/slides/' + id).on('value', snapshot => {
const slide = snapshot.val()
commit('setSlidesPush', { ...slide })
})
}
}
})
My component 1: Array from slides() is reactive
export default {
computed: {
slides() {
return this.$store.getters.loadedSlides
}
},
created() {
this.$store.dispatch('loadSlides')
}
}
My component 2: Array from slides() is not reactive
export default {
computed: {
slides() {
return this.$store.getters.loadedSlides
}
},
created() {
const slides = ['1','2','3']
for (let i = 0; i < slides.length; i++) {
this.$store.dispatch('loadSlidesEach', slides[i])
}
}
}
I think the problem is something with the inital state or the mutation with push(). Any advices?
Update:
The two actions are only different in the mutations. So what is the best way to set the state in vuex? The store get confused if I call the action loadSlidesEach() in a loop.
Don't use await and then together. Use one or another:
loadSlidesEach: async({ commit, getters }, id) => {
const data = await firebase.database().ref('/slides/' + id).once('value')
const slide = data.val()
commit('setSlidesPush', { ...slide })
}
do you try to use mapState from vuex ?:
import {mapState} from 'vuex'
export default {
computed: {
...mapState(['loadedSlides '])
}
}
now you can use loadedSlides in component.
I found a working solution for my problem.
Change the mutation:
setSlidesPush(state, addedSlide) {
const slideIndex = state.loadedSlides.findIndex(slide => slide.id === addedSlide.id)
if (slideIndex !== -1) {
Vue.set(state.loadedSlides, slideIndex, addedSlide)
} else {
state.loadedSlides.push(addedSlide)
}
}
I was creating a useReduxState hook, which made use of Redux's useSelector and lodash's pick module. I've simplified my approach for the sake of brevity for this question. Here is what I have:
interface IReducerA {
loading: boolean
}
interface IReducerB {
open: boolean
}
type TGetReducerState = IReducerA & IReducerB;
interface IState {
reducerA: IReducerA;
reducerB: IReducerB;
}
const state = {
reducerA: { loading: false; },
reducerB: { open: true; }
}
const getState = (reducer: keyof IState, values: Array<keyof TGetReducerState>): TGetReducerState => pick(state, values);
const { loading } = getState('reducerA', ['loading'])
I've managed to get this working (kind of). The last line,
const { loading } = getState('reducerA', ['loading'])
correctly suggested 'loading' as one of the entries to values: Array<keyof TGetReducerState> and it correctly suggests 'loading' as one the return values.
The problem here, is that Typescript would not complain about the following line:
const { loading } = getState('reducerA', ['open'])
There are two problems with this line. The first is that the property 'open' does not exist in reducerA and the second problem is that if I request 'open', then I shouldn't be able to destructure const { loading } = . . ..
How can I properly type this?
Here is my approach.
Edit
Writing here, because it's too long for the comment section.
This approach doesn't work
UPDATE. Full version
function pick<T extends TGetReducerState, V extends ReadonlyArray<keyof T>>(object: T, keys: V):Pick<T, V[number]> {
return keys.reduce((obj, key) => {
if (object && Object.prototype.hasOwnProperty.call(object, key)) {
// I made here small improvement, because I prefer to not mutate accumulator argument in reduce function
return {
...obj,
[key]: object[key]
}
}
return obj;
}, {} as Pick<T, V[number]>);
}
interface IReducerA {
loading: boolean;
age: number;
}
interface IReducerB {
open: boolean
}
type TGetReducerState = IReducerA | IReducerB;
interface IState {
reducerA: IReducerA;
reducerB: IReducerB;
}
const state: IState = {
reducerA: { loading: false, age: 0 },
reducerB: { open: true },
}
const getState = <T extends keyof IState, V extends keyof IState[T]>(reducer: T, values: V[]) => pick(state[reducer], values)
const { loading, age } = getState('reducerA', ['loading', 'age'])
const { open } = getState('reducerA', ['loading', 'age']) // error
I have simple file called _mixin.js which consists of:
const mutations = {
_set(state, data) {
for (let key in data) {
if (key in state) {
state[key] = data[key];
}
}
},
_reset(state) {
const s = initialState();
Object.keys(s).forEach(key => {
state[key] = s[key];
});
}
};
export default {
mutations
};
What I'm trying to do is share this two methods between all existing modules mutation like this:
import _MIXINS from 'store/modules/_mixins';
function initialState() {
return {
id: null,
email: null,
password: null,
name: null,
};
}
const state = initialState();
const mutations = {
..._MIXINS.mutations,
setId(state, id) {
state.id = id;
}
};
The problem is that browser says it cant find function initialState as its not in same file.
Just do like this:
// sharedLogic.js
export function initialState () {
// your logic
}
// store module
import { initialState } from '<pathTo sharedLogic.js>'
const state = initialState();
// mixin module
import { initialState } from '<pathTo sharedLogic.js>'
const mutations = {
...
_reset(state) {
const s = initialState();
Object.keys(s).forEach(key => {
state[key] = s[key];
});
}
};
I have implemented a component (for a typing training app) which tracks key presses on global scope like this:
class TrainerApp extends React.Component {
constructor() {
// ...
this.handlePress = this.handlePress.bind(this);
}
handlePress(event) {
const pressedKey = event.key;
const task = this.state.task;
const expectedKey = task.line[task.position];
const pressedCorrectly = pressedKey == expectedKey;
this.setState(prevState => {
const newPosition = prevState.task.position +
(pressedCorrectly ? 1 : 0);
return {
// ...prevState, not needed: https://reactjs.org/docs/state-and-lifecycle.html#state-updates-are-merged
task: {
...prevState.task,
position: newPosition,
mistakeAtCurrentPosition: !pressedCorrectly
}
}
})
}
componentDidMount() {
document.addEventListener(this.keyEventTypeToHandle,this.handlePress);
}
componentWillUnmount () {
document.removeEventListener(this.keyEventTypeToHandle,this.handlePress);
}
...
}
and I'd like to write some unit-tests using Jest. My initial idea was:
describe('TrainerApp.handlePress should',() => {
test('move to the next char on correct press',() => {
const app = new TrainerApp();
app.state.task.line = 'abc';
app.state.task.position = 0;
const fakeEvent = { key: 'a' };
app.handlePress(fakeEvent);
expect(app.state.task.position).toBe(1);
});
...
});
but the problem is app.handlePress relies on usage of this.setState which is not defined when the component is not mounted yet. Of'course I can modify the app like this:
test('move to the next char on correct press',() => {
const app = new TrainerApp();
app.setState = jest.fn(function(handler) {
this.state = handler(this.state);
});
app.state.task.line = 'abc';
app.state.task.position = 0;
const fakeEvent = { key: 'a' };
app.handlePress(fakeEvent);
expect(app.state.task.position).toBe(1);
});
or even like this:
class ExplorableTrainerApp extends TrainerApp {
setState(handler) {
this.state = handler(this.state);
}
}
test('move to the next char on correct press',() => {
const app = new ExplorableTrainerApp();
app.state.task.line = 'abc';
app.state.task.position = 0;
const fakeEvent = { key: 'a' };
app.handlePress(fakeEvent);
expect(app.state.task.position).toBe(1);
});
but this seems a very fragile approach (here I rely on the fact that .setState is called with the function argument while it can be called with just newState argument and hence I'm testing implementation details, instead of just the behaviour. Is there a more robust way to test this?
There are a few frameworks for testing React components, Enzyme and react-testing-library are both popular and well supported.