switch between root component based on different url parameter - vue.js

i am developing a vujs based museum installation with several clients and one server. I would like to develop the two apps, client and server, in one application.
when calling the url I want to read out the parameters.
https://example.com/mode=client&id=1 or mode=server
then I want to load different root components with creatapp
if server .. const app = Vue.createApp(serverComponent)
if client ... const app = Vue.createApp(serverComponent)
is that a good way?
if so, how can I pass the clientID directly into the root component
EDITED
its simple to pass props to the root component with Vue.createApp(clientComponent, {id:id,...})
but currently I fail to choose between 2 different root components.
following setup
import App from './App.vue'
import AppServer from './AppServer.vue'
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const mode = urlParams.get('mode')
if (mode == "server"){
let app = createApp(AppServer);
} else {
let id = urlParams.get('id')
app = createApp(App, { id: parseInt(id) } );
}
but let app = createApp(AppServer); throws an error. app never initialised

I've implemented and tested the functionality that you need.
import Vue from 'vue'
import App from './App'
import AppServer from './AppServer'
Vue.config.productionTip = false
const NotFound = {
template: '<p>Page not found</p>'
}
/* eslint-disable no-new */
new Vue({
el: '#app',
data: {
currentRoute: window.location.pathname
},
methods: {
RequestDispatcher (url) {
let urlParams = new URLSearchParams(url)
if (url.includes('mode=server')) {
return AppServer // createApp(AppServer)
} else if (url.includes('mode=client')) {
let id = urlParams.get('id')
console.log(id)
return App // createApp(App, { id: parseInt(id) })
}
}
},
computed: {
ViewComponent () {
return this.RequestDispatcher(this.currentRoute) || NotFound
}
},
render (h) {
return h(this.ViewComponent)
}
})

Related

CASL - Vue 3 - Element not showing for role

I am having a bit of a challenge implementing CASL in my app.
I have created the following composable useAppAbility ("hook") that defines all the rules:
import { AbilityBuilder, createMongoAbility, subject } from "#casl/ability";
import { useAbility } from "#casl/vue";
const service = {};
const user = {};
const subscription = {};
const invoice = {};
const account = {};
const ability = createMongoAbility();
const ROLES = ["admin", "account_owner", "beneficiary", "super_admin"];
const defineAbilityFor = (role: Object) => {
const { can, rules } = new AbilityBuilder(createMongoAbility);
const is = (r: string) => {
return ROLES.indexOf(r) >= ROLES.indexOf(role);
};
if (is("admin")) {
can("add", subject("User", user));
can("remove", subject("User", user));
}
return ability.update(rules);
};
export { defineAbilityFor, ability, subject };
export const useAppAbility = () => useAbility();
Added the plugin to the main.ts:
import { ability } from "#/composables/useAppAbility";
import { abilitiesPlugin } from "#casl/vue";
createApp(App)
.use(abilitiesPlugin, ability, {
useGlobalProperties: true,
})
//stuff
.mount("#app");
And then, I found that using the beforeEach hook in the router and passing in the user before each route was the simplest way to deal with page load and SPA routing.
I have therefore added the following to my router/index.ts:
import { ability, defineAbilityFor } from "#/composables/useAppAbility";
import useAuth from "#/composables/useAuth";
const {
getUserByClaims,
} = useAuth();
// routes
router.beforeEach(async (to, _from, next) => {
defineAbilityFor(getUserByClaims.value.roles)
})
At this stage I can verify that the user is being passed properly to the defineAbilityFor function and when using the ability.on("update") hook to log the rules object, I have the following output:
Which seems to confirm that the rules for this user are built and updated correctly?
However, when trying to display a button for the said admin in a component, the button does not show.
MyComponent.vue:
<script setup lang="ts">
import { useAppAbility, subject } from "#/composables/useAppAbility";
const { can } = useAppAbility();
</script>
<template>
<div v-if="can('add', subject('User', {}))">TEST FOR CASL</div> <!-- DOES NOT SHOW-->
</template>
Not sure where to go from there, any help would be appreciated.
Thanks

How to dynamically access a remote component in vue js with module federation

