Use custom JavaScript code in a Vue.js app - javascript

I'm trying to insert JavaScript code in a Vue.js router app. I need to load data from the CMS the app is served from. In order to get the data from the CMS I have to use a JavaScript library from the CMS which is not made for Vue and is not exporting it's class/functions like modern JS. So I import the JS library from in the index.html by a script tag. This works as intended.
But now I have to use the class from this CMS JavaScript library.
Before writing this as a Vue-Router app I just have used Vue for templating purposes.
So I had some code packed in the window.onload event handler.
I have to create an instance for the CMS data access class.
But this leads to a build error (using vue-cli build). Since there
are no understandable error messages from the build process
I have to use trial and error. Even simple variable assignments like var a = 1 seem not to be allowed.
A console.log('something') works. But nothing else seemes to be allowed (except defining the onload-event handler)
I have added this code in a <script> Tag inside App.vue (which was created by vue-cli create)
window.onload = function() {
try {
// Instantiate class obj for CMS data access
cmsDataAccessObj = new CMSAccessData();
waitForPlayerData = true;
}
catch (e) {
console.error(e);
}
}
UPDATE
After testing the different solutions from the answers I got aware that using non-instance variables seems to cause the build errors.
This gives an error:
waitForPlayerData = true;
This works:
this.waitForPlayerData = true;

I wouldn't recommend using window.load to run your code. There are more native approaches to do this in Vue.js.
What you should do in the case you want to run it in the main component of the app before it's been loaded is to put the code inside the beforeCreate lifecycle hook of the main component.
...
beforeCreate () {
this.cmsDataLoader()
},
methods: {
cmsDataLoader () {
try {
// Instantiate class obj for CMS data access
cmsDataAccessObj = new CMSAccessData();
waitForPlayerData = true;
}
catch (e) {
console.error(e);
}
}
}
...
This will run the code everytime a component is created before the creation. You could also use the created lifecycle hook if you want to run it after the creation of the component.
Check the following link for more information about lifecycle hooks.

The best way to place JavaScript in Vue.js App is mounted function, it is called when the component is loaded:
export default {
name: "component_name",
mounted() {
let array = document.querySelectorAll('.list_item');
},
}

