import a function to use the scope it is called from - meteor

Using Meteor, I am trying to use a function from one file on a template's scope lays in a different file. I tried using an arrow function:
first file:
export const myFunc = ()=>{
console.log(this.x);
};
second file:
import {myFunc} from './myFunc.js';
Template.MyTemplate.onCreated(function(){
this.x = 4;
myFunc(); //undefined
});
what is the best way of affecting template's variables using functions that aren't defined at the template itself? (I need those functions for some other templates as well)

What about passing your template's variables as parameters to the function?
Combining that with the use of ReactiveVar you can set them in myFunc
import { myFund } from './myFunc.js'
Template.MyTemplate.onCreated(function () {
this.myVar = new ReactiveVar('Foo');
});
Template.myTemplate.onRendered(function() {
myFunc(this.myVar);
console.log(this.myVar.get()) // 'Bar'
});
and in your function file
export const myFunc = (myVar) => {
//Use myVar
myVar.set('Bar')
};

Related

How to update Chakra UI theme.js file with Redux store value?

What do I want to do?
I want to change Chakra UI global theme.js file with redux store value which can be changed from the front end. Like from the front end a button to change all H1 tag colors to brand colors.
theme.js file
import { extendTheme } from "#chakra-ui/react";
import { store } from "./redux/store";
const getBgColor = (state) => {
return state.theme.bgColor;
};
let test = "red";
const Helper = () => {
const state = store.getState();
const color = getBgColor(state);
console.log("color", color);
test = color;
console.log(test);
};
store.subscribe(Helper);
console.log("test", test);
export const theme = extendTheme({
colors: {
brand: {
100: `${test}`
}
}
});
Currently, this is taking value from the redux store for the first time but not changing as per store value change. Inside the Helper function, it is listening changes but theme.js file is not getting updated.
You can check this code sandbox I am testing on...
CodeSandBox link: https://codesandbox.io/s/upbeat-khayyam-rietmr?file=/src/theme.js
Thanks in advance!

How to test computed value inside setup function in Vue.js 3 with vue-test-utils & Jest