I am trying to build a vue js 2 microfrontend with module federation. I dont want to use static remote imports via the webpack.config.js like this
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1#http://localhost:3001/remoteEntry.js',
},
}),
],
};
I am looking for a way to dynamically import vue components into my host application. I tried this approach so far, but i only found examples that worked with angular or react.
The goal is to have multiple remote frontends that can automatically register somewhere, maybe in some kind of store. The host application then can access this store and get all of the registered remote applications (name, url, components). The host application then loads the components and should be able to use them. I remote import the component HelloDerp, the loading process is working fine but i dont know how to render it on my host application. I read the vue js doc about dynamic and async imports but i think that only works for local components.
What i've got so far in the host application:
<template>
<div id="app">
<HelloWorld />
<HelloDerp />
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
const HelloDerp = null;
export default {
name: "App",
components: {
HelloWorld,
HelloDerp,
},
mounted() {
var remoteUrlWithVersion = "http://localhost:9000/remoteEntry.js";
const element = document.createElement("script");
element.type = "text/javascript";
element.async = true;
element.src = remoteUrlWithVersion;
element.onload = () => {
console.log(`Dynamic Script Loaded: ${element.src}`);
HelloDerp = loadComponent("core", "./HelloDerp");
};
document.head.appendChild(element);
return null;
},
};
async function loadComponent(scope, module) {
// Initializes the shared scope. Fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__("default");
const container = window[scope]; // or get the container somewhere else
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
}
</script>
Sorry i almost forgot about this. Here's my solution.
Load Modules:
export default async function loadModules(
host: string,
ownModuleName: string,
wantedNames: string[]
): Promise<RemoteComponent[]> {
...
uiApplications.forEach((uiApplication) => {
const remoteURL = `${uiApplication.protocol}://${uiApplication.host}:${uiApplication.port}/${uiApplication.moduleName}/${uiApplication.fileName}`;
const { componentNames } = uiApplication;
const { moduleName } = uiApplication;
const element = document.createElement('script');
element.type = 'text/javascript';
element.async = true;
element.src = remoteURL;
element.onload = () => {
componentNames?.forEach((componentName) => {
const component = loadModule(moduleName, `./${componentName}`);
component.then((result) => {
if (componentName.toLowerCase().endsWith('view')) {
// share views
components.push(new RemoteComponent(result.default, componentName));
} else {
// share business logic
components.push(new RemoteComponent(result, componentName));
}
});
});
};
document.head.appendChild(element);
});
});
...
}
export default async function loadModule(scope: string, module: string): Promise<any> {
await __webpack_init_sharing__('default');
const container = window[scope]; // or get the container somewhere else
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
}
Add Modules to routes
router.addRoute({
name: remoteComponent.componentName,
path: `/${remoteComponent.componentName}`,
component: remoteComponent.component,
});

Nativescript Vue - async operations in main.js before rendering

i would like to add a login page to my app with Firebase Authentication:
https://github.com/EddyVerbruggen/nativescript-plugin-firebase/blob/master/docs/AUTHENTICATION.md
Following the guide i've added the "onAuthStateChanged" function inside the init firebase.
Now i would like to pass to the render function in the Vue instance creation the correct page, based on the value returned by the Firebase function.
If the user it's authenticated, will be rendered the "home.vue" page, otherwise the "login.vue" page.
The problem it's that the firebase function return the state of the user after the Vue instance creation.
Here my code:
import Vue from 'nativescript-vue'
import store from './store'
import Home from './components/Page/home.vue'
import Login from './components/Page/login.vue'
import VueDevtools from 'nativescript-vue-devtools'
var firebase = require("#nativescript/firebase").firebase;
var pageToRender;
firebase.init({
onAuthStateChanged: function(data) {
if (data.loggedIn) {
pageToRender = Home;
}else{
pageToRender = Login;
}
}
}).then(
function () {
console.log("firebase.init done");
},
function (error) {
console.log("firebase.init error: " + error);
}
);
if(TNS_ENV !== 'production') {
Vue.use(VueDevtools)
}
// Prints Vue logs when --env.production is *NOT* set while building
Vue.config.silent = (TNS_ENV === 'production')
new Vue({
store,
render: h => h('frame', [h(pageToRender)])
}).$start()
I already tried to move all the code inside an async function in order to await the firebase response before the Vue instance creation but i receive the error:
System.err: Error: Main entry is missing. App cannot be started.
Verify app bootstrap.
In this way:
async function start(){
var loggedIn = await firebase_start();
var pageToRender;
if (loggedIn) {
pageToRender = Home;
}else{
pageToRender = Login;
}
if(TNS_ENV !== 'production') {
Vue.use(VueDevtools)
}
// Prints Vue logs when --env.production is *NOT* set while building
Vue.config.silent = (TNS_ENV === 'production')
new Vue({
store,
render: h => h('frame', [h(pageToRender)])
}).$start()
}
Thanks for the help!
I would approach it differently.
When the user logs in, you can save this on the device with the https://github.com/nativescript-community/preferences plugin as follows:
function successLoginUser(){
const prefs = new Preferences();
prefs.setValue("isUserLogin", true);
}
And then when starting the application you can do something like this:
import Vue from 'nativescript-vue'
import store from './store'
import Home from './components/Page/home.vue'
import Login from './components/Page/login.vue'
import VueDevtools from 'nativescript-vue-devtools'
//This
const prefs = new Preferences();
const pageToRender = prefs.getValue("isUserLogin") ? Home: Login ;
if(TNS_ENV !== 'production') {
Vue.use(VueDevtools)
}
// Prints Vue logs when --env.production is *NOT* set while building
Vue.config.silent = (TNS_ENV === 'production')
new Vue({
store,
render: h => h('frame', [h(pageToRender)])
}).$start()
When the user logs out:
const prefs = new Preferences();
prefs.setValue("isUserLogin", false);
I haven't tried it but it should work
//for me here firebase is imported from a diffrent file where i already initialized it.
let app;
firebase.auth().onAuthStateChanged((user, error) => {
//decide which page to render
//also make sure both components you are trying to render are imported
if (!app) {
app = new Vue({
router,
store,
render: (h) => h(PageToRender),
}).$start()
}
});