You don't need window.onload, you can just put whatever you want there. I'm not entirely certain when precisely in the lifecycle it renders and maybe someone can hop in and let us know but it for sure renders when page starts. (though it makes sense that it does before the lifecycle hooks even start and that it'll solve your needs)
Better & easier solution if you want to load it before Vue loads is to add it to the main.js file. You have full control there and you can load it before Vue initializes.
No need for window.onload there either, just put it before or import a JS file before you initialize Vue, because it's going to be initialized by order.

Related

Vue3 call a components method from another component

I'm trying to have a component call a function from another component but just from
inside the setup.
I've read this from vuemastery https://www.vuemastery.com/blog/understanding-vue-3-expose/
and I see that you can accomplish this like this:
methods: {
reset () {
this.$refs.counter.reset()
},
terminate () {
this.$refs.counter.terminate()
}
}
however, I don't have access to those methods inside the setup, and I also can't use this.$refs inside the setup. Is there a way I can do the same thing inside setup, or a way to access these methods inside the setup?
Those methods are undefined in setup, and I cannot access data setup from within those functions, and I cannot use $refs in setup.
The $refs is a very easy way to call a function from another component - but I can't seem to find a relatively easy way to do this with vue3 composition api - am I missing something?
Hey I figured it out https://vuejs.org/guide/essentials/template-refs.html#accessing-the-refs
If I go
<script>
export default {
setup(props) {
const counter = ref(null);
//now I have access to counter.value.reset after it's mounted
counter.value.reset(); //does not work! it's null here
onMounted(() => {
counter.value.reset(); //here it works
})
//make sure to return counter in return
looks like there is an easy way to do this in setup just has a caveat
you cant just call it right away because setup happens before mounted https://vuejs.org/guide/essentials/lifecycle.html
I am calling it on a trigger - so if someone selects something from a dropdown I am watching the v-model and am able to call it there no problem.
Thanks for all the input.
Putting this answer here in case anyone else needs to accomplish the same thing

How to create and call selectors from a separate file in Cypress?

I am new to Cypress and struggling to get it work. I need to create a file with selectors (I suppose in 'support' folder) to use them in my project file.
Here is an example
describe('Test_spec_1', () => {
it.only('Visits the site & verifies elements', () => {
cy.get('[type=text]').should('be.visible')
cy.get('[type=password]').should('be.visible')
cy.get('[type=submit]').should('be.visible')
cy.get('[routerlink="/login"]').should('be.visible')
cy.get('[routerlink="/reset-password"]').should('be.visible')
cy.get('[routerlink="/support"]').should('be.visible')
cy.get('[routerlink="/reset-password"]').should('be.visible')
})
})
Basically, I need to have all selectors in a separate file, so I can call them and update their values easily.
I have experimented a bit with export/import, but it did not work. Could't find anywhere how to use it properly. It would be great if you can give me some hints how to do it. Thank you very much.
You can create a folder called page-objects inside your integration folder. Inside that you can create one js file for each screen like login.js. Now inside that you can write your locators like:
class login {
usernameInput() {
return cy.get('[type=text]')
}
passwordInput() {
return cy.get('[type=password]')
}
submitButton() {
return cy.get('[type=submit]')
}
}
export default login
Inside your tests you can use them as:
import login from '/page-objects/login.js'
const loginPage = new login();
describe('Test_spec_1', function() {
it('Visits the site & verifies elements', function() {
loginPage.usernameInput().should('be.visible')
loginPage.passwordInput().should('be.visible')
loginPage.submitButton().should('be.visible')
})
})
Please don't use page objects in Cypress, see this tutorial Stop using Page Objects
Page objects problems
Page objects are hard to maintain and take away time from actual application development. I have never seen PageObjects documented well enough to actually help one write tests.
Page objects introduce additional state into the tests, which is separate from the application’s internal state. This makes understanding the tests and failures harder.
Page objects try to fit multiple cases into a uniform interface, falling back to conditional logic - a huge anti-pattern in our opinion.
Page objects make tests slow because they force the tests to always go through the application user interface.
Number 3) killed it for me. You get to a point where you try to figure out more convoluted methods in the page object to cater for different scenarios.
The easiest way to store your selector text in one place is given in this question Where to store selectors in Cypress.io
// cypress/support/selectors.js
export default {
mySelector: '.my-selector',
mySelector2: '.my-selector-2'
};
// cypress/integration/one.spec.js
import selectors from '../support/selectors.js';
describe('test', () => {
it('test', () => {
cy.get(selectors.mySelector);
});
});

Write Vue plugin with custom options

I'm trying to write a vue plugin with custom options. I followed the official vue guidelines (https://v2.vuejs.org/v2/guide/plugins.html) on doing so but can't find a way to define custom options. These options should be read by normal javascript which then exports an object that is used by a vue component.
My folder structure is like this:
/src
factory.js
CustomComponent.vue
factory.js
import Vue from "vue";
import ImgixClient from "imgix-core-js";
var imgixClient = new ImgixClient({
domain: CUSTOM_OPTION_URL <-- important bit
domain: Vue.prototype.$imgixBaseUrl //tried it like this
});
export { imgixClient };
I already tried to set this custom bit by utilizing Vue.prototype in the install method like this but can't seem to get it working
export function install(Vue, options) {
if (install.installed) return;
install.installed = true;
Vue.prototype.$imgixBaseUrl = options.baseUrl;
Vue.component("CustomComponent", component);
}
I'm afraid this isn't going to be the simple answer you might have been hoping for... there's a lot to unpick here.
Let's start with factory.js. That is not a factory. It's a singleton. Singletons have problems around dependencies, configuration and the timing of instantiation and that's precisely the problem you're hitting. More on that later.
Now let's take a look at the plugin. First up, these two lines:
if (install.installed) return;
install.installed = true;
That shouldn't be necessary. Vue already does this automatically and should ensure your plugin is only installed once. Perhaps this came from an old tutorial? Take a look at the source code for Vue.use, there's not a lot to it:
https://github.com/vuejs/vue/blob/4821149b8bbd4650b1d9c9c3cfbb539ac1e24589/src/core/global-api/use.js
Digging into the Vue source code is a really good habit to get into. Sometimes it will melt your mind but there are some bits, like this, that aren't particularly difficult to follow. Once you get used to it even the more opaque sections start to become a little clearer.
Back to the the plugin.
Vue.prototype.$imgixBaseUrl = options.baseUrl;
It is not clear why you are adding this to the prototype.
I'm going to assume you are already familiar with how JavaScript function prototypes work.
Component instances are actually instances of Vue. So any properties added to Vue.prototype will be inherited by your components with almost no overhead. Consider the following simple component:
<template>
<div #click="onClick">
{{ $imgixBaseUrl }}
</div>
</template>
<script>
export default {
methods: {
onClick () {
const url = this.$imgixBaseUrl
// ...
}
}
}
</script>
As $imgixBaseUrl is an inherited property it is available within onClick via this.$imgixBaseUrl. Further, templates resolve identifiers as properties of the current Vue instance, so {{ $imgixBaseUrl }} will also access this.$imgixBaseUrl.
However, if you don't need to access $imgixBaseUrl within a component then there is no need to put it on the Vue prototype. You might as well just dump it directly on Vue:
Vue.imgixBaseUrl = options.baseUrl;
In the code above I've ditched the $ as there's no longer a risk of colliding with component instance properties, which is what motivates the $ when using the prototype.
So, back to the core problem.
As I've already mentioned, singletons have major problems around creation timing and configuration. Vue has its own built-in solution for these 'do it once at the start' scenarios. That's what plugins are. However, the key feature is that plugins don't do anything until you call install, allowing you to control the timing.
The problem with your original code is that the contents of factory.js will run as soon as the file is imported. That will be before your plugin is installed, so Vue.prototype.$imgixBaseUrl won't have been set yet. The ImgixClient instance will be created immediately. It won't wait until something tries to use it. When Vue.prototype.$imgixBaseUrl is subsequently set to the correct value that won't have any effect, it's too late.
One way (though not necessarily the best way) to fix this would be to lazily instantiate ImgixClient. That might look something like this:
import Vue from "vue";
import ImgixClient from "imgix-core-js";
var imgixClient = null;
export function getClient () {
if (!imgixClient) {
imgixClient = new ImgixClient({
domain: Vue.prototype.$imgixBaseUrl
});
}
return imgixClient;
}
So long as nothing calls getClient() before the plugin is installed this should work. However, that's a big condition. It'd be easy to make the mistake of calling it too soon. Besides the temporal coupling that this creates there's also the much more direct coupling created by sharing the configuration via Vue. While the idea of having the ImgixClient instantiation code in its own little file makes perfect sense it only really stands up to scrutiny if it is independent of Vue.
Instead I'd probably just move the instantiation to within the plugin, something like this:
import ImgixClient from "imgix-core-js";
export default {
install (Vue, options) {
Vue.imgixClient = Vue.prototype.$imgixClient = new ImgixClient({
domain: options.baseUrl
});
Vue.component("CustomComponent", component);
}
}
I've made a few superficial changes, using a default export and wrapping the function in an object, but you can ignore those if you prefer the way you had it in the original code.
If the client is needed within a component it can be accessed via the property $imgixClient, inherited from the prototype. For any other code that needs access to the client it can either be passed from the component or (more likely) grabbed directly from Vue.imgixClient. If either of these use cases doesn't apply then you can remove the relevant section of the plugin.

VUE Deallocating memory using THREE JS between routes

I'm in the process of learning VUE JS. I have a very basic SPA that routes between various pages.
I have a number of THREE JS demos that I've built in my spare time and I've noticed that if I jump between pages on them eventually they grind to a halt from a build up of memory. I want to avoid pasting their scripts in here as they're massive and I don't think the problem lies within them.
I think the problem the lies somewhere in between how I'm instantiating them and my lack of knowledge in VUE JS is what's causing me trouble with this problem.
Here is an example of one of the views I'm routing to in VUE JS:
<template>
<div class="particles">
<main-menu></main-menu>
<div id="demo"></div>
</div>
</template>
<script>
import mainMenu from 'root/views/menu.vue';
export default {
components: { mainMenu },
mounted() {
var Particles = require('root/webgl/particles.js');
var demo = new Particles();
demo.run();
}
}
</script>
The original demos were built using traditional JavaScript (it's a ES5/6 class) and I was hoping that I could just plug them into my VUE SPA. Within each demo I am doing things like:
this.vsParticles = document.getElementById('vs-particles').textContent;
to load the shaders and to attach my THREE demo to a DOM element.
The issue I'm having is that somewhere something isn't being deleted. Within my demos i'm not creating anything new in the DOM and they run fine in non-SPA demos, but once I place them into a SPA app and jump between pages they build up.
I'm under the impression that when you change routes everything should get wiped. So all of the elements within my template tags and any objects I create within mounted(). But this doesn't seem to be the case and I'm just wondering is there something extra I need to include in VUE to completely clean everything up inbetween pages?
As Mathieu D. mentionned, you should move your require outside of the method.
Also, you may need to clear the WebGL context on the destroyed () Vue Component's hook.
I am not sure if it is the right way to do, but here is how I dispose it in my program :
this.renderer.forceContextLoss()
this.renderer.context = null
this.renderer.domElement = null
this.renderer = null
Could you try to import particles.js out of mounted method ?
For example, under your import of mainMenu ?
The import would be done one for all of your instanciation of Particles.
This will give you this code :
import mainMenu from 'root/views/menu.vue';
import Particles from 'root/webgl/particles.js';
export default {
components: { mainMenu },
mounted() {
var demo = new Particles();
demo.run();
}
}
You could also read reactivity in depth in the documentation. It will help you understand how VueJS store and access data. I don't know if in your component code you store some data of your ThreeJS examples, but if so, it could consume some memory. In that case, use destroyed hook to clean your data.

Find and manipulate a React.js component from the JavaScript console

When I work with JS I tend to whip out a console for the browser and manipulate values on the fly.
I have a page where I use React to render some components and I had the idea that it would be great to be able to manipulate it's state from the console to debug a design quirk which is only visible if the component is in a corner-case state.
I ran into problem that I was unable to get hold of a reference to my component.
I figured there might be a list of active components currently being rendered somewhere, but I was not able to find one on the React global object or anywhere else.
Is there an exposed reference to the components being rendered?
I'm rendering the component like:
<script>React.render(React.createElement(Comp, domElem))</script>
I could store a reference to the result of React.createElement() but it seems to be an antipattern. Also I'm using the ReactJS.NET library to handle server-side rendering for me so the whole React.render line is generated and is hard to modify.
My other idea was to create a mixin that makes the component explicitly expose itself on mount, like:
var ActiveComponents = [];
var debugMixin = {
componentDidMount: function () {
var id = this.getDOMNode().id;
ActiveComponents[id] = {
id: id,
getState: () => { return this.state; },
setState: (state) => { this.setState(state); },
comp: this
};
}
};
Are there drawbacks for an approach like this? Is this the same antipattern mentioned above?
Although being much cleaner than entangling these test hooks in the component code directly, adding a mixin is still a modification, and I would like to avoid that if possible.
The questions I hope to get answers for are bolded.
A workaround for this is to assign your object to the window object:
window.myStateObject = myStateObject
and then you can inspect it in the console:
window.myStateObject
There is a ReactJS extension for Chrome that may meet your needs https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi
If that isn't good enough, React keeps track of all the mounted components in a private variable instancesByReactID. If you just want to access these for debugging, you could modify the React code and expose that variable as a global.

Categories

Resources