solana-wallets-vue is the VueJS version of solana-wallet. It added a button to connect your browser wallet.
This works, and connects a wallet, but right after connecting the wallet I need to know what the wallet address (public key) is.
Using all of Vue3's life cycle hooks, I can only determine what the wallet address is if I refresh, and it will then show up in onMounted
Their component is added like so
<!-- ? Wallet Connect -->
<wallet-multi-button dark></wallet-multi-button>
This is the markup that is injected into the app with their component
<div class="swv-dark">
<button class="swv-button swv-button-trigger"> Select Wallet </button>
</div>
After I connect my Solana wallet, the markup will change like so, but no Vue Lifecycle event triggers:
<div class="swv-dark">
<div class="swv-dropdown">
<button
class="swv-button swv-button-trigger"
aria-expanded="false"
title="DqabcUFkt9UvV9wDEtK59nySRPhy71n3JuFkCcw854vL"
style="pointer-events: auto;"
>
<i class="swv-button-icon">
<img src="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjM0IiB3aWR0aD0iMzQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI" alt="Phantom icon">
</i>
<p>Dqab..54vL</p>
</button>
<ul aria-label="dropdown-list" class="swv-dropdown-list" role="menu">
<li class="swv-dropdown-list-item" role="menuitem">Copy address</li>
<li class="swv-dropdown-list-item" role="menuitem"> Change wallet </li>
<li class="swv-dropdown-list-item" role="menuitem"> Disconnect </li>
</ul>
</div>
</div>
In the onMounted LifeCycle, I am able to see the wallet address / public, but again only if I refresh:
onMounted(async () => {
console.log('onMounted')
const { publicKey, sendTransaction } = useWallet()
if (publicKey && publicKey.value) {
console.log('publicKey', publicKey.value.toBase58())
}
})
Used watch to be able to accomplish this task.
Doc: https://vuejs.org/guide/essentials/watchers.html#basic-example
Real world example: https://www.netlify.com/blog/2021/01/29/deep-dive-into-the-vue-composition-apis-watch-method/
import { ref, computed, watch } from 'vue'
const connectedWallet = computed(() => {
const { publicKey, sendTransaction } = useWallet()
if (publicKey && publicKey.value) {
return publicKey.value.toBase58()
}
})
This watches for the change when you use the solana-wallet connect
watch(connectedWallet, async (currentValue) => {
console.log('currentValue', currentValue)
})
When I want to do a search multiple times it shows me the NavigationDuplicated error. My search is in the navbar and the way I have configured the search is to take the value using a model and then pass the value as a parameter to the ContentSearched component, and then receive the value of the search in that component.
I know the right way is to use an emitter, but I still don't know how to learn to use it. To access the emit is context.emit('', someValue)
NavigationDuplicated {_name: "NavigationDuplicated", name: "NavigationDuplicated", message: "Navigating to current location ("/search") is not allowed", stack: "Error↵ at new NavigationDuplicated (webpack-int…node_modules/vue/dist/vue.runtime.esm.js:1853:26)"}
NavBar.vue
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-nav" v-bind:class="{'navbarOpen': show }">
<div class="container">
<router-link to="/" class="navbar-brand">
<img src="../assets/logo.png" alt="Horizon Anime" id="logo">
</router-link>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation" v-on:click.prevent="toggleNavbar">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent" v-bind:class="{'show': show }">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<router-link class="nav-link" to="/" ><i class="fas fa-compass"></i> Series</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name: 'EpisodesSection'}" ><i class="fas fa-compact-disc"></i> Episodios</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name: 'MovieSection'}" ><i class="fas fa-film"></i> Peliculas</router-link>
</li>
</ul>
<div class="search-bar">
<form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" v-model="query" type="search" placeholder="Buscar películas, series ..." aria-label="Search">
<button class="btn btn-main my-2 my-sm-0" #click.prevent="goto()" type="submit"><i class="fas fa-search"></i></button>
</form>
</div>
</div>
</div>
</nav>
</template>
<script>
import {value} from 'vue-function-api';
import {useRouter} from '#u3u/vue-hooks';
export default {
name: "NavBar",
setup(context){
const {router} = useRouter();
const query = value("");
let show = value(true);
const toggleNavbar = () => show.value = !show.value;
const goto = () =>{
let to = {name: 'ContentSearched' , params:{query: query}}
router.push(to);
};
return{
show,
toggleNavbar,
goto,
query
}
}
}
</script>
ContentSearched.vue
<template>
<div class="container">
<BoxLink/>
<main class="Main">
<div class="alert alert-primary" role="alert">
Resultados para "{{query}}"
</div>
<div v-if="isLoading">
<!-- <img class="loading" src="../assets/loading.gif" alt="loading"> -->
</div>
<div v-else>
<ul class="ListEpisodios AX Rows A06 C04 D02">
<li v-for="(content, index) in contentSearched" :key="index">
<div v-if="content.type === 'serie'">
<Series :series="content"/>
</div>
<div v-if="content.type === 'pelicula'">
<Movies :movies="content"/>
</div>
</li>
</ul>
</div>
</main>
</div>
</template>
<script>
import {onCreated} from "vue-function-api"
import {useState , useRouter , useStore} from '#u3u/vue-hooks';
import BoxLink from "../components/BoxLink";
import Movies from "../components/Movies";
import Series from "../components/Series";
export default{
name: 'ContentSearched',
components:{
BoxLink,
Movies,
Series
},
setup(context){
const store = useStore();
const {route} = useRouter();
const state = {
...useState(['contentSearched' , 'isLoading'])
};
const query = route.value.params.query;
onCreated(() =>{
store.value.dispatch('GET_CONTENT_SEARCH' , query.value);
});
return{
...state,
query,
}
}
};
</script>
This happened to me when I had a router-link pointing to the same route. e.g. /products/1.
The user is able to click on the products, but if a product was already clicked (and the component view was already loaded) and the user attempts to click it again, the error/warning shows in the console.
You can learn more on the github issue..
Posva, one of the main contributors of vue-router suggests:
router.push('your-path').catch(err => {})
However, if you don't want to have a catch block which does nothing, in order to solve the issue you can compare the router navigation with the current route and only navigate if they differ:
const path = `/products/${id}`
if (this.$route.path !== path) this.$router.push(path)
Note: $route is an object provided by vue-router to every component. See The Route Object for more info.
I think the best solution to this problem can be implemented at the root level if we are not going to further use Router.push as asynchronous call.
import Router from 'vue-router';
const originalPush = Router.prototype.push;
Router.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
};
Vue.use(Router);
If you are not feeling comfortable catching all kind of errors, I think this implementation is more considerate:
this.$router.push("path").catch(error => {
if (error.name != "NavigationDuplicated") {
throw error;
}
});
As of 2021, global configuration :
I only wanted to silence NavigationDuplicated error, an empty catch can be dangerous. So I did this :
const router = new VueRouter({/* ... */})
const originalPush = router.push
router.push = function push(location, onResolve, onReject)
{
if (onResolve || onReject) {
return originalPush.call(this, location, onResolve, onReject)
}
return originalPush.call(this, location).catch((err) => {
if (VueRouter.isNavigationFailure(err)) {
return err
}
return Promise.reject(err)
})
}
Insert this once when you initialize vue-router.
Thanks to #Oleg Abrazhaev for the update.
I encountered the same problem while searching. My solution is to add timestamp to the this.$route.query parameters of the search page.
this.$router.push({
path: "/search",
query: {
q: this.searchQuery,
t: new Date().getTime(),
}
});
Hope it helps you.
if you are using router.push in your code and you don't care about the navigation failing, you should catch it by using catch:
router.push('/location').catch(err => {})
in manual:
router.push(location, onComplete?, onAbort?)
You can use more simply
router.push("/", () => {});
You mixed multiple concepts here from router-links to programmatic navigation, to query params to a state store. That makes it a bit difficult to help you and tell you what the "correct" solution here is.
Nonetheless, I think the best approach for you would be to:
1) define your route as
{
path: "/search/:searchString",
component: MySearchComponent,
props: true
}
2) use a responsive <router-link> instead of your router.push
<input type="text" v-model="searchString">
<router-link :to="'/search/'+searchString" tag="button">search</router-link>
3) access the searchString in your search component via props: ['searchString'] and this.searchString
props: ['searchString'],
...
computed: {
msg() {
return `Searching for, ${this.searchString}!`;
}
}
Full example: https://codesandbox.io/s/vue-routing-example-9zc6g
Note, I just forked the first codesandbox with a router I could find, adjust accordingly.
For TypeScript it worked like this
const superPush = VueRouter.prototype.push
VueRouter.prototype.push = async function push(loc:RawLocation):Promise<Route> {
try {
return await superPush.bind(this)(loc)
} catch (e) {
if (e?.name === 'NavigationDuplicated') {
console.warn(e)
return e
} else {
throw e
}
}
}
I'm very late to the party, but I thought I'd add my solution to this issue as it isn't listed: I simply placed an intermediate searching page as a pass-through to the search results view. I am now using this page for doing some preprocessing of the search terms.
The page template simply is:
<template>
<div>searching ...</div>
</template>
The NavigationDuplicated error is now gone and as an added benefit because I perform the fetch in this intermediate page, the responsibility for error-handling is isolated from both the search bar and the results view.
I post here the solution I found, because I was not able to find it well documented somewhere, and I went through it by trial and error.
It could be useful to someone, or someone may fix my misunderstood interpretation of vue-router guards.
It make use of vue-router V4.x and a global beforeEach guard.
The use cases are:
user asks for https://app.com/ without being already authorized;
user asks for https://app.com/ being already authorized;
user asks for any available routing, which requires auth or not.
Routes:
const routes = [
/**
* Routes not requiring auth
*/
{
path: '/',
component: () => import('layouts/NotAuthorizedLayout.vue'),
children: [
{
path: 'login',
name: 'LOGIN',
component: () => import('pages/Login.vue')
},
{
path: 'emailsignup',
component: () => import('pages/EmailSignup.vue')
},
{
path: 'forgottenpassword',
component: () => import('pages/ForgottenPassword.vue')
}
]
},
/**
* Routes requiring auth
*/
{
path: '/',
component: () => import('layouts/AuthorizedLayout.vue'),
meta: { requiresAuth: true },
children: [
{
path: 'authors',
name: 'AUTHORS',
component: () => import('pages/Authors.vue')
},
{ path: 'profile', component: () => import('pages/userProfile/index.vue') }
]
}
];
beforeEach global guard:
const redirectToLogin = route => {
const LOGIN = 'LOGIN';
if (route.name != LOGIN) {
return { name: LOGIN, replace: true, query: { redirectFrom: route.fullPath } };
}
};
const redirectToHome = route => {
const DEFAULT = 'AUTHORS';
return { name: DEFAULT, replace: true };
};
Router.beforeEach((to, from) => {
const userIsAuthenticated = store.getters['authentication/userIsAuthenticated'];
const requiresAuth = to.matched.some((route) => route.meta && route.meta.requiresAuth);
if (!userIsAuthenticated && to.fullPath === '/') {
return redirectToLogin(to);
}
if (!userIsAuthenticated && requiresAuth) {
return redirectToLogin(to);
}
if (to.fullPath === '/') {
return redirectToHome(to);
}
return true;
});
Your question is rather old.
Your error is the "#click.prevent". This statement does not work, because your button is a submit button (so your event is called twice).
Use "#submit.prevent" should work (or change the type of your button).
My solution is a mix of extending prototype with check Navigation Duplicated Error. Other errors and warning should be visible. After a week on production - no NavigationDuplicated and everything is working.
import { equals } from 'ramda'
export function register(Vue) {
const routerPush = Router.prototype.push
const routerReplace = Router.prototype.push
const isNavigationDuplicated = (currentRoute, nextRoute) => {
const { name: nextName, params: nextParams = {}, query: nextQuery = {} } = nextRoute
const { name, params, query } = currentRoute
return equals(nextQuery, query) && equals(nextParams, params) && equals(nextName, name)
}
Router.prototype.push = function push(location) {
if (!isNavigationDuplicated(this.currentRoute, location)) {
return routerPush.call(this, location)
}
}
Router.prototype.replace = function replace(location) {
if (!isNavigationDuplicated(this.currentRoute, location)) {
return routerReplace.call(this, location)
}
}
Vue.use(Router)
}
I noticed that error comes up when I tried to replace url query parameter with same value.
I have select filters and url query string params are in sync with their values. It works well as long as you change to a new value. If value remains the same (for example coming back from history) and thus replacing query string parameter with same value, error pops out.
Solution was to check if value is changed, and then replace query param in router:
let newValue = 'foo'; // new query value for parameter
let qcopy = { ...this.$route.query }; // clone current query
// prevent NavigationDuplicated: Avoided redundant navigation to current location
if (qcopy['your_param'] != newValue){
qcopy['your_param'] = newValue;
this.$router.replace({query: qcopy});
}
Stop the click propagation from hitting the router action
I have a high-level answer here that may be useful to others. It may not directly answer the OP, but this thinking applies.
My take on this is to NOT fiddle around with global configuration or attempt to catch router logic. It doesn't make sense to pollute your app with exceptions all over the place.
In the case of a results view with filters. If the filters happen to be presented as router links (for various reasons), but we don't want actual router logic to occur when they're clicked.
So, capture the click before it gets to the router action!
Then you get the best of both worlds:
Search filters (as a link) that...
Do logic within a view
Still provide the benefit of being presented as a link
(benefits to bot scans, user convenience and accessibility, etc)
Technique:
Use #click.prevent on a child inside the router-link, to capture and stop the click from hitting the router.
Example:
Before:
Router logic occurs, even though we're in the route already
<router-link class="nav-link" :to="{name: 'MovieSection'}" >
<i class="fas fa-film"></i>Peliculas
</router-link>
After:
Router logic inhibited, we run other logic (applyFilter)
<router-link class="nav-link" :to="{name: 'MovieSection'}" >
<div #click.prevent="myFunc(something)">
<i class="fas fa-film"></i>Peliculas
</div>
</router-link>
With this approach you may avoid messy high-level exceptions in your app.
Best practice would be:
import { isNavigationFailure, NavigationFailureType } from 'vue-router/src/util/errors';
this.$router.push(...).catch((error) => {
if (!isNavigationFailure(error, NavigationFailureType.duplicated))
throw error
);
}
See the Vue Router docs
This worked for me in the router/index.js init. You can catch can handle the exception types or message you want in the call() method to ignore certain conditions or throw certain ones.
See
//HANDLE ROUTE ERRORS HERE
comment
import Vue from "vue";
import VueRouter from "vue-router";
import routes from "./routes";
Vue.use(VueRouter);
/*
* If not building with SSR mode, you can
* directly export the Router instantiation;
*
* The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Router instance.
*/
export default function(/* { store, ssrContext } */) {
const router = new VueRouter({
scrollBehavior: () => ({ x: 0, y: 0 }),
routes,
// Leave these as they are and change in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
mode: "history"
});
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
var token = window.localStorage.getItem("token");
if (token != null) {
var decodedtoken = JSON.parse(atob(token.split(".")[1]));
var role =
decodedtoken[
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
];
if (role != "Admin") {
next("/accessdenied");
} else {
next();
}
} else {
next("/login");
}
} else {
next();
}
if (to.meta.requiresLogin) {
var token = window.localStorage.getItem("token");
if (token != null) {
next();
} else {
next("/login");
}
}
});
//HANDLE ROUTE ERRORS HERE
const originalPush = router.push;
router.push = function push(location) {
return originalPush.call(this, location).catch(err => {
console.warn("router>index.js>push()", err);
});
};
return router;
}
Based on #Abhishek Shastri's answer
Here's is a simple and efficient solution:
if(from.fullPath === to.fullPath){
return
}
I want to call, depending on the value of the variable, this.loggedInService.isLoggedIn methods: login() and logout()
If the value of the variable !this.loggedInService.isLoggedIn then
call login()
If !this.loggedInService.isLoggedIn then this method logout().
How to implement it correctly in app.html ?
template:
<li class="nav-item">
<a class="btn btn-outline-success"
[class.btn-outline-success]="!this.loggedInService.isLoggedIn"
[class.btn-outline-danger]="this.loggedInService.isLoggedIn"
ngIf ....>
{{this.loggedInService.isLoggedIn ? 'Exit' : 'Enter'}}
</a>
</li>
app.ts:
export class AppComponent implements OnInit {
constructor(public loggedInService: LoggedinService, public router: Router) {}
ngOnInit() {}
login(): void {
this.loggedInService.login().subscribe(() => {
if (this.loggedInService.isLoggedIn) {
let redirect = this.loggedInService.redirectUrl
? this.loggedInService.redirectUrl
: '/gallery';
this.router.navigate([redirect]);
}
});
}
logout(): void {
this.loggedInService.logout();
}
}
You can use ternary operator to run a function based on state like this
<li (click)="this.loggedInService.isLoggedIn ? logout() : logIn()" >
{{this.loggedInService.isLoggedIn ? logout : logIn}}
</li>
Let this logic move ts file and Just create one function toggleLogin() in ts file and call it from the html.
In HTML
<li class="nav-item">
<a class="btn btn-outline-success"
[class.btn-outline-success]="!this.loggedInService.isLoggedIn"
[class.btn-outline-danger]="this.loggedInService.isLoggedIn"
(click)="toggleLogin()"
>
{{this.loggedInService.isLoggedIn ? 'Exit' : 'Enter'}}
</a>
</li>
in ts file
toggleLogin(): void {
if(this.loggedInService.isLoggedIn){
this.logout();
}else{
this.login();
}
}
call a logInOrOut function that will check this.loggedInService.isLoggedIn and then call the appropriate function
I have Component which has a member array variable. This array is bind to DOM with *ngFor. When I add new variable to array my view changes accordingly. Array holds tab names and initially it is set to have only 1 tab. When I refresh page array reinitialized which is what I was expecting. But when I logout and then log back in(router navigation) I see all previous tabs. It is weird to me, because if I console.log(myTabs) array has only 1 element(homeTab).
UPDATE:
.html
<div style="display: table-caption" id="notify-tabs">
<ul class="nav nav-tabs" role="tablist" id="nav-bar">
<li role="presentation" data-toggle="tab" id="homeTab" [class.active]="activeTab==='homeTab'"><a (click)="setValues('home')">Home</a>
<li role="presentation" *ngFor="let tab of myTabs" data-toggle="tab" id={{tab}} [class.active]="activeTab===tab.toString()"><a (click)="setValues(tab)">{{tab}}</a>
</ul>
</div>
.component.ts
#Component({
selector: 'notify-homepage',
templateUrl: 'app/home/home.component.html',
styleUrls: ['styles/css/bootstrap.min.css', 'styles/home.css'],
directives: [DynamicComponent, TileComponent, MapComponent, HeaderComponent, ConversationComponent, ROUTER_DIRECTIVES]
})
export class HomeComponent{
public myTabs: number[] = [21442];
public activeTab: string = 'homeTab';
ngOnInit() {
//Assume fully operating MapService here
this.subscription = this.mapService.conversationId.subscribe(
(id: number) => {
this.myTabs.push(id);
this.setValues(id);
this.activeTab = id.toString();
})
}
ngOnDestroy() {
this.subscription.unsubscribe();
...
}
}
map.service.ts
#Injectable()
export class MapService {
private conversationIdSource = new ReplaySubject<number>();
public conversationId = this.conversationIdSource.asObservable();
...
showConversation(id: number) {
this.conversationIdSource.next(id);
}
}
The answer of #Andrei works, but in my opinion there's a better and more elegant solution.
Just use a combination of #ViewChild() and setters.
For example:
// component.html
<ng-el ... #myElement>
// component.ts
#ViewChild('myElement') set(el) {
if (el) {
console.log('element loaded!');
}
}
Check Lifecycle hooks:
OnChanges https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html#!#onchanges
DoCheck https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html#!#docheck
They help tracking changing in Input and local variables.
OnChanges for Input variables:
ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
for (let propName in changes) {
let chng = changes[propName];
let cur = JSON.stringify(chng.currentValue);
let prev = JSON.stringify(chng.previousValue);
this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
}
}
DoCheck for everything:
ngDoCheck() {
if (this.hero.name !== this.oldHeroName) {
this.changeDetected = true;
this.changeLog.push(`DoCheck: Hero name changed to "${this.hero.name}" from "${this.oldHeroName}"`);
this.oldHeroName = this.hero.name;
}
}