Calling VueJS Components after login from an external script - javascript

I have a VueJS Program where I want to provide a login. Thus this login is used one multiple platforms, it is loaded via an external server (See: mounted). It is loaded, puts elements into the mainlogin-div creating a login view and when the user clicks Login it performs a http login request to another server. When the login is successful, the external script calls done(result) or error(result) whether the login was a success or not.
But now of course how can I go back to calling Vue-stuff from there? I cant just do like this.$router.push("/coolPath") or call a defined method like loggedIn to make a toast or what not... How is this implemented?
<template>
<div id="main">
<div id="mainlogin"></div>
<script type="application/javascript">
function done(result){
console.log(result);
loggedIn(result, true)
}
function error(result) {
console.log(result)
loggedIn(result, false)
}
</script>
</div>
</template>
<script>
const Toast = require("#/toast/toast")
export default {
name: "Login",
components: {},
data() {
return {
isAuthenticated: false
}
},
beforeMount() {
let loginScript = document.createElement('script')
loginScript .setAttribute('src', 'http://**.***.***.***/index_krass.js')
document.body.appendChild(loginScript )
},
methods: {
loggedIn: function (result, success){
Toast.toast(result.message, success)
}
}
}
</script>
<style>
...
</style>
Error is always "this." is not defined or loggedIn(...) is not defined...
Maybe its some race condition where VueJS needs to be loaded first or the other way around?

Instead of the <script> block in the template, just set the global functions up on window as arrow functions to capture the Vue instance context:
export default {
beforeMount() {
window.done = result => this.loggedIn(result, true)
window.error = result => this.loggedIn(result, false)
let loginScript = document.createElement("script")
loginScript.setAttribute("src", ".../index_krass.js")
document.body.appendChild(loginScript)
},
destroyed() {
window.done = window.error = undefined
},
}
demo

Related

Wait for data in mapstate to finish loading

I have stored a userProfile in Vuex to be able to access it in my whole project. But if I want to use it in the created() hook, the profile is not loaded yet. The object exists, but has no data stored in it. At least at the initial load of the page. If I access it later (eg by clicking on a button) everything works perfectly.
Is there a way to wait for the data to be finished loading?
Here is how userProfile is set in Vuex:
mutations: {
setUserProfile(state, val){
state.userProfile = val
}
},
actions: {
async fetchUserProfile({ commit }, user) {
// fetch user profile
const userProfile = await fb.teachersCollection.doc(user.uid).get()
// set user profile in state
commit('setUserProfile', userProfile.data())
},
}
Here is the code where I want to acess it:
<template>
<div>
<h1>Test</h1>
{{userProfile.firstname}}
{{institute}}
</div>
</template>
<script>
import {mapState} from 'vuex';
export default {
data() {
return {
institute: "",
}
},
computed: {
...mapState(['userProfile']),
},
created(){
this.getInstitute();
},
methods: {
async getInstitute() {
console.log(this.userProfile); //is here still empty at initial page load
const institueDoc = await this.userProfile.institute.get();
if (institueDoc.exists) {
this.institute = institueDoc.name;
} else {
console.log('dosnt exists')
}
}
}
}
</script>
Through logging in the console, I found out that the problem is the order in which the code is run. First, the method getInstitute is run, then the action and then the mutation.
I have tried to add a loaded parameter and played arround with await to fix this issue, but nothing has worked.
Even if you make created or mounted async, they won't delay your component from rendering. They will only delay the execution of the code placed after await.
If you don't want to render a portion (or all) of your template until userProfile has an id (or any other property your users have), simply use v-if
<template v-if="userProfile.id">
<!-- your normal html here... -->
</template>
<template v-else>
loading user profile...
</template>
To execute code when userProfile changes, you could place a watcher on one of its inner properties. In your case, this should work:
export default {
data: () => ({
institute: ''
}),
computed: {
...mapState(['userProfile']),
},
watch: {
'userProfile.institute': {
async handler(institute) {
if (institute) {
const { name } = await institute.get();
if (name) {
this.institute = name;
}
}
},
immediate: true
}
}
}
Side note: Vue 3 comes with a built-in solution for this pattern, called Suspense. Unfortunately, it's only mentioned in a few places, it's not (yet) properly documented and there's a sign on it the API is likely to change.
But it's quite awesome, as the rendering condition can be completely decoupled from parent. It can be contained in the suspensible child. The only thing the child declares is: "I'm currently loading" or "I'm done loading". When all suspensibles are ready, the template default is rendered.
Also, if the children are dynamically generated and new ones are pushed, the parent suspense switches back to fallback (loading) template until the newly added children are loaded. This is done out of the box, all you need to do is declare mounted async in children.
In short, what you were expecting from Vue 2.

Using Google One Tap in Angular

