Stencil: call public method from another component via the DOM - javascript

I have a Stencil sidebar component with a refresh() method. I've exposed it with #Method():
#Method() async refresh() { ... }
and I can call it from my app:
const sidebar = document.querySelector('my-sidebar');
await sidebar.refresh();
However, I also have a popup component which is generated ad-hoc by the app via a separate API, and I want to make a button in the popup trigger the sidebar's refresh(). I've set the method as a Prop on the popup:
#Prop() refresh: Function;
and I've set the Prop in the app code as a reference to the method:
popup.refresh = sidebar.refresh;
...but when I try to execute it I get an error:
Uncaught (in promise) TypeError: ref.$lazyInstance$[memberName] is not a function
How can I get the popup to see the sidebar's method?

If you look at the value of sidebar.refresh in your app when you're trying to assign that Prop, you'll see something like this:
value(...args) {
const ref = getHostRef(this);
return ref.$onInstancePromise$.then(() => ref.$lazyInstance$[memberName](...args));
}
Stencil components are lazy-loaded by generating proxies with references pointing to methods, so you're just passing a reference to a proxy. The easiest way to fix this is to make a function which explicitly calls the method, forcing its hydration. (If the source component is not already hydrated, you'll need to wait for that first by using its lifecycle methods.)
Here's what that would look like in your app:
popup.refresh = () => sidebar.refresh();
// or, passing any arguments:
popup.refresh = args => sidebar.refresh(args);
You can also do it in your popup component:
async popupRefresh() {
let sidebar = document.querySelector('my-sidebar');
await sidebar.refresh();
}
If calling the method from inside another component in this way, you may see the TypeScript error Property 'refresh' does not exist on type 'Element'.
To avoid this, import the sidebar component into your popup component:
import { mySidebar } from 'stencil';
Then, use it as the type:
let sidebar: mySidebar = document.querySelector('my-sidebar');

Related

Test method called in mounted hook Vue class component typescript

