Svelte/SvelteKit: Dynamic import of components with variable - import

I want to dynamically import components without importing a specific component.
I want to set the component name with a variable, received from the store:
<script lang="ts">
// SVELTE
import { onMount } from 'svelte';
// STORE
import { dynamicComponent } from '$stores/dynamicTitle';
$: $dynamicComponent;
console.log($dynamicComponent)
let renderDynamicComponent
onMount(async () => {
const importValue = (await import(`../../lib/components/Home/DynamicComponents/${String($dynamicComponent)}.svelte`)).default;
// const importValue = (await import(`../../lib/components/Home/DynamicComponents/IntroSectionCustom.svelte`)).default;
renderDynamicComponent = importValue
});
<svelte:component this={renderDynamicComponent}/>
But I get:
Uncaught (in promise) TypeError: Failed to fetch dynamically imported module: http://localhost:3000/src/lib/components/Home/DynamicComponents/Intro-Section-Custom.svelte
I do not understand. From the error, it seems to be the right path ...

The Rollup plugin #rollup/plugin-dynamic-import-vars might be of help here. I haven't used it with SvelteKit specifically, but it worked fine with standard Svelte with Vite as bundler.
// Example.svelte
function importLocale(locale) {
return import(`./locales/${locale}.js`);
}
// vite.config.js
import dynamicImportVars from '#rollup/plugin-dynamic-import-vars';
export default (mode) =>
defineConfig({
plugins: [
dynamicImportVars({
include: './src/Example.svelte'
})
]
});
SvelteKit uses Vite behind the scenes, but has its own configuration format. In svelte.config.js, pass dynamicImportVars() to the config.vite.plugins key:
// svelte.config.js
/** #type {import('#sveltejs/kit').Config} */
const config = {
vite: {
plugins: [
dynamicImportVars({
include: './src/Example.svelte'
})
]
}
};
export default config;
Please take note of the limitations mentioned in the README of the Rollup plugin.

I don't think Svelte + the bundler currently support dynamically generated paths:
let thing = 'Thing';
Thing = (await import(`./${thing}.svelte`)).default; // this won't work
Thing = (await import(`./Thing.svelte`)).default; // this will work
limitation of the bundler.
github issue: https://github.com/sveltejs/svelte/issues/6702

What you're doing does work if not using import vars. When adding an import variable you need make your renderDynamicComponent identifier reactive. So instead of this:
let renderDynamicComponent
Do this:
$: renderDynamicComponent = null
This will allow svelte:component to render your imported component with dynamic path variable. This seems to be a special case when using dynamic import vars with Vite.

Related

How to gracefully handle errors in responses on a component level with Axios and Vue 3?

I am sorry if it is obvious/well-covered elsewhere, but my google-fu has been failing me for over a full day by now. What I would like to achieve is a rich component-level handling of request errors: toaster notifications, status bars, you name it. The most obvious use case is auth guards/redirects, but there may be other scenarios as well (e.g. handling 500 status codes). For now, app-wide interceptors would do, but there is an obvious (to me, at least) benefit in being able to supplement or override higher-level interceptors. For example, if I have interceptors for 403 and 500 codes app-wide, I might want to override an interceptor for 403, but leave an interceptor for 500 intact on a component level.
This would require access to component properties: I could then pass status messages in child components, create toaster notifications with custom timeouts/animations and so on. Naively, for my current app-wide problem, this functionality belongs in App.vue, but I can not figure out how to get access to App in axios.interceptors.response using the current plugin arrangement and whether it is okay to use a single axios instance app-wide in the first place.
The trimmed down code I have tried so far (and which seems the most ubiquitous implementation found online) can be found below. It works with redirects, producing Error: getTranslators: detection is already running in the process (maybe because another 401 happens right after redirect with my current testing setup). However, import Vue, both with curly brackets and without, fails miserably, and, more importantly, I have no way of accessing app properties and child components from the plugin.
// ./plugins/axios.js
import axios from 'axios';
import { globalStorage } from '#/store.js';
import router from '../router';
// Uncommenting this import gives Uncaught SyntaxError: ambiguous indirect export: default.
// Circular dependency?..
// import Vue from 'vue';
const api = axios.create({
baseURL: import.meta.env.VUE_APP_API_URL,
});
api.interceptors.response.use(response => response,
error => {
if (error.response.status === 401) {
//Vue.$toast("Your session has expired. You will be redirected shortly");
delete globalStorage.userInfo;
localStorage.setItem('after_login', router.currentRoute.value.name);
localStorage.removeItem('user_info');
router.push({ name: 'login' });
}
return Promise.reject(error);
});
export default api;
// --------------------------------------------------------------------------
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import axios from './plugins/axios'
import VueAxios from 'vue-axios'
const app = createApp(App)
app.use(router)
  .use(VueAxios, axios)
  .mount('#app')
