http requests in stenciljs - xmlhttprequest

How can I make http requests (GET / POST / PUT / DELETE) in StencilJS?
I tried using axios as follows: Did npm install axios --save and in the stencil component import axios from 'axios';. As soon as I call axios.get(...)I get the following error message:
[ ERROR ] bundling: node_modules/axios/lib/adapters/http.js, line: 4
A module cannot import itself
L3: var utils = require('./../utils');
L4: var settle = require('./../core/settle');
L5: var buildURL = require('./../helpers/buildURL');
I understand it might have to do with this issue: https://github.com/ionic-team/stencil/issues/98
However, any recommendations on how to get html requests work within a stencil component?

We can use the fetch API. It is browser native and does therefore not need an import. StencilJS also has a polyfill for it, so it works everywhere.
Thanks to #insanicae for pointing me to it.
Example:
import { Component, State } from '#stencil/core';
#Component({
tag: 'app-profile',
styleUrl: 'app-profile.css'
})
export class AppProfile {
#State() name: string;
componentWillLoad() {
fetch('https://api.github.com/users/ErvinLlojku')
.then((response: Response) => response.json())
.then(response => {
this.name = response['name'];
});
}
render() {
return [
<ion-header>
<ion-toolbar color="primary">
<ion-buttons slot="start">
<ion-back-button defaultHref="/" />
</ion-buttons>
<ion-title>Profile: {this.name}</ion-title>
</ion-toolbar>
</ion-header>,
<ion-content padding>
<p>My name is {this.name}.</p>
</ion-content>
];
}
}
Consult official docs of fetch for more. https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API

You can even use my component to use Fetch API, just drop the URL and listen for the OK or KO event.

Related

Testing with vitest and testing-library is not working: it is due to using the SFC Script Setup?

I'm new to Vue and especially with the composition functions. I'm trying to test a component that uses the script setup; however, it seems that it is not working.
The component is this one:
<template>
<el-card class="box-card" body-style="padding: 38px; text-align: center;" v-loading="loading">
<h3>Login</h3>
<hr class="container--separator">
<el-form ref="formRef"
:model="form"
>
<el-form-item label="Username">
<el-input v-model="form.username" placeholder="Username"/>
</el-form-item>
<el-form-item label="Password">
<el-input type="password" v-model="form.password" placeholder="Password" />
</el-form-item>
<el-button color="#2274A5" v-on:click="submitForm()">Login</el-button>
</el-form>
</el-card>
</template>
<script lang="ts" setup>
import {reactive, ref} from 'vue'
import { useRouter } from 'vue-router'
import type {FormInstance} from 'element-plus'
import {useMainStore} from "../../stores/index"
import notification from "#/utils/notification"
import type User from "#/types/User"
const formRef = ref<FormInstance>()
const form: User = reactive({
username: "",
password: "",
})
const router = useRouter()
const loading = ref(false)
const submitForm = (async() => {
const store = useMainStore()
if (form.username === "") {
return notification("The username is empty, please fill the field")
}
if (form.password === "") {
return notification("The password is empty, please fill the field")
}
loading.value = true;
await store.fetchUser(form.username, form.password);
loading.value = false;
router.push({ name: "home" })
})
</script>
<style lang="sass" scoped>
#import "./LoginCard.scss"
</style>
When I try to test it:
import { test } from 'vitest'
import {render, fireEvent} from '#testing-library/vue'
import { useRouter } from 'vue-router'
import LoginCard from '../LoginCard/LoginCard.vue'
test('login works', async () => {
render(LoginCard)
})
I had more lines but just testing to render the component gives me this error.
TypeError: Cannot read properties of undefined (reading 'deep')
❯ Module.withDirectives node_modules/#vue/runtime-core/dist/runtime-core.cjs.js:3720:17
❯ Proxy._sfc_render src/components/LoginCard/LoginCard.vue:53:32
51| loading.value = false;
52|
53| router.push({ name: "home" });
I tried to comment parts of the component to see if it was an issue with a specific line (the router for example), but the problem seems to continue.
I tried to search about it but I don't know what I'm doing wrong, it is related to the component itself? Should I change how I've done the component?
I had the same issue, and was finally able to figure it out. Maybe this will help you.
The problem was I had to register global plugins used by my component when calling the render function.
I was trying to test a component that used a directive registered by a global plugin. In my case, it was maska, and I used the directive in a input that was rendered somewhere deeply nested inside my component, like so:
<!-- a global directive my component used -->
<input v-maska="myMask" .../>
#vue/test-utils didn't recognize it automatically, which caused the issue. To solve it, I had to pass the used plugin in a configuration parameter of the render() function:
import Maska from 'maska';
render(MyComponent, {
global: {
plugins: [Maska]
}
})
Then, the issue was gone. You can find more info about render()
configuration here:
https://test-utils.vuejs.org/api/#global

