Vuejs Module Federation dynamic import - vue.js

I'm trying to develop microfrontend with Vuejs and Module Federation. So, i have method loadModule, that returns me a an object(maybe component), but what i should do with it i don't know. I tried to import this component like defineAsyncComponent but it's useless.
And i'm tried use the render function in this object and paste this it to markup.
The screenshot shows what the object I get, but I don’t know what to do next with it and how to register this like component.
I'm stumped, any advice would be welcome
<script setup>
import { ref, onMounted, defineAsyncComponent, h } from 'vue'
import TestComp from './TestComp.vue'
let Header
const modules = [
{
protocol: 'http',
host: 'localhost',
port: 8080,
moduleName: 'Company',
fileName: 'remoteEntry.js',
componentNames: ['Header'],
},
]
onMounted(() => {
modules.forEach((uiApplication) => {
const remoteURL = `${uiApplication.protocol}://${uiApplication.host}:${uiApplication.port}/${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 = loadComponent(moduleName, `./${componentName}`)
component().then((res) => {
Header = res
console.log(res)
// what should i do next?
// This is not working
// Header = defineAsyncComponent(() => import('Company/Header'))
})
})
}
document.head.appendChild(element)
})
})[enter image description here][1]
function loadComponent(scope, module) {
return async () => {
// 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
}
}
const remoteImport = async (location, name, options) => {
const module = await importResource(location, options)
if (name) {
let m = module[name]
if (!m) {
throw new Error(
`No component named ${name} founded in component ${location}`
)
}
return module[name]
} else {
return module
}
}
</script>
<template>
<div>
<Header ref="Header" title="Test shop" />
<h2>Our Shop Page</h2>
</div>
</template>

Related

How to watch localeStorage changes in Pinia state?

I have an item called nuxt-color-mode saved in localeStorage. It has either dark or light value. And I would like to watch the value of this item from any component. So I put the Setup & Option Stores that I tried for now! By the way I'm using Nuxt 3!
'~/stores/colorTheme.ts/'
import { defineStore } from "pinia";
// Option Stores
export const useColorThemeStore = defineStore('colorThemeStore', {
state: async () => {
return { mode: await localStorage.getItem('nuxt-color-mode')};
},
getters: {},
actions: {}
});
//Setup Stores
export const useColorThemeStore = defineStore('colorThemeStore', async () => {
const colorMode = await localStorage.getItem('nuxt-color-mode');
const mode = ref(colorMode);
return { mode }
});
'~/component/Portfolio.vue'
<script setup lang="ts">
import { useColorThemeStore } from '~/stores/colorTheme';
const mode = useColorThemeStore(); // Got undedfined!
watch(mode, () => {
console.log('Mode = ', mode) // Doesn't watch or print anything!
})
</script>

Use props in composables vue3

I am upgrading an app from vue 2 to vue 3 and I am having some issues with composables. I'd like to use props in the composable but it doesn't seem to be working. The code sample is pulled from a working component and works fine when I leave it in the component.
I assume defineProps isn't supported by composables, but then I am unclear how to handle it. When I pass the src in the parameters it loses its reactivity.
// loadImage.js
import { defineProps, onMounted, ref, watch } from 'vue'
// by convention, composable function names start with "use"
export function useLoadImage() {
let loadingImage = ref(true)
let showImage = ref(false)
const props = defineProps({
src: String,
})
const delayShowImage = () => {
setTimeout(() => {
showImage.value = true
}, 100)
}
const loadImage = (src) => {
let img = new Image()
img.onload = (e) => {
loading.value = false
img.onload = undefined
img.src = undefined
img = undefined
delayShowImage()
}
img.src = src
}
onMounted(() => {
if (props.src) {
loadImage(props.src)
}
})
watch(
() => props.src,
(val) => {
if (val) {
loadingImage.value = true
loadImage(val)
}
},
)
// expose managed state as return value
return { loadingImage, showImage }
}
Edit
This method worked for me, but the two methods mentioned in the comments below did not.
I have a new question here.
// loadImage.js
import { onMounted, ref, watch } from 'vue'
// by convention, composable function names start with "use"
export function useLoadImage(props) {
let loadingImage = ref(true)
let showImage = ref(false)
const delayShowImage = () => {
setTimeout(() => {
showImage.value = true
}, 100)
}
const loadImage = (src) => {
let img = new Image()
img.onload = (e) => {
loading.value = false
img.onload = undefined
img.src = undefined
img = undefined
delayShowImage()
}
img.src = src
}
onMounted(() => {
if (props.src) {
loadImage(props.src)
}
})
watch(
() => props.src,
(val) => {
if (val) {
loadingImage.value = true
loadImage(val)
}
},
)
// expose managed state as return value
return { loadingImage, showImage }
}
<script setup>
import { defineProps, toRef } from 'vue'
import { useLoadImage } from '../../composables/loadImage'
const props = defineProps({
src: String
})
const { loading, show } = useLoadImage(props)
</script>
According to official docs :
defineProps and defineEmits are compiler macros only usable inside <script setup>
You should pass the props as parameter without destructing them to not loose the reactivity :
export function useLoadImage(props) {
....
}
you can use useRef to pass specific props without losing reactivity
const imgRef = toRef(props, "img");
const { loding, show } = useLoadImage(imgRef);

Passing a single property to a composable in Vue3

I am using a composable to load images in Vue3. I have been able to pass all the props successfully as one object, see this question, but I am unable to pass the one property I want to be reactive. I am fairly certain that the issue is that the property in undefined
// loadImage.js
import { onMounted, ref, watch } from 'vue'
// by convention, composable function names start with "use"
export function useLoadImage(src) {
let loading = ref(true)
let show = ref(false)
const delayShowImage = () => {
setTimeout(() => {
show.value = true
}, 100)
}
const loadImage = (src) => {
let img = new Image()
img.onload = (e) => {
loading.value = false
img.onload = undefined
img.src = undefined
img = undefined
delayShowImage()
}
img.src = src
}
onMounted(() => {
if (src) {
loadImage(src)
}
})
watch(
() => src,
(val) => {
if (val) {
loading.value = true
loadImage(val)
}
},
)
// expose managed state as return value
/**
* loading is the image is loading
* show is a delayed show for images that transition.
*/
return { loading, show }
}
The below method returns this in the console.log and does not error.
Proxy {src: undefined} undefined
<script setup>
import { defineProps, computed } from 'vue'
import { useLoadImage } from '../../composables/loadImage'
const props = defineProps({
src: String
})
console.log(props, props.src)
const srcRef = computed(() => props.src)
const { loading, show } = useLoadImage(srcRef)
</script>
The below method returns this in the console.log
Proxy {src: undefined} undefined
and gives the following error
TypeError: Cannot read properties of undefined (reading 'undefined')
<script setup>
import { defineProps, toRef } from 'vue'
import { useLoadImage } from '../../composables/loadImage'
const props = defineProps({
src: String
})
console.log(props, props.src)
const srcRef = toRef(props.src)
const { loading, show } = useLoadImage(srcRef)
</script>
As indicated in comments, it seems src is undefined in your component because you're probably not passing the prop correctly to the component.
Even if src were set with a string, there still would be a few other issues:
toRef's first argument should be a reactive object (i.e., props), and the second argument should be the name of a key (i.e., 'src'):
// MyComponent.vue
const srcRef = toRef(props.src) ❌
const srcRef = toRef(props, 'src') ✅
Note: It's also valid to use const srcRef = computed(() => props.src), as you were originally doing.
watch's first argument is a WatchSource. When WatchSource is a function dealing with a ref, it should return the ref's unwrapped value. Alternatively, the WatchSource can be the ref itself:
// loadImage.js
watch(() => srcRef, /* callback */) ❌
watch(() => srcRef.value, /* callback */) ✅
watch(srcRef, /* callback */) ✅
The composable receives the image source in a ref, and your onMounted() hook is passing that ref to loadImage(), which is actually expecting the string in the ref's unwrapped value:
// loadImage.js
onMounted(() => {
if (src) { ❌ /* src is a ref in this composable */
loadImage(src)
}
})
onMounted(() => {
if (src.value) { ✅
loadImage(src.value)
}
})
demo

Vuex + Jest + Composition API: How to check if an action has been called

I am working on a project built on Vue3 and composition API and writing test cases.
The component I want to test is like below.
Home.vue
<template>
<div>
<Child #onChangeValue="onChangeValue" />
</div>
</template>
<script lang="ts>
...
const onChangeValue = (value: string) => {
store.dispatch("changeValueAction", {
value: value,
});
};
</scirpt>
Now I want to test if changeValueAction has been called.
Home.spec.ts
...
import { key, store } from '#/store';
describe("Test Home component", () => {
const wrapper = mount(Home, {
global: {
plugins: [[store, key]],
},
});
it("Test onChangeValue", () => {
const child = wrapper.findComponent(Child);
child.vm.$emit("onChangeValue", "Hello, world");
// I want to check changeValueAction has been called.
expect(wrapper.vm.store.state.moduleA.value).toBe("Hello, world");
});
});
I can confirm the state has actually been updated successfully in the test case above but I am wondering how I can mock action and check if it has been called.
How can I do it?
I have sort of a similar setup.
I don't want to test the actual store just that the method within the component is calling dispatch with a certain value.
This is what I've done.
favorite.spec.ts
import {key} from '#/store';
let storeMock: any;
beforeEach(async () => {
storeMock = createStore({});
});
test(`Should remove favorite`, async () => {
const wrapper = mount(Component, {
propsData: {
item: mockItemObj
},
global: {
plugins: [[storeMock, key]],
}
});
const spyDispatch = jest.spyOn(storeMock, 'dispatch').mockImplementation();
await wrapper.find('.remove-favorite-item').trigger('click');
expect(spyDispatch).toHaveBeenCalledTimes(1);
expect(spyDispatch).toHaveBeenCalledWith("favoritesState/deleteFavorite", favoriteId);
});
This is the Component method:
setup(props) {
const store = useStore();
function removeFavorite() {
store.dispatch("favoritesState/deleteFavorite", favoriteId);
}
return {
removeFavorite
}
}
Hope this will help you further :)

Reset Vue for every jest test?

I am using Vue JS with #vue/test-utils and jest. For my tests I am calling:
let localVue = createLocalVue();
vueMount(MyComponent, { localVue: localVue, options });
The problem is, I am referencing libraries which does stuff like this:
import Vue from 'vue'
import Msal from 'vue-msal'
//...
Vue.use(Msal, {...});
The Vue.use() registers some global stuff on the prototype, etc. For testing purposes, I need this to start fresh each test. The only thing I could think of is to use mockImplementation() with jest on the Vue object. But I am not quite sure how I could accomplish that, if at all possible.
Is there any way to do this? Thanks!
It took me a while, but here is how I solved this...
let setupComplete = false;
let setupFailure = false;
let testContext = {};
function resetTestContext() {
Object.keys(testContext).forEach(function(key) { delete testContext[key]; });
}
function createTestContext(configureTestContext) {
beforeEach(() => {
jest.isolateModules(() => {
setupFailure = true;
jest.unmock('vue');
resetTestContext();
testContext.vueTestUtils = require('#vue/test-utils');
testContext.vue = testContext.vueTestUtils.createLocalVue();
jest.doMock('vue', () => testContext.vue);
testContext.vuetify = require('vuetify');
testContext.vue.use(testContext.vuetify);
testContext.vuetifyInstance = new testContext.vuetify();
if (configureTestContext) {
configureTestContext(testContext);
}
setupComplete = true;
setupFailure = false;
});
});
afterEach(() => {
setupComplete = false;
resetTestContext();
jest.resetModules();
setupFailure = false;
});
return testContext;
},
What made this possible was the jest.isolateModules() method. With this approach, Vue and it's prototype, and also Vuetify, are completely recreated and brand new with each test case.
For it to work, the test spec and the library containing the utility above may not 'import' Vue or any module which depends on Vue. Instead, it needs to be required in the configureTestContext() function or in the test case itself.
My test specs look like this:
import createTestContext from '#/scripts/createTestContext'
describe('sample', () => {
const testContext = createTestContext(function configureTestContext(testContext)
{
testContext.vueDependency = require('#/scripts/vueDependency').default;
});
test('demo', () => {
// I added a helper to more easily do this in the test context...
const sample = testContext.vueTestUtils.mount(require('#/components/Sample').default, {
localVue: testContext.vue,
vuetify: testContext.vuetifyInstance
});
expect(testContext.vueDependency.doSomething(sample)).toBe(true);
expect(sample.isVueInstance()).toBeTruthy();
});
});
import { shallowMount, createLocalVue } from '#vue/test-utils';
import Vuex from 'vuex';
const localVue = createLocalVue();
const { user } = require('./customer.mock');
const originUser = { ...user };
const resetUserData = wrapper => {
wrapper.setData( { userData: originUser } );
};
const TestComponent = localVue.component( 'TestComponent', {
name : 'TestComponent',
data : () => {
return { userData: user };
},
render( createElement ) {
return createElement( 'h3', 'hoy hoy' );
},
} );
describe( 'computed fields', () => {
afterEach( () => {
resetUserData( wrapper );
} );
it( 'isPrivatePerson should return false', () => {
wrapper.setData( { userData: { Contacts: [{ grpid: 'bad field' }] } } );
expect( !wrapper.vm.isPrivatePerson ).toBeTruthy();
} );
it( 'isPrivatePerson should return true', () => {
expect( wrapper.vm.isPrivatePerson ).toBeTruthy();
} );
});