I am building a simple website in which I have a route to category pages. I want to use a single dynamic route to move between pages.I am using vue-router for this project and the routes need to load different component
These are the desired routes for the website
example: '/shop/men' , '/shop/women','/shop/kids'
This my index.js file for router in which gender is appended in the last deciding which component to load the issue I am facing is how to handle this and load different component on depending on it
router-> index.js:
{
name: 'shop',
path: '/shop/:gender',
component: menCategoryViewsHandler('mencategory')
}
views -> viewHandler -> mencategory.js:
'use strict'
import Handle from '../mencategory.vue'
const camelize = str => str.charAt(0).toUpperCase() + str.slice(1)
// This is a factory function for dynamically creating root-level views,
// since they share most of the logic except for the type of items to display.
// They are essentially higher order components wrapping the respective vue file.
export default function ViewsHandler (type) {
console.log('1',type)
return {
name: `${type}-mencategory-view`,
asyncData ({store, route}) {
//#todo : add the ssr and routerbefore load change script here
return Promise.resolve({})
},
title: camelize(type),
render (h) {
return h(Handle,
{
props: {type},
},
)
},
}
}
You need to use dynamic route matching along with a wrapper component which renders the correct Category component. This would handled by passing props to components.
// CategoryResolver.vue
import menCategory from './mencategory'
import womenCategory from './womencategory'
import kidsCategory from './kidscategory'
const components = {
menCategory,
womenCategory,
kidsCategory
}
export default {
functional: true,
props: ['category'],
render(h, ctx) {
return h(`components[${category}Category`], ctx.data, ctx.children)
}
}
Then your router would be defined as such:
const router = new VueRouter({
routes: [
{ path: '/shop/:category', component: CategoryResolver, props: true }
]
})
Say menCategoryViewsHandler('mencategory') returns a component called MenCat. It must have a prop that matches the route above, in this example category. In MenCat you would define:
export default {
props: ['category'],
...
}
Vue router will pass the matching url prop into your component for you.
Related
I need to create a component in Vue JS dynamically on click and then route to that component. I am using Vue 3. Everything needs to happen in one click.
My code looks something like this
methods:{
routerClick(value){
console.log("number is "+value)
this.$router.push({path:'New', name:'New', component: ()=>Vue.component('New')})
}
},
I do not need to move a component that is already created. I want to create a component inside this method and then route to the component using this router. Please, any suggestions will be highly appreciated.
Below is a simplistic solution that works (I'm not an expert in Vue 3).
The main point is to use addRoute before pushing to it, because you cannot specify the route component when pushing to a route.
Here is the codesandbox with the working solution.
<template>
<router-link to="/">Home</router-link>
<button #click="createComponent">Create Component</button>
<router-view></router-view>
</template>
<script>
import { getCurrentInstance } from "vue";
import { useRouter } from "vue-router";
export default {
name: "App",
setup() {
const app = getCurrentInstance().appContext.app;
const router = useRouter();
const createComponent = () => {
// Check if the component has been alreadey registered
if (!app.component("NewComponent")) {
app.component("NewComponent", {
name: "NewComponent",
template: `<div>This is a new component</div>`
});
}
const newComponent = app.component("NewComponent");
// Adding a new route to the new component
router.addRoute({ path: "/new", component: newComponent });
router.push("/new");
};
return {
createComponent,
};
},
};
</script>
In Vue 3, I created the following Home component, 2 other components (Foo and Bar), and passed it to vue-router as shown below. The Home component is created using Vue's component function, whereas Foo and Bar components are created using plain objects.
The error that I get:
Component is missing template or render function.
Here, the Home component is causing the problem. Can't we pass the result of component() to a route object for vue-router?
<div id="app">
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/foo">Foo</router-link></li>
<li><router-link to="/bar">Bar</router-link></li>
</ul>
<home></home>
<router-view></router-view>
</div>
<script>
const { createRouter, createWebHistory, createWebHashHistory } = VueRouter
const { createApp } = Vue
const app = createApp({})
var Home = app.component('home', {
template: '<div>home</div>',
})
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar },
],
})
app.use(router)
app.mount('#app')
</script>
See the problem in codesandbox.
FOR vue-cli vue 3
render function missed in createApp.
When setting your app by using createApp function you have to include the render function that include App.
in main.js
update to :
FIRST
change the second line in javascript from:-
const { createApp } = Vue
to the following lines:
import { createApp,h } from 'vue'
import App from './App.vue'
SECOND
Change from :-
const app = createApp({})
to:
const app = createApp({
render: ()=>h(App)
});
app.mount("#app")
When app.component(...) is provided a definition object (the 2nd argument), it returns the application instance (in order to allow chaining calls). To get the component definition, omit the definition object and provide only the name:
app.component('home', { /* definition */ })
const Home = app.component('home')
const router = createRouter({
routes: [
{ path: '/', component: Home },
//...
]
})
demo
Make sure you have added <router-view></router-view> in your #app container.
The solution was simple on my side, I created a component that was empty, after filling in the template and a simple text HTML code, it was fixed.
The solution for me was to upgrade node module vue-loader to version 16.8.1.
I had this issue too. It's a timing issue. I added a v-if to create the component when the page is mounted. That fixed it for me.
<review-info
v-if="initDone"
:review-info="reviewInfo"
/>
// script
onMounted(() => {
initDone = true
})
I was extending a Quasar component in Vue 3, and ran into this problem. I solved it by adding the setup: QInput.setup line last in the component options.
<script>
import { defineComponent } from 'vue'
import { QInput } from 'quasar'
const { props } = QInput
export default defineComponent({
props: {
...props,
outlined: {
type: Boolean,
default: true
},
dense: {
type: Boolean,
default: true
},
uppercase: {
type: Boolean,
default: false
}
},
watch: {
modelValue (v) {
this.uppercase && this.$emit('update:modelValue', v.toUpperCase())
}
},
setup: QInput.setup
})
</script>
I've created a mixin to change the page titles, using document.title and global mixins.
My mixin file (title.ts):
import { Vue, Component } from 'vue-property-decorator'
function getTitle(vm: any): string {
const title: string = vm.title
if (title) {
return `${title} | site.com`
}
return 'Admin panel | site.com'
}
#Component
export default class TitleMixin extends Vue {
public created(): void {
const title: string = getTitle(this)
if (title) {
document.title = title
}
}
}
Then i registered this mixin globally in main.ts:
import titleMixin from '#/mixins/title'
Vue.mixin(titleMixin)
Then setting up the title in a Vue component:
#Component
export default class Login extends Vue {
public title: string = 'New title'
}
I have like 5 components in my project, if i use console.log in a mixin, i can see that it fired in every component, step by step, thus document.title is set by a last component created() hook.
How to correctly set a title for a CURRENT page?
As you said, a global mixin will affect every component in your Vue app, which means that the logic to set the document.title will fire in the created hook of every component in your app.
I think what you're looking for is VueRouter's beforeRouteEnter hook, which is one of the navigation guards that the library makes available to any of your components. A component's beforeRouteEnter hook fires immediately before the route changes to whichever one it's associated with.
In your case it would look like this:
#Component
export default class TitleMixin extends Vue {
public beforeRouteEnter(to, from, next): void {
next(vm => {
const title: string = getTitle(vm)
if (title) {
document.title = title
}
})
}
}
You'll notice that the next function (which needs to be called for the route to resolve) is being passed a callback which has a reference to the component's instance (vm), which we're passing to getTitle instead of this. This is necessary because the beforeRouteEnter hook does not have a reference to this. You can read the docs I linked to for more info.
Instead of creating a global mixin, try using a route meta field along with a global resolve guard.
First, we'll start by adding a meta field to each RouteConfig in the /router/routes.ts file:
import { RouteConfig } from 'vue-router'
export default [
{
path: '/login',
name: 'Login',
component: () => import(/* webpackChunkName: 'login-view' */ '#views/Login.vue'),
meta: {
title: 'Login', // Set the view title
},
},
// ... Add the title meta field to each `RouteConfig`
] as RouteConfig[]
Then, we'll create a global resolve guard, to set the title meta field as the document title, in the /router/index.ts file:
import Vue from 'vue'
import Router, { Route } from 'vue-router'
import routes from './routes'
Vue.use(Router)
const router = new Router({
// ... RouterOptions
})
// Before each route resolves...
// Resolve guards will be called right before the navigation is confirmed,
// after all in-component guards and async route components are resolved.
router.beforeResolve((routeTo, routeFrom, next) => {
const documentTitle = getRouteTitle(routeTo)
// If the `Route` being navigated to has a meta property and a title meta field,
// change the document title
if (documentTitle ) {
document.title = documentTitle
}
// Call `next` to continue...
next()
function getRouteTitle(route: Route): string {
const title: string = route.meta && route.meta.title
if (title) {
return `${title} | site.com`
}
return 'Admin panel | site.com'
}
})
export default router
You should use the mixin only in the parent component for your page (the one that holds all the page itself).
Using your vue-property-decorator should be in this way:
import { Vue, Component, Mixins } from 'vue-property-decorator';
#Component
export default class Login extends Mixins(titleMixin) {
public title: string = 'New title'
}
And do not import it globally with Vue.mixin(titleMixin). In this way it is imported for all the components.
So to explain clearly my problem, I have a component for each of my entities in my application like Author component and Book component. And for each of them I will have two child which is a list component and a form component.
So basically my route configuration look like this :
export const routing = RouterModule.forRoot([
{
path: 'author', component: AuthorComponent,
children: [
{ path: 'author-list', component: AuthorListComponent },
{ path: 'author-form', component: AuthorFormComponent }
]
},
{
path: 'book', component: BookComponent,
children: [
{ path: 'book-list', component: BookListComponent },
{ path: 'book-form', component: BookFormComponent }
]
}
]);
In my AuthorComponent for example I have a method to delete an author that call the service :
deleteBadge = (event): void => {
// Call delete service
this._badgeService.delete(event).subscribe(
result => {
// Good
},
error => {
// Error
}
My question is how can I call that method from my route child (author list or form component) knowing that I can't call it like a normal child component using event.
PS: I put method (and many other) in the parent because I need to access to it in both child components and so to avoid redundancy.
Standard practice is to use a shared service for Component Interaction. However, if you still want to avoid using a shared service, you can use the Injector API.
In your child component, AuthorListComponent for example, do the following:
import { Injector } from '#angular/core';
import {AuthorComponent} from "./author.component";
// ....
constructor(private injector:Injector){
let parentComponent = this.injector.get(AuthorComponent);
parentComponent.deleteBadge('String passed from AuthorListComponent');
}
Here is a link to working demo.
Use a communication Service which unites several communication observables.
An example can be found in the official Angular docs: https://angular.io/guide/component-interaction#parent-and-children-communicate-via-a-service
I'm really new to React and I can't figure out how to render a "loading..." screen when a route is being loaded with getComponent. The getComponent call works fine and displays the component, but there's no indication on the UI that anything is happening between the request and the response. That's what I'm trying to figure out.
import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';
var Routes = {
path: "/",
component: Main,
indexRoute: {
component: Home
},
childRoutes: [
{
path: "test",
component: Test
},
{
path: "about",
getComponent: function(path, cb) {
require.ensure([], (require) => {
cb(null, require("./pages/about/About.jsx"));
});
}
}
]
};
export default Routes;
After trying to unsuccessfully force a "loading" component to display using onEnter or within the getComponent function, I thought maybe I should try using Redux to set a loading state to true/false and getting my main view component to display a loading screen:
import React from 'react';
import {connect} from 'react-redux';
import NavBar from '../components/Navigation/NavBar.jsx';
import Footer from '../components/Footer.jsx';
import Loading from './Loading.jsx';
import navItems from '../config/navItems.jsx';
import setLoading from '../actions/Loading.jsx';
var Main = React.createClass({
renderPage: function() {
if (this.props.loading) {
return (
<Loading/>
);
} else {
return this.props.children;
}
},
render: function() {
return (
<div>
<header id="main-header">
<NavBar navigation={navItems}/>
</header>
<section id="main-section">
{this.renderPage()}
</section>
<Footer id="main-footer" />
</div>
);
}
});
function mapStateToProps(state) {
return {
loading: state.loading
}
}
export default connect(mapStateToProps)(Main);
This seems to work if I manually set the loading state using an action, which is what I was looking to do. But (and I feel this is going to be a real noob question) I can't figure out how to access the store/dispatcher from within the router.
I'm not sure if I'm using the wrong search terms or whatever, but I'm completely out of ideas and every react-router/redux tutorial seems to skip over what I feel like has to be a common problem.
Can anyone point me in the right direction (and also let me know if what I'm doing is best practice?)?
EDIT: I'll try and clarify this a bit more. In the first code block, you can see that if I click a <Link to="/about"> element then the getComponent function will fire, which will lazy-load the About.jsx component. The problem I am having is I can't figure out how to show some sort of loading indicator/spinner that would appear immediately after clicking the link and then have it get replaced once the component loads.
MORE EDITING: I've tried creating a wrapper component for loading async routes and it seems to work, however it feels really hacky and I'm sure it isn't the right way to go about doing this. Routes code now looks like this:
import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';
import AsyncRoute from './pages/AsyncRoute.jsx';
var Routes = {
path: "/",
component: Main,
indexRoute: {
component: Home
},
childRoutes: [
{
path: "test",
component: Test
},
{
path: "about",
component: AsyncRoute("about")
}
]
};
export default Routes;
The AsyncRoute.jsx page looks like this:
import React from 'react';
function getRoute(route, component) {
switch(route) {
// add each route in here
case "about":
require.ensure([], (require) => {
component.Page = require("./about/About.jsx");
component.setState({loading: false});
});
break;
}
}
var AsyncRoute = function(route) {
return React.createClass({
getInitialState: function() {
return {
loading: true
}
},
componentWillMount: function() {
getRoute(route, this);
},
render: function() {
if (this.state.loading) {
return (
<div>Loading...</div>
);
} else {
return (
<this.Page/>
);
}
}
});
};
export default AsyncRoute;
If anyone has a better idea, please let me know.
I think I have this figured out. It may or may not be the correct way to go about things, but it seems to work. Also I don't know why I didn't think of this earlier.
First up, move my createStore code to its own file (store.jsx) so I can import it into the main entry point as well as into my Routes.jsx file:
import {createStore} from 'redux';
import rootReducer from '../reducers/Root.jsx';
var store = createStore(rootReducer);
export default store;
Root.jsx looks like this (it's an ugly mess, but I'm just trying to get something that works on a basic level and then I'll clean it up):
import {combineReducers} from 'redux';
import user from './User.jsx';
import test from './Test.jsx';
var loading = function(state = false, action) {
switch (action.type) {
case "load":
return true;
case "stop":
return false;
default:
return state;
}
};
export default combineReducers({
user,
test,
loading
});
I've made a basic component that shows Loading/Loaded depending on the Redux store's value of "loading":
import React from 'react';
import {connect} from 'react-redux';
var Loading = React.createClass({
render: function() {
if (this.props.loading) {
return (
<h1>Loading</h1>
);
} else {
return (
<h1>Loaded</h1>
);
}
}
});
export default connect(state => state)(Loading);
And now my Routes.jsx file looks like this (note I've imported the Redux store):
import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';
import store from './config/store.jsx';
var Routes = {
path: "/",
component: Main,
indexRoute: {
component: Home
},
childRoutes: [
{
path: "test",
component: Test
},
{
path: "about",
getComponent: function(path, cb) {
store.dispatch({type: "load"})
require.ensure([], (require) => {
store.dispatch({type: "stop"});
cb(null, require("./pages/about/About.jsx"));
});
}
}
]
};
export default Routes;
This seems to work. As soon as a <Link/> is clicked to go to the /about route, an action is dispatched to set the "loading" state to true in the main store. That causes the <Loading/> component to update itself (I envision it would eventually render a spinner in the corner of the window or something like that). That weird require.ensure([]) function is run to get webpack to do its fancy code splitting, and once the component is loaded then another action is dispatched to set the loading state to false, and the component is rendered.
I'm still really new to React and while this seems to work, I'm not sure if it's the right way to do it. If anyone has a better way, please chime in!
Following the same approach as #David M I implemented a loading reducer and a function to wrap the dispatches.
Excluding the store creation and manage, they are basically as follows:
loadingReducer:
// ------------------------------------
// Constants
// ------------------------------------
export const LOADING = 'LOADING'
// ------------------------------------
// Actions
// ------------------------------------
const loadQueue = []
export const loading = loading => {
if (loading) {
loadQueue.push(true)
} else {
loadQueue.pop()
}
return {
type: LOADING,
payload: loadQueue.length > 0
}
}
export const actions = {
loading
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[LOADING]: (state, action) => (action.payload)
}
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = false
export default function reducer (state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}
Notice how loadingQueue keeps the loading message active while there are remaining modules to fetch, for nested routes.
withLoader function:
import { loading } from 'loadingReducer'
const withLoader = (fn, store) => {
return (nextState, cb) => {
store.dispatch(loading(true))
fn(nextState, (err, cmp) => {
store.dispatch(loading(false))
cb(err, cmp)
})
}
}
export default withLoader
Now when defining new routes we can dispatch the loading action implicitly using withLoader:
someRoute:
import withLoader from 'withLoader'
import store from 'store'
const route = {
path: 'mypath',
getComponent: withLoader((nextState, cb) => {
require.ensure([], require => {
cb(null, require('something').default)
}, 'NamedBundle')
}, store)
}
export default route
OK, let's see if I can shed some light on this here:
I can't figure out how to access the store/dispatcher from within the router
There is no need to do that AFAIK. You can specify all routes, listing the components that should answer each route (like you did above), and then connect each of the components to the redux store. For connecting, your mapStateToProps function can be written in a much simpler fashion, like this:
export default connect(state => state)(Main);
Regarding the loading state: I think it is a step in the wrong direction to have a slow-loading component and to display a waiting indicator while it is loading. I would rather have a fast-loading component that loads all of its data asynchronously from the backend, and while the data is not yet available, the component renders a waiting indicator. Once the data is available, it can be displayed. That is basically what you sketched in your second edit.
It would be even better if you could drive this off of your actual data, i.e. no data present -> show the loading screen / data present -> show the real screen. This way, you avoid issues in case your loading flag gets out of sync. (More technically speaking: Avoid redundancy.)
So, instead of making the wrapper generic, I would rather create a standalone component for the loading screen and display that whenever each individual component feels the need for it. (These needs are different, so it seems to be difficult to handle this in a generic way.) Something like this:
var Page = function(route) {
return React.createClass({
getInitialState: function() {
// kick off async loading here
},
render: function() {
if (!this.props.myRequiredData) {
return (
<Loading />
);
} else {
return (
// display this.props.myRequiredData
);
}
}
});
};
dynamic load async routers are using require.ensure, which use jsonp to download scripts from network.
because of slow networking, sometime, UI blocks, the screen is still showing the previews react component.
#Nicole , the really slow is not the data loading inside component, but is the component self, because of jsonp