How to call a dynamically created/ mounted global component in Quasar2 (Vue3 - Options API)

I am not exactly sure how to phrase this question. I am attempting to port one of my Vue2 global components to Vue3 (Options API). In short, I am trying to mount a custom dialog component globally, i.e. creating it in the boot folder (And have added it to quasar.conf.js boot section) and then be able to call it in any of my Vue files in my project, without having to import it in every vue file i.e. similar to this.$axios.
The original plug-ins file using an earlier version of Vue was similar to
import Vue from 'vue';
import dialog from '#/components/dialog.vue';
export default ({ app }, inject) => {
const $dialog = Vue.extend( dialog );
const vm = new $dialog({ i18n: app.i18n }).$mount();
document.body.appendChild(vm.$el);
inject('dialog', vm);
};
I found my answer to work around the deprecation of Vue.extend here and used a component that implements the suggested solution by pearofducks on github, which appears to mount the component correctly.
Back to my boot file
boot_dialog.js
import { boot } from 'quasar/wrappers'
import { mount } from 'mount-vue-component'
import customDialog from '../~global/scripts/debug/debug'
export default boot( async ({ app }) => {
const element = document.createElement('div');
element.setAttribute('id','modal-dialog-div');
document.body.appendChild(element);
const { vNode, destroy, el } = mount(customDialog, { app, element, props: { i18n: app.i18n }});
app.config.globalProperties.$dialog = customDialog;
// ^^^ Believe the error is here
});
I know that the component gets mounted as on mount in debug.vue I call the this.show() which displays the q-dialog correctly.
devug.vue
<template>
<q-dialog v-model="alert">
<q-card>
<q-card-section>
<div class="text-h6">Alert test</div>
</q-card-section>
<q-card-section class="q-pt-none">
Lorem ipsum dolor sit amet
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="OK" color="primary" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script>
export default {
data () {
return {
alert: false,
}
},
methods: {
show (params) {
console.log('Show method called',params);
this.alert = true;
},
},
mounted() {
this.show('Parameters');
}
};
</script>
But calling this.$dialog.show('A sample parameter') in any of my main project vue files causes an error. How do I work back from the vNode const to an actual component which I can call in any of the projects vue files ?
I think what you want is a custom plugin:
https://v3.vuejs.org/guide/plugins.html#writing-a-plugin

Vue 3 Vite - dynamic image src