I am getting "TypeError: Cannot add property myData, object is not extensible" on setData
Hello.vue
<template>
<div v-if="isEditable" id="myEditDiv">
<button type="button"> Edit </button>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, reactive} from "vue"
export default defineComponent({
setup() {
const myObject = {myName:"", myNumber:""}
let myData = reactive({myObject})
const isEditable = computed(() => {
return myData.myObject.myName.startsWith('DNU') ? false : true
})
return {
isEditable
}
}
})
</script>
Hello.spec.ts
import { shallowMount } from '#vue/test-utils'
import Hello from '#/components/Hello.vue'
import { reactive } from 'vue'
describe('Hello.vue Test', () => {
it('is isEditable returns FALSE if NAME starts with DNU', async () => {
const myObject = {myName:"DNU Bad Name", myNumber:"12345"}
let myData = reactive({myObject})
const wrapper = shallowMount(Hello)
await wrapper.setData({'myData' : myData})
expect(wrapper.vm.isEditable).toBe(false)
})
})
I also tried to see if that DIV is visible by:
expect(wrapper.find('#myEditDiv').exists()).toBe(false)
still same error. I might be completely off the path, so any help would be appreciated.
Update
This is possible several different ways. There's two issues that need to be addressed.
The variable has to be made available. You can use vue's expose function in setup (but getting the value is really messy: wrapper.__app._container._vnode.component.subTree.component.exposed😱) or just include it in the return object (accessible through wrapper.vm).
change how you mutate the data in the test.
your test has
const myObject = {myName:"DNU Bad Name", myNumber:"12345"}
let myData = reactive({myObject})
const wrapper = shallowMount(Hello)
await wrapper.setData({'myData' : myData})
even if setData was able to override the internal, it would not work.
the problem is that the setup function has this
let myData = reactive({ myObject });
const isEditable = computed(() => {
return myData.myObject.myName.startsWith("DNU") ? false : true;
});
where editable is using a computed generated from that instance of myData. If you override myData with a separate reactive, the computed will still continue to use the old one. You need to replace the contents of the reactive and not the reactive itself
To update the entire content of the reactive, you can use:
Object.assign(myReactive, myNewData)
you can make that a method in your component, or just run that from the test. If you update any value within the reactive (like myData.myObject) you can skip the Object.asign
Here are several versions of how you can test it.
Component:
<template>
<div v-if="isEditable" id="myEditDiv">
<button type="button">Edit</button>
</div>
</template>
<script>
import { computed, defineComponent, reactive } from "vue";
export default defineComponent({
setup(_, { expose }) {
const myObject = { myName: "", myNumber: "" };
let myData = reactive({ myObject });
const isEditable = computed(() => {
return myData.myObject.myName.startsWith("DNU") ? false : true;
});
const updateMyData = (data) => Object.assign(myData, data);
expose({ updateMyData });
return {
isEditable,
updateMyData,
myData
};
},
});
</script>
the test
import { shallowMount } from "#vue/test-utils";
import MyComponent from "#/components/MyComponent.vue";
const data = { myObject: { myName: "DNU Bad Name" } };
describe("MyComponent.vue", () => {
it.only("sanity test", async () => {
const wrapper = shallowMount(MyComponent);
expect(wrapper.vm.isEditable).toBe(true);
});
it.only("myData", async () => {
const wrapper = shallowMount(MyComponent);
Object.assign(wrapper.vm.myData, data);
expect(wrapper.vm.isEditable).toBe(false);
});
it.only("myData", async () => {
const wrapper = shallowMount(MyComponent);
wrapper.vm.myData.myObject = data.myObject;
expect(wrapper.vm.isEditable).toBe(false);
});
it.only("updateMyData method via return", async () => {
const wrapper = shallowMount(MyComponent);
wrapper.vm.updateMyData(data);
expect(wrapper.vm.isEditable).toBe(false);
});
it.only("updateMyData method via expose🙄", async () => {
const wrapper = shallowMount(MyComponent);
wrapper.__app._container._vnode.component.subTree.component.exposed.updateMyData(
data
);
expect(wrapper.vm.isEditable).toBe(false);
});
});
It is not possible through setData
from the docs:
setData
Updates component internal data.
Signature:
setData(data: Record<string, any>): Promise<void>
Details:
setData does not allow setting new properties that are not defined in the component.
Also, notice that setData does not modify composition API setup() data.
It seems that updating internals with composition API is incompatible with setData. See the method name setData, refers to this.data and was likely kept in the vue test utils mostly for backwards compatibility.
I suspect the theory is that it's bad practice anyway to test, what would be considered, an implementation detail and the component test should focus on validating inputs an outputs only. Fundamentally though, this is a technical issue, because the setup function doesn't expose the refs and reactives created in the setup.
There is a MUCH easier way to do this.....
Put your composables in a separate file
Test the composables stand alone.
Here is the vue file:
<template>
<div>
<div>value: {{ counter }}</div>
<div>isEven: {{ isEven }}</div>
<button type="button" #click="increment">Increment</button>
</div>
</template>
<script setup lang='ts'>
import {sampleComposable} from "./sample.composable";
const {isEven, counter, increment} = sampleComposable();
</script>
Here is the composable:
import {computed, ref} from 'vue';
export function sampleComputed() {
const counter = ref(0);
function increment() {
counter.value++;
}
const isEven = computed(() => counter.value % 2 === 0);
return {counter, increment, isEven};
}
Here is the test:
import {sampleComposable} from "./sample.composable";
describe('sample', () => {
it('simple', () => {
const computed = sampleComposable();
expect(computed.counter.value).toEqual(0);
expect(computed.isEven.value).toEqual(true);
computed.increment();
expect(computed.counter.value).toEqual(1);
expect(computed.isEven.value).toEqual(false);
computed.increment();
expect(computed.counter.value).toEqual(2);
expect(computed.isEven.value).toEqual(true);
})
});
This just 'works'. You don't have to deal w/ mounting components or any other stuff, you are JUST TESTING JAVASCRIPT. It's faster and much cleaner. It seems silly to test the template anyway.
One way to make this easier to test is to put all of your dependencies as arguments to the function. For instance, pass in the props so it's easy to just put in dummy values as need. Same for emits.
You can tests watches as well. You just need to flush the promise after setting the value that is being watched:
composable.someWatchedThing.value = 6.5;
await flushPromises();
Here is my flushPromises (which I found here):
export function flushPromises() {
return new Promise(process.nextTick);
}

Vue 3 Composition API: Array.length Not Reactive