So, then, how do I get access to component properties in interceptors? If I need them to behave differently for different components, would I then need multiple axios instances (assuming the behavior is not achieved by pure composition)? If so, where to put the relevant interceptor configuration and how to ensure some parts of global configuration such as baseURL apply to all of these instances?
I would prefer not having more major external dependencies such as Vuex as a complete replacement for the existing solution, but this is not a hill to die on, of course.
Instead of using axios's interceptors, you should probably create a composable. Consider the following:
composables/useApiRequest.js
import axios from 'axios';
import { useToast } from "vue-toastification";
const useApiRequest = () => {
const toast = useToast();
const fetch = async (url) => {
try {
await axios.get(url);
} catch (error) {
if (error.response.status === 403) {
toast.error("Your session has expired", {
timeout: 2000
});
}
}
};
return {
fetch,
};
};
export default useApiRequest;
Here we're creating a composable called useApiRequest that serves as our layer for the axios package where we can construct our api requests and create generic behaviors for certain response attributes. Take note that we can safely use Vue's Composition API functions and also components such as the vue-toastification directly in this composable:
if (error.response.status === 403) {
toast.error("Your session has expired", {
timeout: 2000
});
}
We can import this composable in the component and use the fetch function to send a GET request to whatever url that we supply:
<script setup>
import { ref } from 'vue';
import useApiRequest from '../composables/useApiRequest';
const searchBar = ref('');
const request = useApiRequest();
const retrieveResult = async () => {
await request.fetch(`https://api.ebird.org/v2/data/obs/${searchBar.value}/recent`);
}
</script>
And that's it! You can check the example here.
Now, you mentioned that you want to access component properties. You can accomplish this by letting your composable accept arguments containing the component properties:
// `props` is our component props
const useApiRequest = (props) => {
// add whatever logic you plan to implement for the props
}
<script setup>
import { ref } from 'vue';
import useApiRequest from '../composables/useApiRequest';
import { DEFAULT_STATUS } from '../constants';
const status = ref(DEFAULT_STATUS);
const request = useApiRequest({ status });
</script>
Just try to experiment and think of ways to make the composable more reusable for other components.
Note
I've updated the answer to change "hook" to "composable" as this is the correct term.

SvelteKit With MongoDB ReferenceError: global is not defined