I am trying to test that a method is called on mount of Vue component. Fairly new to Vue and Typescript.
export default class App extends Vue {
mounted () {
this.deviceId()
this.ipAddress()
this.channel()
this.show()
this.campaign()
this.adUnit()
}
this approach works but I get a warning:
it('mounted methods are called', async () => {
const deviceId = jest.fn()
wrapper = shallowMount(App, {
methods: {
deviceId
}
})
expect(deviceId).toHaveBeenCalled()
})
The error:
console.error node_modules/#vue/test-utils/dist/vue-test-utils.js:1735
[vue-test-utils]: overwriting methods via the `methods` property is deprecated and will be removed in the next major version. There is no clear migration path for the `methods` property - Vue does not support arbitrarily replacement of methods, nor should VTU. To stub a complex method extract it from the component and test it in isolation. Otherwise, the suggestion is to rethink those tests.
I have tried using jest spyOn, but I cannot find a way to access the method;
const spy = jest.spyOn(App.prototype, 'methodName')
wrapper = shallowMount(App)
expect(spy).toHaveBeenCalled()
Gives the following error:
Cannot spy the deviceId property because it is not a function; undefined given instead
The following also doesn't work:
const spy = jest.spyOn(App.methods, 'methodName')
Error:
Property 'methods' does not exist on type 'VueConstructor<Vue>'.ts(2339)
And the following:
const spy = jest.spyOn(App.prototype.methods, 'deviceId')
Error:
Cannot spyOn on a primitive value; undefined given
I have read in places I may need to define an interface for the component but I am not sure how this works with defining functions inside or if it is necessary?
I've been facing the same issue for a few days, but I've found the way of pointing to the correct method when calling jest.spyOn().
It's a bit tricky but you'll find the methods of your class like this:
const spy = jest.spyOn(App.prototype.constructor.options.methods, 'deviceId');
Note that (even if it might seem obvious, just in case) you'll need to do this before wrapping your component, i.e. like this:
const spy = jest.spyOn(App.prototype.constructor.options.methods, 'deviceId');
wrapper = mount(App, { /* Your options go here */ });
By the way, you don't need to define methods property inside options.
Define your method under the property methods. Only then you can access them from the class.
export default class App extends Vue {
methods: {
deviceId(){
console.log("do your stuff")
}
}
}
See here for more examples for the usage of methods

Can you use state or call other methods inside the getInitialProps() method in Next.js?

I'm trying to do this but it's not working:
static async getInitialProps({ req, res, query }) {
// Call another method in the component
const foo = this.foo();
// Use the component's state here
const foobar = [...foo, ...this.state.bar];
}
This is just pseudo code, in my actual use case I'm trying to build a URL to send an AJAX request. This URL is built by a method in my component which is also called by the client-side rendering. So I want to keep it in the same place and use it for both the initial rendering and the subsequent client-side rendering. But I can't call that method this.buildURL() from inside getInitialProps. I also can't use state in that function, how to go about this or a workaround?

Testing an isolated function of a component in React Jest

I am fairly new to Jest and Enzyme and I stumbled a cross a problem:
I have a Component which renders Children and also calls a method of those children. I achieve that by using refs. I call these functions something like:
somefunction = () => {
this.myReference.current.childFunction();
this.doSomethingOther();
}
I now want to test the function somefunction. I want to check if the doSomethingOther function was called. Using a shallow render I cannot achieve that. The test would succeed if this.myReference.current.childFunction(); wasn't called. Jest cannot know it because it only renders shallow and therefore throws an error.
I may be missing the full understanding. I wonder if someone has a Idea to test this function without using mount.
Take a look at the below code where I shallow render a component and then get the class instance and mock the required functions. Now when we call somefunction, we check if doSomethingOther has been called. (Assumption you are using jest+enzyme)
const wrapper = shallow(<Comp />);
const inst = wrapper.instance();
inst.myReference = {
current: {
childFunction: jest.fn()
}
}
inst.doSomethingOther = jest.fn();
inst.somefunction();
expect(inst.doSomethingOther).toHaveBeenCalled();

React - Call setState in parent when child is called

I am building a blog in react where I get the data from a JSON-API and render it dynamically. I call setState in my app.js to get the data from JSON via axios and route with match.params to the post pages(childs). This works fine, BUT if I call the child (URL: ../blog/post1) in a seperate window it doesnt get rendered. It works if I hardcode the state.
So, I see the issue, what would be the best way to fix it?
Parent:
class App extends Component {
constructor(props) {
super();
this.state = {
posts: []
}
getPosts() {
axios.get('APIURL')
.then(res => {
let data = res.data.posts;
this.setState({
posts: data
})
})
}
componentDidMount = () => this.getPosts()
Child:
UPDATE - Found the error:
The component threw an error, because the props were empty. This was because the axios getData took some time.
Solution: I added a ternary operator to check if axios is done and displayed a css-spinner while loading. So no error in component.
Dynamic Routing works like a charme.
You can do this with binding. You would need to write a function like
setPosts(posts) {
this.setState({posts});
}
in the parent, and then in the parent, bind it in the constructor, like so.
this.setPosts = this.setPosts.bind(this);
When you do this, you're attaching setPosts to the scope of the parent, so that all mentions of this in the function refer to the parent's this instead of the child's this. Then all you need to do is pass the function down to the child in the render
<Child setPosts={this.setPosts} />
and access that method in the child with
this.props.setPosts( /*post array goes here */ );
This can apply to your getPosts method as well, binding it to the parent class and passing it down to the Child.
When you hit the /blog/post1 url, it renders only that page. The App component is not loaded. If you navigate from the App page, all the loading has been done and you have the post data in the state of the child component.
Please refer to this SO question. This should answer your question.

Bind the vue to root element after plugin finishes ajax call

I am binding my application root element #app to vue and before that I am loading my custom plugin with Vue.use(myplugin). My plugin makes an ajax call, load the data and set it into Vue.$permission property.. so in short I want to load my user permission before mounting the app. but while my ajax call is fetching permission data, app is mounted and my page is getting rendered, which need the permission object.
is there a way I can bind the app root element to vue after my plugin finishes.. or any other alternate solution?
Yeah, that's quite simple actually:
const Vue = require('vue');
const vueInstance = new Vue({
// don't specify the 'el' prop there
});
doingAnAjaxCall() // assuming it returns a promise
.then(() => {
vueInstance.$mount('#root'); // only now the vue instance is mounted
});
If a Vue instance didn’t receive the el option at instantiation, it will be in “unmounted” state, without an associated DOM element. vm.$mount() can be used to manually start the mounting of an unmounted Vue instance.
See: https://v2.vuejs.org/v2/api/#vm-mount
So for your case you may use any asynchronous mechanism to detect the end of the ajax call. Maybe the simplest solution is to pass a callback function to your plugin object, and mount your vue instance inside.
/* Define plugin */
MyPlugin = {};
MyPlugin.install = function(Vue, options) {
doingAnAjaxCall()
.then(data => {
// do something with data
options.callback();
});
};
const Vue = require('vue');
/* Create the vue instance */
const vueInstance = new Vue({
// don't specify the 'el' prop there
});
/* Install the plugin */
Vue.use(MyPlugin, { // This object will be passed as options to the plugin install()
callback: () => {
vueInstance.$mount('#root'); // only now the vue instance is mounted
}
});

Categories

Resources