Vue.js - switching from one component to another - javascript

I'm quite new to vue.js.
I have a page that lists details of a list of people, with an 'Edit' button against each person.
When I click the Edit button, I want to switch to another page which shows a form for editing the selected person's details.
I'm not sure what is the best way to do this. I'm using Bootstrap and Router in my solution.
Option 1: I thought it would be straightforward to route to '/person/:id' when clicking the Edit button, but not sure how to do this from the click handler method.
Option 2:
Below is the main component, where I'm trying to switch between the two components 'PersonsList' and 'EditPersonData' upon receiving an event from either. PersonsList is emitting the event successfully, but I'm not sure how to listen to it here and switch to EditPesonData component.
Home.vue
<template>
<div class="home">
<h1 class='home-text'>
{{message}}
</h1>
<component v-bind:is="component"/>
</div>
</template>
<script>
import PersonsList from '#/components/PersonsList'
import EditPersonData from '#/components/EditPersonData'
export default {
name: 'Home',
data() {
return {
message: 'Welcome to Person Details!',
component: "PersonsList"
}
},
components: {
PersonsList,
EditPersonData
}
};
</script>
Any help would be very much appreciated.
Thanks

The best way would be to send the id as a param in the route /person/:id.
To do this, you can append the id of each list item, for example:
<div v-for="(user,index) in userList" :key="index">
<a :href="'/person/' + user.id">Edit</a>
</div>
Then, you would get it in the other component as this.$route.params.id in order to get more details about the user of this id.
Make sure you define the route in the router with the id param as follows: person/:id
UPDATE :
If you want to change the route on a click function:
<b-btn #click="editPerson(user.id)">Edit</b-btn>
And in the methods, add the function as follows:
editPerson(userId){
this.$router.push({
name: 'person',
params: { id: userId }
});
}

Related

How to make router-link work with custom link component?

<base-link> component
I have a Vue component <base-link>, which I use every time I want to have an achor. It's mostly for applying styles specific to links, so that all the links across the whole page look the same without applying global styles.
Make <router-link> use <base-link>
When using <router-link> component to create a link, I cannot apply those styles (<base-link> styles are scoped) unless <router-link> uses my <base-link> component to create the anchor element.
Fortunately <router-link> provides tag attribute, which seems to do exactly that. Unfortunately I can't get it to work. I have 2 problems:
All my components are locally registered (I use ES6 modules with Webpack and import components locally every time I need them). <router-link> doesn't know what <base-link> component is and can't render it. Is there a way to inject a local component for <router-link> to use?
To solve problem #1, I thought it's enough to declare <base-link> component globally. Unfortunately it still doesn't work. This time <base-link> component gets rendered properly, but is still not functional - doesn't react to click events. It seems to me the problem is that it's href attribute isn't set at all. Is there a way to make <router-link> set it properly? (without setting it manually)
Question
How do I solve problems #1 and #2? I suspect #1 might be not possible, but I hope at least #2 is.
Code example
Here is a pen with code below, which illustrates both problems.
const router = new VueRouter({
routes: [
{
path: "/",
component: {
template: '<p>Homepage template</p>'
}
},
{
path: "/subpage",
component: {
template: '<p>Subpage template</p>'
}
}
]
});
// Globally registered BaseLink.
Vue.component('BaseLinkGlobal', {
props: {
href: String
},
template: `
<a
:href="href"
class="BaseLinkGlobal"
>
<slot />
</a>
`
})
const vue = new Vue({
el: "#app",
router,
components: {
// Locally registered BaseLink.
BaseLinkLocal: {
props: {
href: String
},
template: `
<a
:href="href"
class="BaseLinkLocal"
>
<slot />
</a>
`
}
},
template: `
<div>
<!-- 2 router links. One uses locally registered BaseLink
-- and the other one a globally registered one. -->
<nav>
<router-link
to="/"
tag="base-link-local"
>
Home
</router-link>
<router-link
to="/subpage"
tag="base-link-global"
>
Subpage
</router-link>
</nav>
<router-view />
</div>
`
});
You can create a base link component which can double as a normal a tag or <router-link> when you wish.
//Base link
<template>
<component :is="type" :class="{'base': type === 'a'}" v-bind="$attrs">
<slot></slot>
</component>
</template>
<script>
export default {
props: {
routerLink: {
type: Boolean,
default: false
}
},
computed: {
type() {
return this.routerLink ? 'router-link' : 'a'
}
}
}
</script>
<style scopex>
.base {
border: 1px solid red;
}
</style>
Usage
when you want to use it as a normal link just do not provide the router-link prop as below:
<base-link>This is a base a tag</base-link>
To use it as a router-link just add the router-link prop along with the to prop:
<base-link router-link to="/">This is router-link</base-link>
Explanation about the base-link component:
We use a component which is provided by vuejs to render a tag or router-link base on the truthiness of the routerLink prop.
A class of .base is added if it is a normal link i.e a
we bind $attrs which allows us to make the component more transparent i.e allows us to use attributes like href or to without passing them as props.
<base-link href="https://google.com">go to google</base-link>
You can have a look here for more explanation about usage of $attrs
This is for solving problem #2
The global component doesn't inherit the event listener of the router link. You can make it inherit by adding v-on="$listeners" to the global component.
// Globally registered BaseLink.
Vue.component('BaseLinkGlobal', {
props: {
href: String
},
template: `
<a
:href="href"
class="BaseLinkGlobal"
v-on="$listeners"
>
<slot />
</a>
`
})
The link works after adding it: https://codepen.io/jacobgoh101/pen/YvqJxL?editors=0010