vue-apollo awsappsync - refresh credentials

I am using vue-apollo with AWSAppSyncClient. I have followed this documentation for Vue - https://github.com/awslabs/aws-mobile-appsync-sdk-js . My requirement is user should be able to subscribe to appsync. Here is the main.js code.
import './bootstrap';
import router from './routes';
import store from './store';
import App from './components/templates/App';
import AWSAppSyncClient from 'aws-appsync';
import VueApollo from "vue-apollo";
const config = {
url: process.env.MIX_APPSYNC_URL,
region: process.env.MIX_APPSYNC_REGION,
auth: {
type: process.env.MIX_APPSYNC_TYPE,
credentials: {
accessKeyId: "temporary access key goes here",
secretAccessKey: "temporary secret access key goes here",
sessionToken: "session token goes here"
}
},
};
I get the 'credentials' part after user logged in successfully with aws cognito validation.
const options = {
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
}
}
}
// Create the apollo client
const apolloClient = new AWSAppSyncClient(config, options);
//The provider holds the Apollo client instances that can then be used by all the child components.
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
});
var vm = new Vue({
el:"#dashboardapp",
router:router,
apolloProvider:apolloProvider,
store:store,
components: { App },
template: '<App/>',
data() {
return {
}
},
});
The above set up works fine. User can login. After cognito verifies user, it sends temporary credentials (accesskey, secret key, session token). With temporary credentials I am able to subscribe to aws appsync through vue-apollo. However, the credentials are valid for 1 hour only. So I need to update the credentials and keep the subscription part to get live data. But I dont know how to do it. I have gone through the docs, but not able to find anything specific to my case.
I need to update the credentials from either a child component of 'vm' or from vuex store. I already have updated credentials. I just dont know how to pass it to the 'AWSAppSyncClient' and how to re-subscribe with updated credentials. Also I don't want to reload the page. It should update credentials without reloading the page. Hope anyone would have done this before ?
I have done few changes to my code and now I am able to achieve what I wanted. Here are the changes I have done, in case anyone doing same thing.
First loading the apollo client as blank - means without awsappsyncclient in main.js file.
import './bootstrap';
import router from './routes';
import store from './store';
import App from './components/templates/App';
import VueApollo from "vue-apollo";
// Create the apollo client
const apolloClient = '';
//The provider holds the Apollo client instances that can then be used by all the child components.
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
});
var vm = new Vue({
el:"#dashboardapp",
router:router,
apolloProvider:apolloProvider,
store:store,
components: { App },
template: '<App/>',
data() {
return {
}
},
});
Then from child component I am creating smart subscription. Once temporary credentials are expired, I am generating new credentials and updating in vuex store. Based on the change, I am stooping the old smart subscription and creating a new smart subscription.
Here is the child component code.
<template>
<div class="status-frame">
<!-- relevant code goes here -->
</div>
</template>
<script>
import gql from 'graphql-tag';
import AWSAppSyncClient from 'aws-appsync';
import VueApollo from "vue-apollo";
export default {
data () {
return {
}
},
methods: {
timelineSubscribe() {
if(this.$parent.$apolloProvider.clients[1]) {
delete this.$parent.$apolloProvider.clients[1];
this.$apollo.subscriptions.subscribeToNewNotification.stop();
}
const config = {
url: process.env.MIX_APPSYNC_URL,
region: process.env.MIX_APPSYNC_REGION,
auth: {
type: process.env.MIX_APPSYNC_TYPE,
credentials: {
accessKeyId: this.appsyncObj.accessKeyId,
secretAccessKey: this.appsyncObj.secretAccessKey,
sessionToken: this.appsyncObj.sessionToken
}
},
};
const options = {
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
}
}
}
// Create the apollo client
const apolloClient = new AWSAppSyncClient(config, options);
// add apolloClient to a new index in apolloProvider.
this.$parent.$apolloProvider.clients[1] = apolloClient;
console.log(this.$apollo.provider.clients);
this.$apollo.addSmartSubscription('subscribeToAnything', {
client: '1',
query: gql`subscription subscribeToAnything ($accountId: String!) {
subscribeToAnything(accountId: $accountId) {
// required fields goes here
}
}`,
// Reactive variables
variables() {
// This works just like regular queries
// and will re-subscribe with the right variables
// each time the values change
return {
accountId: 'account_id goes here',
}
},
// Result hook
result(data) {
console.log(data);
},
skip () {
return false;
}
});
}
},
computed: {
appsyncObj() {
return this.$store.getters['profile/appsyncObj']; // get from vuex store
}
},
watch: {
'appsyncObj' () {
this.timelineSubscribe(); // each time appsyncObj changes, it will call this method and resubscribe with new credentials.
}
},
}
I update the vuex store for appsyncObj after login and after getting new credentials. However, I have not added that code here.