I'd like to use Google One Tap in my Angular 11 app. Following the documentation I added <script async defer src="https://accounts.google.com/gsi/client"></script> to my html and then used the following code in my app.component.html:
<div id="g_id_onload"
data-client_id="MY_GOOGLE_CLIENT_ID"
data-callback="handleCredentialResponse",
data-cancel_on_tap_outside="false">
</div>
The popup works fine, though I can't seem to log in. If I create a function handleCredentialResponse in app.component.ts, I get the following error: [GSI_LOGGER]: The value of 'callback' is not a function. Configuration ignored.
If I instead try to use the JavaScript API, Typescript throws the following error: Property 'accounts' does not exist on type 'typeof google'
What should I do to be able to using Google One Tap in Angular?
I had a similar problem when I used the HTML API approach, so I ended up using the JavaScript API instead.
Here's what I did:
First, make sure to install the #types/google-one-tap package.
As you mentioned, I'm also importing the script in my index.html file, like so:
<body>
<script src="https://accounts.google.com/gsi/client" async defer></script>
<app-root></app-root>
</body>
Now, moving on to your main component which in my case is app.component.ts, import the following first:
import { CredentialResponse, PromptMomentNotification } from 'google-one-tap';
Then, you can add this on the ngOnInit(). Make sure to read the documentation to get more details on the onGoogleLibraryLoad event:
// #ts-ignore
window.onGoogleLibraryLoad = () => {
console.log('Google\'s One-tap sign in script loaded!');
// #ts-ignore
google.accounts.id.initialize({
// Ref: https://developers.google.com/identity/gsi/web/reference/js-reference#IdConfiguration
client_id: 'XXXXXXXX',
callback: this.handleCredentialResponse.bind(this), // Whatever function you want to trigger...
auto_select: true,
cancel_on_tap_outside: false
});
// OPTIONAL: In my case I want to redirect the user to an specific path.
// #ts-ignore
google.accounts.id.prompt((notification: PromptMomentNotification) => {
console.log('Google prompt event triggered...');
if (notification.getDismissedReason() === 'credential_returned') {
this.ngZone.run(() => {
this.router.navigate(['myapp/somewhere'], { replaceUrl: true });
console.log('Welcome back!');
});
}
});
};
Then, the handleCredentialResponse function is where you handle the actual response with the user's credential. In my case, I wanted to decode it first. Check this out to get more details on how the credential looks once it has been decoded: https://developers.google.com/identity/gsi/web/reference/js-reference#credential
handleCredentialResponse(response: CredentialResponse) {
// Decoding JWT token...
let decodedToken: any | null = null;
try {
decodedToken = JSON.parse(atob(response?.credential.split('.')[1]));
} catch (e) {
console.error('Error while trying to decode token', e);
}
console.log('decodedToken', decodedToken);
}
I too had the same problem in adding the function to the angular component.
Then i found a solution by adding JS function in appComponent like this:
(window as any).handleCredentialResponse = (response) => {
/* your code here for handling response.credential */
}
Hope this help!
set the div in template to be rendered in ngOnInit
`<div id="loginBtn" > </div>`
dynamically inject script tag in your login.ts as follows
constructor(private _renderer2: Renderer2, #Inject(DOCUMENT) private _document: Document){}
ngAfterViewInit() {
const script1 = this._renderer2.createElement('script');
script1.src = `https://accounts.google.com/gsi/client`;
script1.async = `true`;
script1.defer = `true`;
this._renderer2.appendChild(this._document.body, script1);
}
ngOnInit(): void {
// #ts-ignore
window.onGoogleLibraryLoad = () => {
// #ts-ignore
google.accounts.id.initialize({
client_id: '335422918527-fd2d9vpim8fpvbcgbv19aiv98hjmo7c5.apps.googleusercontent.com',
callback: this.googleResponse.bind(this),
auto_select: false,
cancel_on_tap_outside: true,
})
// #ts-ignore
google.accounts!.id.renderButton( document!.getElementById('loginBtn')!, { theme: 'outline', size: 'large', width: 200 } )
// #ts-ignore
google.accounts.id.prompt();
}
}
async googleResponse(response: google.CredentialResponse) {
// your logic goes here
}
Google One Tap js library tries to find callback in the global scope and can't find it, because your callback function is scoped somewhere inside of your app, so you can attach your callback to window, like window.callback = function(data) {...}.
Also, since you are attaching it to window, it's better to give the function a less generic name.

Why it sends multiple request, when I call it only once?

My project were working fine. I just found out in console network that one of my GET request is sending twice, even I just send it once. See network console
If I comment the the whole code of created function, all GET request would no longer load/exist in the console network. (see code below)
I want to know what causes this, and how should I fix this?
Here is the Component.vue
<script>
export default {
created: async function() {
await this.$store.dispatch('file/all');
},
};
</script>
And the vuex module post.js's action:
const actions = {
all({commit}, data) {
return axios.get(`files`)
.then(response => {
commit('setData', response);
});
},
}
After many hours of searching, I found out that the key that is assigned to the Component caused the problem.
When the key is modified the GET request will send again. This the reason why it sends twice. Special thanks to #Anatoly for giving me the hint.
Below is the usage codes:
<template>
<Component :key="componentKey" #edit="dataIsChanged"/>
</template>
<script>
export default {
components: { Component },
data: () => ({
componentKey: 0,
}),
methods: {
dataIsChanged: function() {
this.componentKey = Math.random();
}
}
};
</script>

Vuejs global function with Google Auth Signin

I am using vuejs 2 and I am having issues with using the Google auth signin.
I successfully setup and got the sign out and user profile functions working using vue:
export default {
data() {
return {
user: null
};
},
methods: {
getUserProfile() {
const profile = gapi.auth2.currentUser.get().getBasicProfile();
console.log(profile.getIdToken());
},
signOut() {
const auth2 = gapi.auth2.getAuthInstance();
auth2.signOut().then(function () {
console.log('user signed out');
});
}
},
};
My main issue here is the onSignIn(googleUser) function from
<div class="g-signin2" data-onsuccess="onSignIn"></div>
The data-onsuccess="onSignIn" is looking for a js function outside the vue instance.
I tried adding the onSignIn(googleUser) function in my HTML file like:
<script>
function onSignIn(googleUser) {
const auth2 = gapi.auth2.init();
if (auth2.isSignedIn.get()) {
const profile = auth2.currentUser.get().getBasicProfile();
console.log(profile.getName());
console.log(googleUser.getAuthResponse().id_token);
console.log(googleUser.getAuthResponse().uid);
console.log(auth2.currentUser.get().getId());
}
}
</script>
This works as expected, but I wanted to know if it would be possible to add this in my vue file instead of a native javascript way, since inside this function, I will be calling other vue methods.
Or is there a way where I could add the onSignIn(googleUser) function in vue and then call it when Google Auth finishes?
The solution here is to use gapi.signin2.render to render the sign-in button inside your component's mounted hook
<template>
<div id="google-signin-btn"></div>
</template>
<script>
export default {
methods: {
onSignIn (user) {
// do stuff, for example
const profile = user.getBasicProfile()
}
},
mounted() {
gapi.signin2.render('google-signin-btn', { // this is the button "id"
onsuccess: this.onSignIn // note, no "()" here
})
}
}
</script>
See https://developers.google.com/identity/sign-in/web/reference#gapisignin2renderid_options

Vue.js - Global Data from AJAX Call

I'm giving Vue.js a try and so far I'm loving it because it's much simpler than angular. I'm currently using vue-router and vue-resource in my single page app, which connects to an API on the back end. I think I've got things mostly working with a the primary app.js, which loads vue-router and vue-resource, and several separate components for each route.
Here's my question: How do I use props to pass global data to the child components when the data is fetched using an asynchronous AJAX call? For example, the list of users can be used in just about any child component, so I would like the primary app.js to fetch the list of users and then allow each child component to have access to that list of users. The reason I would like to have the app.js fetch the list of users is so I only have to make one AJAX call for the entire app. Is there something else I should be considering?
When I use the props in the child components right now, I only get the empty array that the users variable was initialized as, not the data that gets fetched after the AJAX call. Here is some sample code:
Simplified App.js
var Vue = require('vue');
var VueRouter = require('vue-router')
Vue.use(VueRouter);
var router = new VueRouter({
// Options
});
router.map({
'*': {
component: {
template: '<p>Not found!</p>'
}
},
'/' : require('./components/dashboard.js'),
});
Vue.use(require('vue-resource'));
var App = Vue.extend({
ready: function() {
this.fetchUsers();
},
data: function() {
return {
users: [],
};
},
methods: {
fetchUsers: function() {
this.$http.get('/api/v1/users/list', function(data, status, response) {
this.users = data;
}).error(function (data, status, request) {
// handle error
});
}
}
});
router.start(App, '#app')
Simplified app.html
<div id="app" v-cloak>
<router-view users = "{{ users }}">
</router-view>
</div>
Simplified dashboard.js
module.exports = {
component: {
ready: function() {
console.log(this.users);
},
props: ['users'],
},
};
When dashboard.js gets run, it prints an empty array to the console because that's what app.js initializes the users variable as. How can I allow dashboard.js to have access to the users variable from app.js? Thanks in advance for your help!
p.s. I don't want to use the inherit: true option because I don't want ALL the app.js variables to be made available in the child components.
I believe this is actually working and you are being misled by the asynchronous behavior of $http. Because your $http call does not complete immediately, your console.log is executing before the $http call is complete.
Try putting a watch on the component against users and put a console.log in that handler.
Like this:
module.exports = {
component: {
ready: function() {
console.log(this.users);
},
props: ['users'],
watch: {
users: {
handler: function (newValue, oldValue) {
console.log("users is now", this.users);
},
deep: true
}
}
}
};
In the new version of Vue 1.0.0+ you can simply do the following, users inside your component is automatically updated:
<div id="app" v-cloak>
<router-view :users="users"></router-view>
</div>

Categories

Resources