I'm using Vue 3 with Vite. And I have a problem with dynamic img src after Vite build for production. For static img src there's no problem.
<img src="/src/assets/images/my-image.png" alt="Image" class="logo"/>
It works well in both cases: when running in dev mode and after vite build as well. But I have some image names stored in database loaded dynamically (Menu icons). In that case I have to compose the path like this:
<img :src="'/src/assets/images/' + menuItem.iconSource" />
(menuItem.iconSource contains the name of the image like "my-image.png").
In this case it works when running the app in development mode, but not after production build. When Vite builds the app for the production the paths are changed (all assests are put into _assets folder). Static image sources are processed by Vite build and the paths are changed accordingly but it's not the case for the composed image sources. It simply takes /src/assets/images/ as a constant and doesn't change it (I can see it in network monitor when app throws 404 not found for image /src/assets/images/my-image.png).
I tried to find the solution, someone suggests using require() but I'm not sure vite can make use of it.
Update 2022: Vite 3.0.9 + Vue 3.2.38
Solutions for dynamic src binding:
1. With static URL
<script setup>
import imageUrl from '#/assets/images/logo.svg' // => or relative path
</script>
<template>
<img :src="imageUrl" alt="img" />
</template>
2. With dynamic URL & relative path
<script setup>
const imageUrl = new URL(`./dir/${name}.png`, import.meta.url).href
</script>
<template>
<img :src="imageUrl" alt="img" />
</template>
3.With dynamic URL & absolute path
Due to Rollup Limitations, all imports must start relative to the importing file and should not start with a variable.
You have to replace the alias #/ with /src
<script setup>
const imageUrl = new URL('/src/assets/images/logo.svg', import.meta.url)
</script>
<template>
<img :src="imageUrl" alt="img" />
</template>
2022 answer: Vite 2.8.6 + Vue 3.2.31
Here is what worked for me for local and production build:
<script setup>
const imageUrl = new URL('./logo.png', import.meta.url).href
</script>
<template>
<img :src="imageUrl" />
</template>
Note that it doesn't work with SSR
Vite docs: new URL
Following the Vite documentation you can use the solution mentioned and explained here:
vite documentation
const imgUrl = new URL('./img.png', import.meta.url)
document.getElementById('hero-img').src = imgUrl
I'm using it in a computed property setting the paths dynamically like:
var imagePath = computed(() => {
switch (condition.value) {
case 1:
const imgUrl = new URL('../assets/1.jpg',
import.meta.url)
return imgUrl
break;
case 2:
const imgUrl2 = new URL('../assets/2.jpg',
import.meta.url)
return imgUrl2
break;
case 3:
const imgUrl3 = new URL('../assets/3.jpg',
import.meta.url)
return imgUrl3
break;
}
});
Works perfectly for me.
The simplest solution I've found for this is to put your images in the public folder located in your directory's root.
You can, for example, create an images folder inside the public folder, and then bind your images dynamically like this:
<template>
<img src:="`/images/${ dynamicImageName }.jpeg`"/>
</template>
Now your images should load correctly in both development and production.
Please try the following methods
const getSrc = (name) => {
const path = `/static/icon/${name}.svg`;
const modules = import.meta.globEager("/static/icon/*.svg");
return modules[path].default;
};
In the context of vite#2.x, you can use new URL(url, import.meta.url) to construct dynamic paths. This pattern also supports dynamic URLs via template literals.
For example:
<img :src="`/src/assets/images/${menuItem.iconSource}`" />
However you need to make sure your build.target support import.meta.url. According to Vite documentation, import.meta is a es2020 feature but vite#2.x use es2019 as default target. You need to set esbuild target in your vite.config.js:
// vite.config.js
export default defineConfig({
// ...other configs
optimizeDeps: {
esbuildOptions: {
target: 'es2020'
}
},
build: {
target: 'es2020'
}
})
All you need is to just create a function which allows you to generate a url.
from vite documentation static asset handling
const getImgUrl = (imageNameWithExtension)=> new URL(`./assets/${imageNameWithExtension}`, import.meta.url).href;
//use
<img :src="getImgUrl(image)" alt="...">
Use Vite's API import.meta.glob works well, I refer to steps from docs of webpack-to-vite. It lists some conversion items and error repair methods. It can even convert an old project to a vite project with one click. It’s great, I recommend it!
create a Model to save the imported modules, use async methods to dynamically import the modules and update them to the Model
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
const assets = import.meta.glob('../assets/**')
Vue.use(Vuex)
export default new Vuex.Store({
state: {
assets: {}
},
mutations: {
setAssets(state, data) {
state.assets = Object.assign({}, state.assets, data)
}
},
actions: {
async getAssets({ commit }, url) {
const getAsset = assets[url]
if (!getAsset) {
commit('setAssets', { [url]: ''})
} else {
const asset = await getAsset()
commit('setAssets', { [url]: asset.default })
}
}
}
})
use in .vue SFC
// img1.vue
<template>
<img :src="$store.state.assets['../assets/images/' + options.src]" />
</template>
<script>
export default {
name: "img1",
props: {
options: Object
},
watch: {
'options.src': {
handler (val) {
this.$store.dispatch('getAssets', `../assets/images/${val}`)
},
immediate: true,
deep: true
}
}
}
</script>
My enviroment:
vite v2.9.13
vue3 v3.2.37
In vite.config.js, assign #assets to src/assets
'#assets': resolve(__dirname, 'src/assets')
Example codes:
<template>
<div class="hstack gap-3 mx-auto">
<div class="form-check border" v-for="p in options" :key="p">
<div class="vstack gap-1">
<input class="form-check-input" type="radio" name="example" v-model="selected">
<img :src="imgUrl(p)" width="53" height="53" alt="">
</div>
</div>
</div>
</template>
<script>
import s1_0 from "#assets/pic1_sel.png";
import s1_1 from "#assets/pic1_normal.png";
import s2_0 from "#assets/pic2_sel.png";
import s2_1 from "#assets/pic2_normal.png";
import s3_0 from "#assets/pic3_sel.png";
import s3_1 from "#assets/pic3_normal.png";
export default {
props: {
'options': {
type: Object,
default: [1, 2, 3, 4]
}
},
data() {
return {
selected: null
}
},
methods: {
isSelected(val) {
return val === this.selected;
},
imgUrl(val) {
let isSel = this.isSelected(val);
switch(val) {
case 1:
case 2:
return (isSel ? s1_0 : s1_1);
case 3:
case 4:
return (isSel ? s2_0 : s2_1);
default:
return (isSel ? s3_0 : s3_1);
}
}
}
}
</script>
References:
Static Asset Handling of Vue3
Memo:
About require solution.
"Cannot find require variable" error from browser. So the answer with require not working for me.
It seems nodejs >= 14 no longer has require by default. See this thread. I tried the method, but my Vue3 + vite give me errors.
In Nuxt3 I made a composable that is able to be called upon to import dynamic images across my app. I expect you can use this code within a Vue component and get the desired effect.
const pngFiles = import.meta.glob('~/assets/**/*.png', {
//#ts-ignore
eager: true,
import: 'default',
})
export const usePNG = (path: string): string => {
// #ts-expect-error: wrong type info
return pngFiles['/assets/' + path + '.png']
}
sources
If you have a limited number of images to use, you could import all of them like this into your component. You could then switch them based on a prop to the component.