How to handle v-if with the back button in Vue.js?

I am using routing with Vue.js and am trying to use v-if to conditional display 2 divs. The first div is a list of items. The second div is the details of an item. A good example of what I am trying to do would be something similar to Gmail. You have your list of emails and when you click the subject, you are taken to a page that shows the whole email.
I have this somewhat working using v-if. In my data, I have a variable called view. When the page loads, this value is set to 'list', which shows the list div. When someone clicks something in the list div, this value switches to 'details'. and the details div shows.
The problem that I am running into is if the user hits the back button in the browser, the view variable does not get updated. This seems like it is a common scenario, so I am wondering if there is a better way to do this.
<div id="app">
{{ view }} //for debugging
<div class="list" v-if="view === 'list'">
<table>
<tr v-for="item in list">
<td><router-link v-on:click.native="doSomething" v-bind:to="'/items/' + item.id">{{ item.title }}</router-link></td>
</tr>
</table>
</div>
<div class="details" v-if="view === 'details'">
<router-view></router-view>
</div>
</div>
Snippets from my Vue declaration.
data: {
view : "list"
},
doSomething: function () {
var app = this;
app.view = "details";
}
Try this:
<div id="app">
{{ view }} //for debugging
<router-view></router-view>
</div>
And then add two routes:
{
path: '/',
name: 'emails',
component: Emails,
},
{
path: '/login',
name: 'email.show',
component: EmailShow,
}
After that Emails component should handle logic of showing all emails and EmailShow should handle showing just one email

Meteor- Use a Blaze template in a React template from a route

