Vue.js Router. Place methods in App or in Component? - javascript

I have created this Vue.js router example to wrap my mind around how routing works. I intentionally load it everything from CDN so that whoever looks at this, gets to begin learning immediately instead of having to learn how to import dependencies, etc.
My question is, where do I place the methods that are going to fetch data from a JSON API based on the route parameter of that route? In the App, or in the Component?
Forgive if my question seems naive. I think I'm almost there.
Run Code Snippet then click Full Page to better view.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Simple Vue.js Router Example</title>
<!-- VUE JS v2.6.1 -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<!-- VUE ROUTER JS v3.1.3 -->
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<!-- BOOTSTRAP CSS v4.3.1 -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<!-- GOOGLE FONT CSS - Roboto Mono -->
<link href="https://fonts.googleapis.com/css?family=Roboto+Mono:100,300,400,500,700&display=swap" rel="stylesheet">
<!-- GOOGLE FONT CSS - Material Icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style type="text/css">
body {
font-family: 'Roboto Mono', monospace;
font-weight: 400;
font-size: 1rem;
background-color: #e0e0e0;
}
.active {
color: #f44336;
}
</style>
</head>
<body>
<!-- VUE APP - PARENT CONTAINER -->
<div id="app" class="container">
<!-- HEADER CONTAINER -->
<header>
<hr>
<h1>Header</h1>
<p>Static header text</p>
<ul>
<li>
<router-link to="/">/ </router-link>
</li>
<li>
<router-link to="/users">/users</router-link>
</li>
<li>
<router-link to="/users/123">/users/123</router-link>
</li>
<li>
<router-link to="/posts">/posts</router-link>
</li>
<li>
<router-link to="/posts/456">/posts/456</router-link>
</li>
<li>
<router-link to="/unsaved-changes">/unsaved-changes</router-link>
</li>
<li>
<router-link to="/unknown-route/789">/unknown-route/789</router-link>
<br>
<small>*forwards to route /404</small>
</li>
</ul>
</header>
<!-- MAIN CONTAINER -->
<main>
<hr>
<h1>Main</h1>
<p>Static main text</p>
<router-view name="routerView0"></router-view>
<router-view name="routerView1"></router-view>
<router-view name="routerView2"></router-view>
<router-view name="routerView3"></router-view>
<router-view name="routerView4"></router-view>
<router-view name="routerView5"></router-view>
<router-view name="routerView6"></router-view>
</main>
<!-- FOOTER CONTAINER -->
<footer>
<hr>
<h1>Footer</h1>
<p>Static footer text</p>
</footer>
</div>
<!-- JAVA SCRIPT -->
<script type="text/javascript">
// DISABLE
Vue.config.productionTip = false;
// DISABLE
Vue.config.devtools = false;
// COMPONENT 0
const Component0 = {
template:
`
<div style="background-color: #bcaaa4;">
<strong>Component 0</strong>
<br>
success: route /
<br>
result: component rendered.
</div>
`
}
// COMPONENT 1
const Component1 = {
template:
`
<div style="background-color: #80deea;">
<strong>Component 1</strong>
<br>
success: route /users
<br>
result: component rendered.
</div>
`
}
// COMPONENT 2
const Component2 = {
template:
`
<div style="background-color: #80deea;">
<strong>Component 2</strong>
<br>
success: route /users/{{ $route.params.id }}
<br>
result: component rendered.
</div>
`
}
// COMPONENT 3
const Component3 = {
template:
`
<div style="background-color: #b39ddb;">
<strong>Component 3</strong>
<br>
success: route /posts
<br>
result: component rendered.
</div>
`
}
// COMPONENT 4
const Component4 = {
template:
`
<div style="background-color: #b39ddb;">
<strong>Component 4</strong>
<br>
success: route /posts/{{ $route.params.id }}
<br>
result: component rendered.
</div>
`
}
// COMPONENT 5
const Component5 = {
template:
`
<div style="background-color: #ffe082;">
<strong>Component 5</strong>
<br>
success: route /unsaved-changes
<br>
result: component rendered.
<br>
<small><strong>*If you leave this route,<br> all text typed in below will be lost.</strong></small>
<br>
<input type="text">
</div>
`,
// IN COMPOMENT ONLY...
beforeRouteLeave (to, from, next) {
const answer = window.confirm('Are you sure you want to leave this route? There are unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
}
}
// COMPONENT 6
const Component6 = {
template:
`
<div style="background-color: #ef9a9a;">
<strong>Component 6</strong>
<br>
error: unknown route.
<br>
action: forwarded to route /404.
<br>
result: component rendered.
</div>
`
}
// IN THIS ROUTE I WILL RENDER THESE COMPONENTS..
const router = new VueRouter({
mode: 'hash',
linkExactActiveClass: "active",
routes: [
// ROUTE 0
{ path: '/',
// COMPONENT(S) TO RENDER IN ORDER
components: {
// ONE OR MORE...
routerView0: Component0,
}
}, // END ROUTE 0
// ROUTE 1
{ path: '/users',
// COMPONENT(S) TO RENDER
components: {
// ONE OR MORE...
routerView1: Component1,
}
}, // END ROUTE 1
// ROUTE 1.1
{ path: '/users/:id',
// COMPONENT(S) TO RENDER
components: {
// ONE OR MORE...
routerView2: Component2,
},
// REPORT WHEN THIS ROUTE IS VISITED
beforeEnter: (to, from, next) => {
// ...
console.warn('ROUTE CHANGE')
console.log('ROUTE', 'FROM:', from.path, 'TO:', to.path);
next();
}
}, // END ROUTE 1.1
// ROUTE 2
{
path: '/posts',
components: {
// ONE OR MORE...
routerView3: Component3,
}
}, // END ROUTE 2
// ROUTE 2.1
{
path: '/posts/:id',
components: {
// ONE OR MORE...
routerView4: Component4,
}, // END ROUTE 2.1
// REPORT WHEN THIS ROUTE IS VISITED
beforeEnter: (to, from, next) => {
// ...
console.warn('ROUTE CHANGE')
console.log('ROUTE', 'FROM:', from.path, 'TO:', to.path);
next();
}
},
// ROUTE UNSAVED CHANGES
{
path: '/unsaved-changes',
components: {
// ONE OR MORE...
routerView5: Component5,
}
}, // END ROUTE UNSAVED CHANGES
// REDIRECT!
{
path: '*', redirect: '/404',
// TRAP ANY UNDEFINED ROUTE AND...
// FORWARD IT TO /404 ROUTE
},
// ROUTE UNDEFINED - CUSTOM PAGE
{
path: '/404',
components: {
// ONE OR MORE...
routerView6: Component6,
}
}, // END ROUTE UNDEFINED
]
});
// WATCH EVERY ROUTE THAT IS VISITED
/*
router.beforeEach((to, from, next) => {
// ...
console.info('Global Route Watcher')
console.log('ROUTE', 'FROM:', from.path, 'TO:', to.path);
next();
});
*/
const App = new Vue({
el: '#app',
router,
data: {
},
})
</script>
</body>
</html>

I personally do the fetch inside the components, you can fetch the data on the created or beforeCreate hook if you want the component to render before fetching the data and add a loading animation, alternatively you could use the beforeRouteEnter or beforeRouteUpdate hooks on the component.
My logic behind this is the following, if you fetch the data from the component they would be easier to test by just passing them a new route param and you avoid doing some complex logic on the App component because it wont have to track which route is selected and act accordingly.
take a look on solid and Single responsibility principle

Related

communication between components in vuejs. (search field)

well i'm new to vue js and developing an application with a search function.
This is my component where the search results will be rendered.
<script>
import RecipeItem from "../recipe/RecipeItem";
import { baseApiUrl } from "#/global";
import axios from "axios";
import PageTitle from "../template/PageTitle";
export default {
name: "Search",
components: { PageTitle, RecipeItem },
data() {
return {
recipes: [],
recipe: {},
search: '',
}
},
methods: {
getRecipes() {
const url = `${baseApiUrl}/search?search=${this.search}`;
axios(url).then((res) => {
this.recipes = res.data;
});
}
},
watch: {
search() {
const route = {
name: 'searchRecipes'
}
if(this.search !== '') {
route.query = {
search: this.search
}
}
},
'$route.query.search': {
immediate: true,
handler(value) {
this.search = value
}
}
},
};
</script>
<template>
<div class="recipes-by-category">
<form class="search">
<router-link :to="{ path: '/search', query: { search: search }}">
<input v-model="search" #keyup.enter="getRecipes()" placeholder="Search recipe" />
<button type="submit">
<font-icon class="icon" :icon="['fas', 'search']"></font-icon>
</button>
</router-link>
</form>
<div class="result-search">
<ul>
<li v-for="(recipe, i) in recipes" :key="i">
<RecipeItem :recipe="recipe" />
</li>
</ul>
</div>
</div>
</template>
ok so far it does what it should do, searches and prints the result on the screen.
But as you can see I created the search field inside it and I want to take it out of the search result component and insert it in my header component which makes more sense for it to be there.
but I'm not able to render the result in my search result component with my search field in the header component.
but I'm not able to render the result in my search result component with my search field in the header component.
Header component
<template>
<header class="header">
<form class="search">
<input v-model="search" #keyup.enter="getRecipes()" placeholder="Search recipe" />
<router-link :to="{ path: '/search', query: { search: this.search }}">
<button type="submit">
<font-icon class="icon" :icon="['fas', 'search']"></font-icon>
</button>
</router-link>
</form>
<div class="menu">
<ul class="menu-links">
<div class="item-home">
<li><router-link to="/">Home</router-link></li>
</div>
<div class="item-recipe">
<li>
<router-link to="/"
>Recipes
<font-icon class="icon" :icon="['fa', 'chevron-down']"></font-icon>
</router-link>
<Dropdown class="mega-menu" title="Recipes" />
</li>
</div>
<div class="item-login">
<li>
<router-link to="/auth" v-if="hideUserDropdown">Login</router-link>
<Userdropdown class="user" v-if="!hideUserDropdown" />
</li>
</div>
</ul>
</div>
</header>
</template>
Result component
<template>
<div class="recipes-by-category">
<div class="result-search">
<ul>
<li v-for="(recipe, i) in recipes" :key="i">
<RecipeItem :recipe="recipe" />
</li>
</ul>
</div>
</div>
</template>
Keep the state variables in whatever parent component is common to both the header component and the results component. For example, if you have a Layout component something like this:
<!-- this is the layout component -->
<template>
<HeaderWithSearch v-on:newResults="someFuncToUpdateState" />
<ResultsComponent v-bind:results="resultsState" />
</template>
<!-- state and function to update state are in a script here... -->
When the search bar returns results you need to pass that data "up" to the parent component with an $emit call, then the parent component can then pass that state back down to the results component using normal props.
Check out this documentation: https://v2.vuejs.org/v2/guide/components-custom-events.html
Be sure to pay special attention to the .sync part of the documentation and determine if that's something you need to implement as well.
Unless you want to use a more complicated state management library like vuex (which shouldn't be necessary in this case) you can just keep state in a common parent and use $emit to pass up and props to pass down.

vue js nested routing setup for navigation bar

I'm trying to make my project to have 2 kind of nagigation bar, I have read some documentation and searching for tutorial on youtube, i did not found anything related to my problem, i want to have 2 kind of nav-bar for each route without refreshing it on every page. I put my nav-bar on app file in the current project, but what i want to do is making another nested routed that displaying another nav-bar and have it own routes, can anyone help me with this problem? simple code example would be greatly appreciated.
You could use the $route.meta for controlling which navbar to display. This is an easy solution, but you always have to take care of the nav (or set a default, like in the snippet below):
const Foo = {
template: `
<div>This is Foo</div>
`
}
const Bar = {
template: `
<div>This is Bar</div>
`
}
const routes = [{
path: "/",
redirect: "/foo",
},
{
path: "/foo",
component: Foo,
meta: {
nav: "nav1",
},
},
{
path: "/bar",
component: Bar,
meta: {
nav: "nav2",
},
},
]
const router = new VueRouter({
routes,
})
new Vue({
el: "#app",
components: {
Foo,
Bar,
},
router,
computed: {
computedRoute() {
return this.$route.meta.nav
},
},
})
.link {
padding: 0 8px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<div v-if="computedRoute === 'nav1'">
<router-link to="/foo" class="link">
FOO 1
</router-link>
<router-link to="/bar" class="link">
BAR 1
</router-link>
</div>
<div v-else>
<router-link to="/foo" class="link">
FOO 2
</router-link>
<router-link to="/bar" class="link">
BAR 2
</router-link>
</div>
<br>
<hr>
<router-view />
</div>

Vuejs: Displaying Selected Item On a Page You are On

I need some help displaying a selected an item on the page you are on. I have three component Home, Product, and Index. The Index component is Global component which have a list of items with a route-link attached to them to got to the Product Page (Component) and display the item that was click. I am passing the item in the route-link as params to be able to access the item on the Product page. I am using the Index Component on the Home component to display all item. And when I click an item from the Index component from the Home page, it go to the Product page and display that item. That part is working fine, but when I am on the Product page and I click an item from the Index component, it is not displaying the clicked item in the Product page. I really need some help how to solve this problem.
Main.js code
import Vue from 'vue'
import App from './App'
import router from './router/routes'
import Index from './components/Index'
Vue.component('list-items', Index)
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: {App},
template: '<App/>'
})
Route.js File
import Vue from 'vue'
import Router from 'vue-router'
import Home from '../pages/Home'
import Product from '../pages/Product'
Vue.use(Router)
export default new Router({
routes: [
{
path: '' || '/',
name: 'Home',
component: Home
},
{
path: '/product',
name: 'Product',
component: Product,
props: true
}
],
mode: 'history'
})
Home.vue component
<template>
<div class="container-fluid p-0">
<list-items />
</div>
</template>
<script>
export default {
name: 'Home',
data: () => ({
})
}
Product.vue component
<template>
<div class="container-fluid p-0">
<div class="container-fluid py-5">
<div class="row">
<div class="col-md-7">
<div class="col-md-12">
<img v-bind:src="item.url" alt="" class="img-fluid">
</div>
</div>
<div class="col-md-4 margin-padding-right">
<div class="col-md-12">
<h3>{{$route.params.product.name}}</h3>
<h5 class="price">${{item.price}}</h5>
</div>
</div>
</div>
<br>
<div class="container-fluid">
<div class="col-md-12 related-item">
<h5>Related Items</h5>
</div>
<list-items />
</div>
</div>
</div>
<script>
export default {
name: 'Product',
props: ['product'],
data: () => ({
quantity: 1,
item: {
name: '',
url: ''
}
}),
mounted () {
if (this.product) {
this.item.name = this.product.name
this.item.url = this.product.image
}
},
watch: {
change: function (newValue, oldValue) {
this.item.name = newValue.product.name
}
}
}
</script>
<style lang="scss" scoped>
</style>
This is an image of what I am trying to achieve.

Vue/Vuecli3 - How to route from one component to another with parameters

I'm running into an issue when trying to redirect from one component to another. It appears that it's not routing to the URL thats specified in my router to the desired component and is staying on my home page instead. I can't figure out where the error is occuring.
I'm using the Vue CLI version 3.
Below is my index.js, Home.vue and Model.vue
Then under that is images of the Home.vue then it shows what happens when I try to redirect to the link in my href tag.
You can see that it's not going to the other component and it's staying on my home page.
Any ideas on whats causing the issue here? Thanks!
/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Homefrom '#/components/Home'
import Model from '#/components/Model'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/model/:model_tag_name',
name: 'Model',
component: Model
// props: true
}
]
})
/components/Home.vue
<template>
<div class="hello container-fluid">
<h1>{{msg}}</h1>
<div class="row">
<div class="col-4 text-left">
<ol>
<li v-for="tag in tags" v-bind:key="tag.model_tag_name">
<a :href="'/model/'+ tag.model_tag_name"> {{tag.model_tag_name}}</a>
</li>
</ol>
</div>
<div class="col-8">
<p>Data</p>
</div>
</div>
</div>
</template>
<script>
var axios = require('axios');
export default {
name: 'Home',
data () {
return {
msg: 'Welcome to Your Vue.js App',
tags: []
}
},
mounted: function() {
var url = 'http://10.0.0.5:5000';
console.log(url)
axios.get(url + '/')
.then((response) => {
console.log(response.data);
this.tags = [{"model_tag_name": response.data}];
})
.catch(function(error) {
console.log(error);
});
},
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
font-weight: normal;
}
a {
color: #42b983;
}
</style>
/components/Model.vue
<template>
<div class="container-fluid">
<h1> Model </h1>
</div>
</template>
<script>
var axios = require('axios');
export default {
name: 'Model',
data () {
return {
model_tag_name: this.$route.params.model_tag_name
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
font-weight: normal;
}
a {
color: #42b983;
}
</style>
http://localhost:8080/
Then this is what happenes when I click the href link on the home page. It's redirecting back to the home page even though the URL matches the routerview for Model.vue
Pleas update this code in /components/Home.vue
<li v-for="tag in tags" v-bind:key="tag.model_tag_name">
<router-link :to="{ name: 'Model', params: { model_tag_name: tag.model_tag_name}}">
{{tag.model_tag_name}}</router-link>
</li>

Passing data from parent to child when rendering a list in vue.js

I am writing something in Vue.js and it requires iterating over a list of tags. When clicked, each of those tags should show the 5 most recent stories with said tag.
I am using vue resources rather than ajax and that is all working fine.
So far I have one component which generates the list of tags and a child component to generate a list of stories. The child should take the tag id from the clicked tag and make a get request. I am struggling to make vue pass the tag id from parent to child.
I am using v-bind so it looks like this (p.s. ignore the #, using Laravel's blade and that is to tell blade not to act on the moustache tags)
<child v-bind:tag="#{{tags.id}}"></child>
Though tags.id works perfectly well elsewhere, it does not appear to here.
If I try to pass through a string literal it is quite happy, ie:
<child tag="1"></child>
but I am having no luck with vue's dynamic syntax :tag
Here is my full code:
<!DOCTYPE html>
<html>
<head>
<title>Laravel</title>
<link href="https://fonts.googleapis.com/css?family=Lato:100" rel="stylesheet" type="text/css">
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
<style type="text/css">
.toggled { display: none; }
</style>
</head>
<body>
<div class="container">
<h1>My tags</h1>
<tags></tags>
</div>
<template id="tags-template">
<ul class="list-group"
>
<li class="list-group-item"
v-for="tags in list"
#click=" tags.open =! tags.open"
>
<span>#{{ tags.name }}</span>
<div v-bind:class="{'toggled': tags.open}">
See all recent stories #{{tags.id}}
<child v-bind:tag="#{{tags.id}}"></child>
</div>
</li>
</ul>
</template>
<template id="stories-template">
<p>Stories dropdown goes here #{{ tag }}</p>
</template>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.16/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-resource/0.7.0/vue-resource.js"></script>
<script>
Vue.component('tags', {
//
template: '#tags-template',
data: function() {
return {
list: [],
url: '/tags/',
};
},
created: function() {
this.fetchTagsList();
},
methods: {
fetchTagsList: function() {
var resource = this.$resource('/api/tags/{id}');
resource.get( {}, function(tags)
{
this.list = tags;
}.bind(this));
},
// toggle: function() {
// var resource = this.$resource('/api/storyFromTag/{id}');
// resource.get( {id: 1}, function(stories)
// {
// this.list = stories;
// }.bind(this));
// alert(stories);
// }
}
}),
Vue.component('child', {
props: ['tag'],
template: '#stories-template'
})
new Vue ({
el: 'body'
})
</script>
</body>
</html>

Categories