import and call function within Method not working

I am creating a small VueJs app, that calls the NASA image API and displays on the screen.
In the Header component (below) I have a search bar, when clicked this will call the Axios method defined in another file, I understood from the documents that if import functions to component then they need to be defined in the 'methods'. However, when I click search nothing displays on the console?
note: the call to NASA has been tested and does work when I include in the component. Which I guess begs the question if I should leave it in the component as I won't use it elsewhere.
but would still like to understand the logic behind the issue.
component code:
<template>
<div>
<h1>Nasa Image Search</h1>
<div class="search-container">
<form action="/action_page.php">
<input type="text" placeholder="Search.." name="search" />
<button v-on:click="search" type="submit">Search</button>
</form>
</div>
</div>
</template>
<script>
import nasa from '../apiCall'
export default {
name: 'Header',
methods: {
search : function(){
nasa
}
}
}
</script>
Axios function call:
import axios from 'axios'
const nasa = () => {
var url = `https://images-api.nasa.gov/search?q=apollo-13&media_type=image`
console.log(url) //bug testing
axios
.get(url)
.then(function(response) {
// handle success
console.log(response)
})
.catch(function(error) {
// handle error
console.error(error)
})
}
export default { nasa }
Your default export is actually an object with a function property, like this:
{
nasa: () => { ... }
}
When you import the object, you give it the name nasa, so you'd actually have to call the function like:
nasa.nasa()
Since you probably intend to just export the function, leave your import as is but change your export to:
export default nasa; // no brackets
And in your component, you don't need to embed that in a method, you can set it directly to the search method:
methods: {
search: nasa
}