Vue I18n - TypeError: Cannot redefine property: $i18n

So I'm getting kind of crazy with this. I really don't understand.
This is a minimal version of my app.js file:
import Vue from 'vue'
import VueI18n from 'vue-i18n'
console.log("vue.prototype", Vue.prototype.$i18n)
Vue.use(VueI18n)
console.log("vue.prototype", Vue.prototype.$i18n)
const createApp = function() {
// create store and router instances
const store = createStore()
const router = createRouter()
if(process.browser) {
if(window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
}
// sync the router with the vuex store.
// this registers `store.state.route`
sync(store, router)
// create the app instance.
// here we inject the router, store and ssr context to all child components,
// making them available everywhere as `this.$router` and `this.$store`.
//
const app = new Vue({
router,
store,
render: h => h(Application)
})
// expose the app, the router and the store.
// note we are not mounting the app here, since bootstrapping will be
// different depending on whether we are in a browser or on the server.
return { app, router, store }
}
export { createApp }
As you can see I did nothing but adding Vue.use(VueI18n) to the code.
I'm using:
{
"vue-i18n": "^7.6.0"
}
Now I'm getting this error:
TypeError: Cannot redefine property: $i18n
The line where this errors appear is this function in the source code:
function install (_Vue) {
Vue = _Vue;
var version = (Vue.version && Number(Vue.version.split('.')[0])) || -1;
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && install.installed) {
warn('already installed.');
return
}
install.installed = true;
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && version < 2) {
warn(("vue-i18n (" + (install.version) + ") need to use Vue 2.0 or later (Vue: " + (Vue.version) + ")."));
return
}
console.log("VUE:PROTOTYPE", Vue.prototype.$i18n)
Object.defineProperty(Vue.prototype, '$i18n', {
get: function get () { return this._i18n }
});
console.log("VUE:PROTOTYPE", Vue.prototype.$i18n)
extend(Vue);
Vue.mixin(mixin);
Vue.directive('t', { bind: bind, update: update });
Vue.component(component.name, component);
// use object-based merge strategy
var strats = Vue.config.optionMergeStrategies;
strats.i18n = strats.methods;
}
Both console.log("VUE:PROTOTYPE") where added by me, and surprise, the first one returns "undefined" and the second one is never reached because of the error.
What is happening? Anybody got a clue?