I'm struggling to figure out how to make properties of an array reactive. Is this possible? In the example below, the filteredResults array itself is reactive, and working, but neither the resultCountRef (wrapped in reactive()) nor the resultCount fields are reactive. In otherwords, if I click the Filter for Apples button, the filteredResults changes to just the one item, but the two count fields remain at 3. Note that using {{filteredResults.length}} in the template does work as expected. Here is a working sample.
And here is the code (a Search.vue composition API component, and a useFilter composition function):
Search.vue:
<template>
<div>resultCountRef: {{resultCountRef}}</div>
<div>resultCount: {{resultCount}}</div>
<div>filteredResults.length: {{filteredResults.length}}</div>
<div>filteredResults: {{filteredResults}}</div>
<div>filters: {{filters}}</div>
<div><button #click="search()">Search</button></div>
<div><button #click="applyFilter('apples')">Filter for Apples</button></div>
</template>
<script>
import { reactive } from 'vue';
import useFilters from './useFilters.js';
export default {
setup(props) {
const products = reactive(['apples', 'oranges', 'grapes']);
const { filters, applyFilter, filteredResults } = useFilters(products);
const resultCountRef = reactive(filteredResults.value.length);
const resultCount = filteredResults.value.length;
return {
resultCountRef,
resultCount,
filteredResults,
filters,
applyFilter,
};
},
};
</script>
useFilters.js:
import { ref, onMounted } from 'vue';
function filterResults(products, filters) {
return products.filter((product) => filters.every(
(filter) => {
return product.includes(filter);
},
));
}
export default function useFilters(products) {
const filters = ref([]);
const filteredResults = ref([...products]);
const applyFilter = (filter) => {
filters.value.push(filter);
filteredResults.value.splice(0);
filteredResults.value.splice(0, 0, ...filterResults(products, filters.value));
};
return { filters, applyFilter, filteredResults };
}
UPDATED
const resultCountRef = reactive(filteredResults.value.length);
The reason why reactive is not working is because filteredResults.value.length returned a simple number and not referencing to anything.
From #JimCopper 's comment:
The returned object is simply providing an observable object that wraps the object/value/array passed in (in my case the filteredResults.length value). That returned object resultCountRef is the object that is tracked and can be then modified, but that's useless in my case and it's why reactive didn't work. )
As the resultCount depends on filteredResults, you can use computed to monitor the change
Here is the modified playground
setup(props) {
const products = reactive(['apples', 'oranges', 'grapes']);
const { filters, applyFilter, filteredResults } = useFilters(products);
// const resultCountRef = reactive(filteredResults.length);
const resultCount = computed(() => filteredResults.length) // use computed to react to filteredResults changes
return {
// resultCountRef,
resultCount,
filteredResults,
filters,
applyFilter,
};
},
The new doc does not have a very nice explanation on computed so i just quote it from the old doc explanation
A computed property is used to declaratively describe a value that depends on other values. When you data-bind to a computed property inside the template, Vue knows when to update the DOM when any of the values depended upon by the computed property has changed.

onMounted hook is not called by Vue

I have two components, one is managing data, the other is a vue template. I have simplified them here. The problem is that when the locations come in via the fetch, the locations in the vue template stays empty. I've checked with isRef() and that returns true, but it's just an empty array. Looking in the Vue dev tools panel, the locations does have elements in the array.
Locations.js
import {
ref,
isRef,
onMounted,
} from 'vue';
export default function useLocations () {
const locations = ref([]);
const loadImageData = (locId) => {
isRef(locations); // === true
// #FIXME locations.value is always empty here.
locations.value.forEach( (loc,key) => {
console.debug( loc.id, locId )
})
};
const getLocations = async () => {
const locs = await apiFetch({ path: '/wp/v2/tour-location'});
locations.value = locs;
};
onMounted( getLocations );
return {
locations,
getLocations,
loadImageData,
};
}
App.vue
<template>
<div class="location">
<h1>{{ location.name }}</h1>
<img :src="location.main_image" />
</div>
</template>
<script>
import useLocations from '#/composables/Locations';
export default {
name: 'Location',
props: [,'location'],
data () {return {}},
watch: {
location: {
// deep: true,
immediate: true,
handler: function(){
const { loadImageData } = useLocations();
loadImageData( location.id );
}
}
},
}
</script>
When loadImageData() is called from the Location.vue component, locations is always an empty array. Why doesn't it get updated in that function as it does in other places within the app?
onMounted is a hook registration function.
These lifecycle hook registration functions can only be used synchronously during setup(), since they rely on internal global state to locate the current active instance (the component instance whose setup() is being called right now). Calling them without a current active instance will result in an error.
[emphasis mine]
Docs
As you are using your useLocations composition function outside setup(), your getLocations function is never called and locations is always empty array
To explain it further. You do not have to call onMounted (or any other hook registration function) directly inside setup(). It is perfectly fine to place that call into separate composition function outside any component (as you did) but that function must then be used from inside the setup()