Gridsome Full Calendar build error - no SSR

I'm trying to use the Full Calendar vue component (https://github.com/fullcalendar/fullcalendar-vue) in a Gridsome project like so:
<template>
<div class="tabStaffManage">
<div>
<FullCalendar
ref="staffCalendar"
class="fullCalendar"
defaultView="dayGridMonth"
:events="calendarEvents"
:plugins="calendarPlugins"
:allDaySlot="false"
:header="{
center: 'dayGridMonth, timeGridDay',
right: 'prev, next'
}"
minTime="09:00:00"
:selectable="true"
maxTime="18:30:00"
#eventClick="onEventClick"
#select="onDateSelect"
:showNonCurrentDates="false"
></FullCalendar>
</div>
</div>
</template>
<script>
import { formatDate } from "#fullcalendar/core"
import FullCalendar from "#fullcalendar/vue"
import timeGridPlugin from "#fullcalendar/timegrid"
import dayGridPlugin from "#fullcalendar/daygrid"
import interactionPlugin from "#fullcalendar/interaction"
export default {
components: {
FullCalendar,
},
data() {
return {
calendarPlugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
}
},
}
</script>
This, however, produces an error on build:
Could not generate HTML for "/staff/dashboard/":
ReferenceError: Element is not defined
at Object.338 (node_modules/#fullcalendar/core/main.esm.js:102:0)
at __webpack_require__ (webpack/bootstrap:25:0)
at Module.552 (assets/js/page--src-pages-staff-dashboard-vue.ea5234e7.js:598:16)
at __webpack_require__ (webpack/bootstrap:25:0)
I understand that Full Calendar does not support SSR. So as per the Gridsome documentation (https://gridsome.org/docs/assets-scripts/#without-ssr-support) I did this to import the component:
I created an alias for it's dependencies in gridsome.config.js like so:
var path = require('path');
api.configureWebpack({
resolve: {
alias: {
"timeGridPlugin": path.resolve('node_modules', '#fullcalendar/timegrid'),
etc....
}
},
})
and required those plugins in the mounted() lifecycle hook:
mounted() {
if (!process.isClient) return
let timeGridPlugin = require('timeGridPlugin')
...
},
components: {
FullCalendar: () =>
import ('#fullcalendar/vue')
.then(m => m.FullCalendar)
.catch(),
}
I then wrapped the FullCalendar component in:
<ClientOnly>
<FullCalendar></FullCalendar>
</ClientOnly>
The extra dependencies required in the mounted() hook are included no problem.
However I now get the following error:
TypeError: Cannot read property '__esModule' of undefined
It seems that components() is failing to import the '#fullcalendar/vue' component.
Am I doing something wrong when importing the '#fullcalendar/vue' component?
Is there another way to include both the '#fullcalendar/vue' component and the plugin dependencies with no SSR?
Requiring the full calendar vue component in main.js by checking the gridsome client API and registering the component globally in vue seems to work and does what I expected:
// Include no SSR
if (process.isClient) {
const FullCalendar = require("#fullcalendar/vue").default
Vue.component("full-calendar", FullCalendar)
}
I also was not pointing to the default object when requiring the other modules in the component:
mounted() {
if (!process.isClient) return
let timeGridPlugin = require('timeGridPlugin').default
...
}