I'm trying to setup MongoDB connection library function. I know this function is solid, its used in a whole lot of places (search for Global is used here to maintain a cached connection across hot reloads) and you'll find a whole lot of uses including next.js releases. Note, the purpose of global storage for the database connection is to reduce the overall # of db connections in use at any one time.
What I'm not understanding is the error I'm getting when I import this library via import { connectToDatabase } from '$lib/database';
database.js
// https://github.com/mongodb-developer/mongodb-next-todo/blob/main/util/mongodb.js
import { ENV_OBJ } from "$lib/env";
import { MongoClient } from "mongodb";
const uri = ENV_OBJ.MONGODB_URI;
if (!uri) {
throw new Error("Please define the Mongodb uri environment variable inside .env");
}
/**
* Global is used here to maintain a cached connection across hot reloads
* in development. This prevents connections growing exponentially
* during API Route usage.
*/
let cached = global.mongo
if (!cached) {
cached = global.mongo = { conn: null, promise: null }
}
export const connectToDatabase = async() => {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const options = {
useNewUrlParser: true,
useUnifiedTopology: true
};
cached.promise = MongoClient.connect(MONGODB_URI, opts).then((client) => {
return {
client,
db: client.db(MONGODB_DB),
}
})
}
cached.conn = await cached.promise;
return cached.conn;
}
The errors:
global is not defined
ReferenceError: global is not defined
at node_modules/mongodb/lib/promise_provider.js (http://localhost:3000/node_modules/.vite/mongodb.js?v=3885e04e:548:25)
at __require2 (http://localhost:3000/node_modules/.vite/chunk-6ODJH7E3.js?v=3885e04e:10:44)
at node_modules/mongodb/lib/utils.js (http://localhost:3000/node_modules/.vite/mongodb.js?v=3885e04e:6524:30)
at __require2 (http://localhost:3000/node_modules/.vite/chunk-6ODJH7E3.js?v=3885e04e:10:44)
at node_modules/mongodb/lib/cursor/abstract_cursor.js (http://localhost:3000/node_modules/.vite/mongodb.js?v=3885e04e:10873:19)
at __require2 (http://localhost:3000/node_modules/.vite/chunk-6ODJH7E3.js?v=3885e04e:10:44)
at node_modules/mongodb/lib/index.js (http://localhost:3000/node_modules/.vite/mongodb.js?v=3885e04e:25281:29)
at __require2 (http://localhost:3000/node_modules/.vite/chunk-6ODJH7E3.js?v=3885e04e:10:44)
at http://localhost:3000/node_modules/.vite/mongodb.js?v=3885e04e:25616:23
Note, I do see a file in my generated minimal sveltekit repo called global.d.ts I'm not sure of its purpose. It contains only:
/// <reference types="#sveltejs/kit" />
Any ideas on what's causing the error?
Reference: "#sveltejs/kit": "version": "1.0.0-next.118",
Edit: After spending a whole lot of time on this issue, the global not defined error seems to come from import { MongoClient } from "mongodb"; If I add appropriate console.logs, I can see that the MongoClient function works fine on the server, but then I get the global error on the client. The server indicates no errors at all.
So it turns out I was calling import { connectToDatabase } from '$lib/database' not in a .js helper file or api style (.js) endpoints. I was attempting to use that import and make a database call directly from the <script> portion of a xxx.svelte file.
Definite no go. That generates an immediate global not defined error.

What's the equivalent of Angular Service in VueJS?

I want to put all my functions that talk to the server and fetch data into a single reusable file in VueJS.
Plugins don't seem to be the best alternative. Template less components..?
In total there are 4 ways:
Stateless service: then you should use mixins
Stateful service: use Vuex
Export service and import from a vue code
any javascript global object
I am using axios as HTTP client for making api calls, I have created a gateways folder in my src folder and I have put files for each backend, creating axios instances, like following
myApi.js
import axios from 'axios'
export default axios.create({
baseURL: 'http://localhost:3000/api/v1',
timeout: 5000,
headers: {
'X-Auth-Token': 'f2b6637ddf355a476918940289c0be016a4fe99e3b69c83d',
'Content-Type': 'application/json'
}
})
Now in your component, You can have a function which will fetch data from the api like following:
methods: {
getProducts () {
myApi.get('products?id=' + prodId).then(response => this.product = response.data)
}
}
As I assume you want to re-use this method in multiple components, you can use mixins of vue.js:
Mixins are a flexible way to distribute reusable functionalities for Vue components. A mixin object can contain any component options. When a component uses a mixin, all options in the mixin will be “mixed” into the component’s own options.
So you can add a method in mixin and it will be available in all the components, where mixin will be mixed. See following example:
// define a mixin object
var myMixin = {
methods: {
getProducts () {
myApi.get('products?id=' + prodId).then(response => this.product = response.data)
}
}
}
// define a component that uses this mixin
var Component = Vue.extend({
mixins: [myMixin]
})
// alternate way to have a mixin while initialising
new Vue({
mixins: [myMixin],
created: function () {
console.log('other code')
}
})
I'm using Vue Resource mostly.
1.I create new file where I do connection to API endpoint using Vue.http.xxx.So let's say we have endpoint that output the posts.Create new directory in your project, I call it services, and then create file called PostsService.js - content looks like this:
import Vue from 'vue'
export default {
get() {
return Vue.http.get('/api/posts)
}
}
Then I go to component where I want use this service, and import it
import PostsService from '../services/PostsService'
export default {
data() {
return {
items: []
}
},
created() {
this.fetchPosts()
},
methods: {
fetchPosts() {
return PostsService.get()
.then(response => {
this.items = response.data
})
}
}
}
For more info about this approach, feel free to check my repo on GitHub https://github.com/bedakb/vuewp/tree/master/public/app/themes/vuewp/app
I suggest creating an API Provider that you can access from anywhere in your app.
Simply create a src/utils folder and inside of it a file called api.js.
In it, export your wrapper that knows how to communicate with your API as an object or a ES6 static class (I prefer how the latter looks and works if you're not afraid of classes). This provider can use any HTTP request library that you like and you can easily swap it later by changing a single file (this one) instead of hunting down the whole codebase. Here's an example of using axios, assuming we have a REST API available at api.example.com/v1 that uses SSL:
import axios from 'axios'
import { isProduction, env } from '#/utils/env'
const http = null // not possible to create a private property in JavaScript, so we move it outside of the class, so that it's only accessible within this module
class APIProvider {
constructor ({ url }) {
http = axios.create({
baseURL: url,
headers: { 'Content-Type': 'application/json' }
})
}
login (token) {
http.defaults.headers.common.Authorization = `Bearer ${token}`
}
logout () {
http.defaults.headers.common.Authorization = ''
}
// REST Methods
find ({ resource, query }) {
return http.get(resource, {
params: query
})
}
get ({ resource, id, query }) {
return http.get(`${resource}/${id}`, {
params: query
})
}
create ({ resource, data, query }) {
return http.post(resource, data, {
params: query
})
}
update ({ resource, id, data, query }) {
return http.patch(`${resource}/${id}`, data, {
params: query
})
}
destroy ({ resource, id }) {
return http.delete(`${resource}/${id}`)
}
}
export default new APIProvider({
url: env('API_URL') // We assume 'https://api.example.com/v1' is set as the env variable
})
Next, in your main.js file or wherever else you bootstrap the Vue app, do the following:
import api from '#/src/utils/api'
Vue.$api = api
Object.defineProperty(Vue.prototype, '$api', {
get () {
return api
}
})
Now you can access it anywhere in your Vue app as well as anywhere you import Vue itself:
<template>
<div class="my-component">My Component</div
</template>
<script>
export default {
name: 'MyComponent',
data () {
return {
data: []
}
},
async created () {
const response = await this.$api.find({ resource: 'tasks', query: { page: 2 } })
this.data = response.data
}
}
</script>
or:
// actions.js from Vuex
import Vue from 'vue'
export async function fetchTasks ({ commit }) {
const response = await Vue.$api.find({ resource: 'tasks', query: { page: 2 } })
commit('SAVE_TASKS', response.data)
return response
}
Hope this helps.
I think for your simple question the answer could be any ES6 module containing functions (equivalent to methods in class in ANgular) and directly importing them in components using ES6 imports and exports. There are no such services that could be injected in components.
You can make your own service where you can place all your HTTP server calls and then import that to the components where you want to use them.
Best is to make use of Vuex for complex state management applications because in Vuex you are able to handle all async calls via actions which always run asynchronously and then commit the mutation once you have the result.Mutation will directly interact with the state and will update it in an immutable manner (which is preferred). This is stateful approach.
There are other approaches as well. But these are the ones which I follow in my code.

Ember - Sideloading data within RESTAPISerializer

I am trying to sideload data with RESTAPISerializer, but there seems to be some problem.
The goal is to store articles with tags. The articles appear in the store just fine, and I can look at them with Ember inspector. But I can't seem to figure out how to store the tags, they never show up in the inspector.
I created a model called blog-tag to store the tags. They come in a specific format, which I can't change, and it looks like this:
tid:term
... and I am getting an array of them with each article.
The article model includes a hasMany relationship with blog-tags. I am trying to pass them in through the serializer, and have been using various formats (JSON, javascript arrays, etc.) I wrote a custom serializer for blog tags as well, but it doesn't seem to do much.
Can someone explain what I am missing?
The article model:
import DS from 'ember-data';
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
title: attr(),
blogTags: hasMany('blog-tag', { async: true })
});
The blog-tag model:
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
tid: attr('number'),
term: attr('string'),
});
The article serializer:
import RESTAPISerializer from 'ember-data/serializers/rest';
export default RESTAPISerializer.extend({
normalizeResponse: function(store, primaryModelClass, payload, id, requestType) {
var normalizedPayload = {articles: []};
payload.forEach(function(item) {
var tags = item.tags.split("|"), blogTags = [];
// storing blog tags in an array, have also done it as a
// JSON structure
var blogTags = tags.map((tag) => {
var item = tag.split(":"),
vals = {};
vals.tid = item[0];
vals.term = item[1];
return vals;
});
var article = {
id: item.nid,
title: item.title,
blogTags: blogTags,
};
normalizedPayload.articles.push(article);
});
return this._super(store, primaryModelClass, normalizedPayload, id, requestType);
},
});
Solved my own problem. Answering the question for the benefit of anyone else running into the same issue.
What I was trying to do is embed the tags from the articles serializer. This mean I needed to use the EmbeddedRecordsMixin.
http://emberjs.com/api/data/classes/DS.EmbeddedRecordsMixin.html
It was actually pretty simple to do this. Made the following changes to the serializer described above, the code worked as-is with these additions.
// app/serializers/articles.js
import RESTSerializer from 'ember-data/serializers/rest';
import EmbeddedRecordsMixin from 'ember-data/serializers/embedded-records-mixin';
export default RESTSerializer.extend(EmbeddedRecordsMixin, {
attrs: {
blogTags: { embedded: 'always' }
}
});

dynamic segments is not working

I have setup the following route in router.js:
this.route('reports',{path:'/reports/:report_name'}, function(){
});
I have following link setup in on of the hbs file:
{{#link-to 'reports' 'eod-2015'}}EOD 2015{{/link-to}}
in app/route/reports/index.js
model: function(params) {
console.log(params);
}
and I am getting {} as a result, how can I find report_name in my index.js route in app/route/reports folder?
Instead of trying to access param in app/route/reports/index.js you should access it in app/route/reports.js.
See demo.
Comparison:
Inside reports.js file: Object {report_name: "eod-2015"}
Inside index.js file: Object {}
If you would like to access reports params inside reports.index route you could do:
model(params) {
const parentParams = this.paramsFor('reports');
console.log('Accessing reports.js from index.js:', parentParams);
}