mapStateToProps and mapDispatchToProps: getting IDE to "see" the props

Problem: IDE does not resolve props passed to the component via connect()
Note: this is not a bug, but an inconvenience to the coder
Say I have this React component connected to Redux via connect():
class SomeComponent extends Component {
render() {
return (
{this.props.someObject ? this.props.someObject : ''}
);
}
}
function mapStateToProps(state) {
return {
someObject: new SomeObject(state.someReducer.someObjectInfo),
};
}
function mapDispatchToProps(dispatch) {
return {
// ...
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatsList);
I'm using the IntelliJ IDE, and any prop connected to the component in the above manner, such as someObject, will get an unresolved variable warning. And if someObject has some properties/methods, they will neither be resolved nor show up in code suggestions (which are really helpful).
A workaround
Pass state and dispatch themselves as props:
function mapStateToProps(state) {return {state};}
function mapDispatchToProps(dispatch) {return {dispatch};}
Define my variables in the constructor (as opposed to via props):
constructor(props) {
this.someVar = props.state.someReducer.someVar;
this.someObj = new SomeObject(props.state.someReducer.someObjectInfo;
}
Update the variables manually whenever props change:
componentWillReceiveProps(nextProps) {
someObject.update(nextProps.state.someReducer.someObjectInfo);
}
The drawback is having additional boilerplate logic in componentWillReceiveProps, but now the IDE happily resolves the variables and code suggestion works.
Question
Is the workaround preferable? I'm using it, like it so far, and have not observed any other drawbacks thus far. Is there a better way to get the IDE to understand the code?
Motivation (verbose; only for those interested in why I want to accomplish the above)
The Redux tutorials show a simple way to connect state/dispatch to props, e.g.:
function mapStateToProps(state) {
users: state.usersReducer.users
chats: state.chatsReducer.chats
}
function mapDispatchToProps(dispatch) {
addUser: (id) => dispatch(usersActions.addUser(id))
addChatMsg: (id, msg) => dispatch(chatsActions.addChatMsg(id, msg)
}
In the example above, the coder of a component will need to know every relevant reducers' names and their state variables. This can get messy for the coder. Instead, I want to abstract these details away from the component. One way is with a "module" class that accepts state and dispatch, and provides all get/set methods:
class Chats {
// Actions
static ADD_MESSAGE = "CHATS/ADD_MESSAGE";
constructor(globalState, dispatch) {
this.chatsState = globalState.chats;
this.dispatch = dispatch;
}
// Get method
getChats() {
return this.chatsState.chats;
}
// Set method
addChatMessage(id, msg) {
return this.dispatch({
type: Chats.ADD_MESSAGE,
id,
msg
};
}
// Called by componentWillReceiveProps to update this object
updateChats(nextGlobalState) {
this.chatsState = nextGlobalState.chats;
}
}
Now, if a Component requires the Chats module, a coder simply does this:
class SomeComponent extends Component {
constructor(props) {
this.chats = new Chats(props.state, props.dispatch);
}
componentWillReceiveProps(nextProps) {
this.chats.updateChats(nextProps);
}
// ...
}
And now, all Chats get/set methods and properties will be available, and will be picked up by the IDE.
I think newest Idea can now understand component properties defined via propTypes and provides code completion for them. So you just declare propTypes. And it is not even a workaround, it's a good practice in my opinion.
class ChatsList extends Component {
static propTypes = {
someObject: PropTypes.shape({
color: PropTypes.string,
someFunc: PropTypes.func
}),
someDispatcher: PropTypes.func
};
render() {
return (
{this.props.someObject ? this.props.someObject : ''}
);
}
}
function mapStateToProps(state) {
return {
someObject: new SomeObject(state.someReducer.someObjectInfo),
};
}
function mapDispatchToProps(dispatch) {
return {
someDispatcher: Actions.someDispatcher
// ...
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatsList);
Also, passing the entire state is a bad idea, since a component will receive props and get re-renderend if anything changes in the entire state (unless you provide shouldComponentUpdate)

Resources