react init app only after calling function in html file - javascript

I am currently building app in React.js that is suppose to be a advanced widget. Main requirement is that app should initialise only when at some point in time someone will execute function:
startApp({
someData: 'test',
wrap: 'domSelector'
});
i found this article Link, where author claims you can do this:
<div id="MyApp"></div>
<script>
window.runReactApplication({
user: { name: 'BTM', email: 'example#example.com' }
}, document.querySelector('#MyApp'));
</script>
and index.js
function runApplication(data, node) {
ReactDOM.render(<App data={data} />, node);
}
window.runReactApplication = runApplication;
It doesnt work for me as i get runReactApplication is undefined error.
Just out of curiosity i tried to wrap execution around timeout and it worked:
<script>
setTimeout(function() {
window.runReactApplication({
user: { name: 'BTM', email: 'example#example.com' }
}, document.querySelector('#MyApp'));
}, 150);
</script>
i can guess that in production its not a big problem because i can include app code first and then script so i have guarantee that function is added to the window.
But in development environment its a problem i dont know how to solve, and having timeout around function is obviously crazy. Anyone has an idea ?

You can use dynamic imports instead of setTimeout.
Let's say all of your react logic is bundled inside of index.js. Simply do the following to ensure that index.js has been downloaded and run before the desired DOM attachment:
import('index.js').then(() => {
window.runReactApplication({
user: { name: 'BTM', email: 'example#example.com' }
}, document.querySelector('#MyApp'));
}
If you want to preload the index.js file for optimal speed, simply add
<script src="index.js"></script>
to the head of your index.html file.
In the broader context, however, I am not sure why your startApp function wouldn't simply be:
function startApp(params){
const {data, selector} = params;
ReactDOM.render({
<App data={data} />
}, document.querySelector(selector));
}
And you could just bind this as a callback to whatever html event you want (i.e., onClick for a button, onLoad for the document loading, etc.).
button.onclick = startApp({data:{email: me#test.com}, selector: 'domSelector'});

Related

Nuxt re-rendering for no reason - content popping from one version to another

I'm trying to run an A/B test on my landing page, which is part of a statically-generated Nuxt (v2.15.7) site. For 50% of users, we show a different heading and description on the form.
The issue is that when the page loads, the content sometimes pops from the A test version to B test version (without refreshing the page, or anything else manually causing a re-render).
Here's my code, reduced the most barebones reproduction of the issue:
My landing page component:
<template>
<div>
<SliceMinimalFormHeader
:locale="locale"
v-bind="formContent"
:experiments="experiments"
/>
</div>
</template>
<script>
import SliceMinimalFormHeader from '#/components/SliceMinimalFormHeader.vue'
import { EXPERIMENTS } from '#/data/consts.js'
export default {
components: {
SliceMinimalFormHeader,
},
props: {
locale: {
type: String,
default: 'en-us'
}
},
data() {
const testGroup = Math.random() > 0.5 ? 'A' : 'B'
return {
experiments:
this.locale === 'en-us' && testGroup === 'A'
? [EXPERIMENTS.CALCULATOR]
: [],
formContent: {
'form-heading': '',
'form-description': ''
},
}
},
created() {
const newFormContent = {
'form-heading': 'Heading for the B test version',
'form-description': 'Description for the B test version'
}
if (this.experiments.includes(EXPERIMENTS.CALCULATOR)) {
newFormContent['form-heading'] =
'Heading for the A test version'
newFormContent['form-description'] =
'Description for the A test version'
}
this.formContent = newFormContent
}
}
</script>
Then, inside the child SliceMinimalFormHeader component:
<template>
<section class="grid-12 content-wrapper">
<h4 class="heading-2" :class="$style['form-heading']">
{{ formHeading }}
</h4>
<div :class="$style['form-description']">
{{ formDescription }}
</div>
</section>
</template>
<script>
export default {
props: {
formHeading: {
type: String,
default: ''
},
formDescription: {
type: String,
default: ''
}
}
}
</script>
I'm at my wits' end trying to figure this out!! Any help would be much appreciated.
Vue is a client side framework. All HTML is generated by JS inside the browser. This means that typical Vue app is just very simple HTMl file with almost no HTML and some CSS and Javascript <script> tags...
This is problem for SEO because most crawlers (Google, FB, Twitter) do not execute JS and just scan the HTML returned from server...
To solve this, frameworks as Nuxt was created. They solve the problem by executing Vue app on the server and rendering the HTML on the server - either at request time (Nuxt classic - new HTML is generated each time the request comes) or at build time (Nuxt generate - HTML is generated to a file and same HTML is returned for each request)
In all cases, HTML returned by the server is different but everything else is same. It is still a Vue app, which is executed on the client and once it is started, it overrides any HTML returned from the server...
So in your case you generate some HTML for all users (so either A or B is randomly chosen), this HTML is loaded for all users, but once the Vue app is loaded, it takes the control and renders (randomly) either A or B variant...

Cannot read property '$options' of undefined making external js file for head options

I need to set up global head in Nuxt for my app, which some subpages will overwrite. Those global head needs to contain translated data.
I created seoHead.js file with code:
import Vue from "vue";
export const $t = (sign) => Vue.prototype.$nuxt.$options.i18n.t(sign);
export default {
title: $t("seoGlobal.title"),
meta: [
{ charset: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{
hid: "description",
name: "description",
content: $t("seoGlobal.description"),
},
{
hid: "ogSiteName",
name: "og:site_name",
content: "Test Page",
},
{
hid: "ogTitle",
name: "og:title",
content: $t("seoGlobal.ogTitle"),
},
(...)
],
};
I import and use this data in my index.vue and other pages like this:
import seoHead from "~/constants/seoHead";
export default {
head() {
const metaI18n = this.$nuxtI18nSeo();
const currentPath = process.env.LP_URL + this.$router.currentRoute.fullPath;
return {
...seoHead,
meta: [
{
hid: "ogLocale",
name: "og:locale",
content: metaI18n.meta[0].content,
},
{
hid: "ogLocaleAlternate",
name: "og:locale:alternate",
content: metaI18n.meta[1].content,
},
{
hid: "ogUrl",
name: "og:url",
content: currentPath,
},
],
};
},
(...)
Unfortunately, I am facing Cannot read property '$options' of undefined error. It's strange for me, because I already used export const $t = (sign) => Vue.prototype.$nuxt.$options.i18n.t(sign); code in another js file. Anyone know why this error appears? You know the best way to translate global head options?
As discussed in the comments, there seems to be a timing issue with the Nuxt lifecycle and your component: at the time your component seoHead.js is imported, Nuxt has not yet injected its $nuxt object into Vue. So an easy workaround would be to delay the execution of your $t function (which accesses $nuxt):
Change your component to export a function which returns the object, instead of directly exporting the object:
export default function() {
return {
title: $t("seoGlobal.title"),
// ...
}
}
In index.vue, change your head function to call seoHead when spreading it:
return {
...seoHead(),
// ...
This way, the code which accesses $nuxt will be executed later -- not when seoHead is imported, but only when the head function is executed. At this time, the Nuxt lifecycle hopefully has finished its startup work and the required object is in place.
As I said, this is merely a workaround; if you would be calling head immediately in index.vue, the same error would appear. So unless you find out a proper way to integrate into the Nuxt lifecycle, I suggest to also put a safeguard into your translation function:
const $t = (sign) => Vue.prototype.$nuxt
? Vue.prototype.$nuxt.$options.i18n.t(sign)
: sign
This will return the i18n key if the required infrastructure is not yet in place. Not great, but better than an exception ;)
Alternatively you might be able to directly import your i18n functionality, without going through Nuxt at all; this way you wouldn't have any dependency on the infrastructure at all -- much better.
I think what you basically need here is a mixin.
export default {
title: $t("seoGlobal.title"),
meta: this.computedMeta,
computed:{
computedMeta(){
return [....] // this contains the array of objects in meta
}
}
methods:{
yourMethod(sign){
return this.$nuxt.$options.i18n.t(sign);
}
}
};
then just import it as a mixin in whatever file you need.

How to render variable stored DOM Nodes on VueJS?

I'm using Tokbox, a WebRTC SDK and one of their methods returns a video object into a variable.
With Vanilla JS I'd simply use append to add it to my DOM, but with VueJS, I'm not sure how can I accomplish this. I tried using v-html but it outputs the object as a JSON. Here is a screenshot of the object representation on chrome's console:
I don't want to use vanilla JS to append it, I'd rather expect VueJS to convert it to its own Virtual DOM object so I can freely manipulate it and don't worry about wrong states for this object.
I don't know if I'm making sense over here, but I hope you get the idea.
Thanks.
I believe you could use $refs, something like
<video ref="tokboxVideo" />
then
mounted() {
this.$refs.tokboxVideo.srcObject = this.tokboxVideoElementCreated;
}
You might have better success with the streamCreated event on the session. Create a Subscriber component like below:
<template>
<div>
</div>
</template>
<script>
import OT from '#opentok/client';
export default {
name: 'subscriber',
props: {
stream: {
type: OT.Stream,
required: true
},
session: {
type: OT.Session,
required: true
},
opts: {
type: Object,
required: false
}
},
mounted: function() {
const subscriber = this.session.subscribe(
this.stream,
this.$el,
this.opts,
err => {
if (err) {
this.$emit('error', err);
} else {
this.$emit('subscriberConnected', subscriber);
}
}
);
this.$emit('subscriberCreated', subscriber);
}
};
</script>
This will insert the video element inside the component. Then you can control how the component is displayed in your app.
Check out the basic vue sample in our repos.

How To import sanitize.js to react apps Function / class not found

I've some problem, in my project I need to add Sanitize.js on my project, I've copied to my own 3rd party folder ex vendor
to import it I'm using
import {san} from '../../vendor/Sanitize' //There's No error when compiling this one
but there's an error when I run the page, I'm trying to call the function from Sanitize.js as in readme saying to use it just do like this
var s = new san.Sanitize({
elements: ['a', 'span'],
attributes: {
a: ['href', 'title'],
span: ['class']
},
protocols: {
a: { href: ['http', 'https', 'mailto'] }
}
});
s.clean_node(p);
The Error is
san.Sanitize is not a function/ class constructor
Any idea why this is happening? or did I miss something? There's no Error in compiling process, the error only occurs when I try to run the web page,
Because Sanitize.js is not a module.
Maybe you can try the following solution:
Add export default Sanitize; in end of sanitize.js.
Use import Sanitize from "./sanitize"; to import it.
Remove the following code from sanitize.js.
if ( typeof define === "function" ) {
define( "sanitize", [], function () { return Sanitize; } );
}

How to get the latest data from parent to child components after page refresh

I am working on a project and using Vue.js for the frontend. I have following code in the main.js file.
new Vue({ // eslint-disable-line no-new
//el: '#app',
router,
data () {
return {
friends: []
}
},
methods: {
getFriends: function () {
return this.friends;
}
},
created: function () {
this.$http.get('/user/' + this.getUserIDCookie('userID') +
'/friends').then(function (response) {
this.friends = response.data;
});
},
components: {
'nav-bar': require('./components/Navigation.vue')
},
template: `
<div id="app">
<nav-bar></nav-bar>
<router-view class="router-view"></router-view>
</div>`
}).$mount('#app');
In one of the pages(for ex. when the page is redirected to localhost/#/user/1/details, I am retrieving the friends' list from main.js like below:
<script type="text/babel">
export default {
name: 'profile',
data: function () {
return {
user: {},
friends: []
}
},
methods: {
// Some methods
},
created: function () {
this.friends = this.$root.getFriends();
}
}
</script>
The problem arises when I refresh the current page. After page refresh, this.friends is null/undefined because this.$root.getFriends() is returning null/undefined. I can move it to user component, but I want to keep it in main.js so that GET call is used once and data will be available to the whole application.
Any input regarding how to solve this issue would be great. I am using Vue 2.0.1
Really, what you want to do, is pass the data the component needs as props.
The dirt simple easiest way to do it is this.
<router-view class="router-view" :friends="friends"></router-view>
And in your profile component,
export default {
props:["friends"],
name: 'profile',
data: function () {
return {
user: {},
friends: []
}
},
methods: {
// Some methods
}
}
If you want to get more sophisticated, the later versions of VueRouter allow you to pass properties to routes in several ways.
Finally, there's always Vuex or some other state management tool if your application gets complex enough.
The problem is that when you refresh the page, the whole app reloads, which includes the get, which is asynchronous. The router figures out that it needs to render details, so that component loads, and calls getFriends, but the asynchronous get hasn't finished.
You could work around this by saving and pulling the Promise from the get, but Bert's answer is correct: the Vue Way is to send data as props, not to have children pull it from parents.

Categories