I am currently developing a reactive web app through meteor that is utilizing the templates:tabs package, which is designed to create a tabular interface. I plan on displaying a data table within these tabs and sending queries to different databases depending on which tab is selected similar to cars.com.
The app already has a FlowRouter which links to two different routes, and I only want the tabs to be present for one of them. The router I wish to have displaying the tabs is as follows.
#router.jsx
FlowRouter.route('/', {
action() {
mount(MainLayout, {
content: (<Landing />)
}
)
}
});
I need to create the following template:
template name="myTabbedInterface">
#Tabs.html
{{#basicTabs tabs=tabs}}
<div>
<p>Here's some content for the <strong>first</strong> tab.</p>
</div>
<div>
<p>Here's some content for the <strong>second</strong> tab.</p>
</div>
<div>
<p>Here's some content for the <strong>third</strong> tab.</p>
</div>
{{/basicTabs}}
</template>
Here is the JS file that has the helpers for the template.
#myTabbedInterface.js
ReactiveTabs.createInterface({
template: 'basicTabs',
onChange: function (slug, template) {
// This callback runs every time a tab changes.
// The `template` instance is unique per {{#basicTabs}} block.
console.log('[tabs] Tab has changed! Current tab:', slug);
console.log('[tabs] Template instance calling onChange:', template);
}
});
Template.myTabbedInterface.helpers({
tabs: function () {
// Every tab object MUST have a name and a slug!
return [
{ name: 'First', slug: 'reports' },
{ name: 'Second', slug: 'sources' },
{ name: 'Third', slug: 'feedback' }
];
},
activeTab: function () {
// Use this optional helper to reactively set the active tab.
// All you have to do is return the slug of the tab.
// You can set this using an Iron Router param if you want--
// or a Session variable, or any reactive value from anywhere.
// If you don't provide an active tab, the first one is selected by default.
// See the `advanced use` section below to learn about dynamic tabs.
return Session.get('activeTab'); // Returns "first", "second", or "third".
}
});
Lastly, here is the file that "Landing" routes to from the router where I want the template to be called:
#Landing.jsx
`import {Blaze} from 'meteor/blaze';
import React, {Component} from 'react';
import { Meteor } from 'meteor/meteor';
export default class Landing extends Component{
render(){
return(
<div>
//Want to render template here
</div>
)
}
}`
So how is it possible to render the (Blaze) template in HTML in a React render? Thank you.
I suggest not using a Blaze package to solve a React problem. I'm aware this is not what you asked, but maybe it helps.
Implementing a tabbed UI is really a simple task, mixing React and Blaze isn't worth it. A nice library to solve this problem is React-Bootstrap, it implements several useful React components like <Tab>:
<Tabs>
<Tab title="Apples">Apple content</Tab>
<Tab title="Pears">Pear content</Tab>
<Tab title="Melons">Melon content</Tab>
</Tabs>
But if you wish to walk that road, you could try blazetoreact.

Using if to have different elements on different pages in Meteor.js

I have a certain div class (landing), that I only want to have appear on certain routes. I am using a "main" template that has my header and footer (with a >yield included between for each pages unique code). However, there is an element in the header that I only want to appear on the landing page ('/' route).
I am using Iron Router.
Is there a way to do this easily with an #if in meteor? Thanks!
Here's a generic route name equality testing helper:
Template.registerHelper('routeEquals',function(name){
return Router.current().route.getName() === name;
});
Then in any template you can do (for example):
{{#if routeEquals '/'}}
in the / route
{{else}}
Not in the / route
{{/if}}
You want to create a helper on your header template that reactively checks the current route. I haven't used Iron Router in a while, but believe Router.current().route.getName() is reactive. Try the following:
Template.header.helpers({
onLanding() {
return Router.current().route.getName() === '<landingRouteName>';
},
});
<template name="header">
{{#if onLanding}}
<div>I should only show on the landing page!</div>
{{/if}}
</template>
Or, alternatively (and maybe optimally - it would keep the template loaded, rather than injecting it into the DOM when you hit landing), you can use the helper to apply a class like
<div class="{{#if onLanding}}isVisible{{/if}}>...</div>
You can put a session flag in the onBeforeAction of the landing route and then, on the header, check if that session is valid and show the div
Something like this
this.route("landing", {
path: "/landing",
onBeforeAction: function () {
Session.set('showDiv', true);
}
});
<template name='main'>
<header>{{#if helper_getShowDivFlag}}<div></div>{{/if}}</header>
</template>
helper_getShowDivFlag: function() {
return Session.get('showDiv');
}
Alternatively, checking the router name in the main template and show/hide the div
<template name='main'>
<header>{{#if helper_showDivFlag}}<div></div>{{/if}}</header>
</template>
helper_showDivFlag: function() {
return Router.current().route.getName() == 'landing'
}
Or if you don't want to write code, use this package and check the router :D
<template name='main'>
<header>{{#if isActiveRoute 'landing' }}<div></div>{{/if}}</header>
</template>
You can accomplish this using the data property of your route.
Router.route('/', {
name: 'home',
data: function() {
return {
showMySpecialHeaderWidget: true
};
}
});
Then you can access this data property in your template.
<template name="main">
<header>
{{#if showMySpecialHeaderWidget}}
<div>My special header widget</div>
{{/if}}
</header>
{{> yield}}
</template>
Using this method, you don't need to worry about updating your template if you ever change the names of your routes, and it's easy to show your widget on multiple routes by including this data property.

How to create an Ember JS Master Detail where detail appears between rows of data

I'm trying to create a page that contains master/detail, which is obviously easy using a static {{ outlet }} in ember, however, I'd like the detail to slide down after the row that was selected. For example, on this page: http://jsbin.com/ijejap/1/edit, if the detail of the name was to appear after the currently selected name instead of at the bottom of the page.
The problem I'm trying to solve is that I can't have an outlet within a repeater so that when I click Row 1, I want the outlet to be positioned below Row 1, and when I click Row 2, I want the outlet after Row 2. In other words, I want to dynamically position an outlet, I think, unless there's another way to do it.
Ok, I finally made a conditionnal outlet depending on wether the user is selected or not, but I had to work with DS.Model and its FIXTURES to make this work.
To be brief, I added an action before the link to set a selected property on the user object and record which user is selected to prevent having two users at the same time.
The position of the {{outlet}} then depends on the current selected user.
Here's the template part:
{{#each model}}
<li {{action 'select' this}}>
{{#link-to "user" this}}{{first}}{{/link-to}}
</li>
{{#if selected}}
{{outlet}}
{{/if}}
{{/each}}
And the App.ApplicationController part:
App.ApplicationController = Ember.ArrayController.extend({
currentUser: null,
actions: {
select: function(newUser) {
var oldUser = this.get('currentUser');
if(newUser !== oldUser) {
if(oldUser)
oldUser.toggleProperty('selected');
this.set('currentUser', newUser);
newUser.toggleProperty('selected');
}
}
}
});
Finally, the model definition:
App.ApplicationRoute = Ember.Route.extend({
model: function() {
return this.store.find('user');
}
});
App.User = DS.Model.extend({
first: DS.attr(),
last: DS.attr(),
avatar: DS.attr()
});
Here's the link to the JSBin: http://jsbin.com/cilari/3/
You can make use of components power here.
I made it on JSBin : http://jsbin.com/zovos/2/
First, you declare your user details as a new component, say user-details :
<script type="text/x-handlebars" id="components/user-details">
<li>{{user.first}}</li>
<h2>
{{user.first}} {{user.last}}
<img {{bindAttr src="user.avatar"}} class="pull-right" width=50 />
</h2>
<dl>
<dt>First</dt>
<dd>{{user.first}}</dd>
<dt>Last</dt>
<dd>{{user.last}}</dd>
</dl>
</script>
Then you call it from your list, using the current user as the user in the component :
<script type="text/x-handlebars">
<div class="container">
<div class="row-fluid">
<ul class="nav nav-list span3 well">
{{#each model}}
{{user-details user=this}}
{{/each}}
</ul>
</div>
</div>
</script>
You add the selected property to your component and an action like expandDetails :
App.UserDetailsComponent = Ember.Component.extend({
selected: false,
actions: {
expandDetails: function() {
this.toggleProperty('selected');
}
}
});
Finally, you add the action to your component template and a conditional display on the selected property :
<script type="text/x-handlebars" id="components/user-details">
<li><a {{action "expandDetails"}}>{{user.first}}</a></li>
{{#if selected}}
<h2>
{{user.first}} {{user.last}}
...
</dl>
{{/if}}
</script>
And, of course, you get rid of your user route.
Here is a link to Ember's Guide that show this on a sample post example : http://emberjs.com/guides/components/handling-user-interaction-with-actions/
Hope this helps.
Are you looking for an accordion?
http://addepar.github.io/#/ember-widgets/accordion
I have an idea but unproved yet, maybe you can try it.
For detail, you only need one template. For master, you need write several named outlet (see: http://emberjs.com/guides/routing/rendering-a-template/) and place each below the corresponding item of master view.
When each master item has been clicked, you need pass a param to the detail route in order to let it know what kind of data should be populated.
The key part is the renderTemplate hook of route (see: http://emberjs.com/api/classes/Ember.Route.html#method_renderTemplate). You can take advantage of it because it allows you to specify which named outlet should be used to render the template.
Or you may load all details' data at once and cache it, then pass an identifier when master item got clicked, in this way you can filter the model in advance and get prepared for later renderTemplate use.
This is just an idea and currently don't have spare time to try it, but as your wish, you got the dedicated route, controller and model. I think it is highly doable, let me know it you make any progress.

Categories