Vue $emit and #click handler behaving differently on Firefox - vue.js

New to Vue and wrapping my head around the main concepts.
I currently have two separate components BodyHero.vue and SelectedHero.vue that I need to pass data from BodyHero.vue to SelectedHero.vue. Since there isn't a parent-child relationship between them I've set up a simple EventBus.
import Vue from 'vue'
export const EventBus = new Vue()
I've set up a simple emitter to pass information from BodyHero.vue to SelectedHero.vueupon click on router-link. (I'm using vue-router to route pages and the routes work appropriately - upon click the user is taken from BodyHero.vue to SelectedHero.vue.)
This is a portion of BodyHero.vue:
<template>
<div class="columns" style="margin: 0px 10px">
<div v-for="cryptoCurrency in firstFiveCryptoCurrencies" class="column">
<router-link to="/selected" #click.native="selectCryptoCurrency(cryptoCurrency)">
<div class="card">
<div class="card-image">
<figure class="image is-4by3">
<img :src="`/static/${cryptoCurrency.id}_logo.png`">
</figure>
</div>
<div class="card-content">
<p class="title is-5">{{ cryptoCurrency.name }}</p>
</div>
</div>
</router-link>
</div>
</div>
</template>
<script>
import { EventBus } from '../../event-bus.js'
const cryptoCurrencyData = require('../../cryptocurrency-data.json')
export default {
props: {},
name: 'bodyHero',
selectCryptoCurrency (cryptoCurrency) {
EventBus.$emit('cryptoCurrencySelected', cryptoCurrency)
}
And in SelectedHero.vue, upon created I listen for the emit.
created () {
EventBus.$on('cryptoCurrencySelected', cryptoCurrency => {
...
})
}
Now for the weird part. Chrome/Safari everything seems to work fine - the user is routed to the next component, the emit is successfully called and the cryptoCurrency object is appropriately passed.
For Firefox however, it appears that the EventBus.$emit('cryptoCurrencySelected', cryptoCurrency) doesn't seem to fire upon click (#click.native). So the user is directed to the next screen - and the object is empty. Not sure what's really the issue here or if I'm missing something. Would be happy to hear other opinions!

Related

Nanostores in Astro is not working as expected with Vue... does not sync or hold state on page re-render

I am trying to create an online store with astro while using Vue componenents (SSR suing Netlify as an Adapter).
It's been a good learning experience and I am intentionally not using NUXT as I'd like to try out the SSR option of Astro with Vue Components. However if I feel this does not work out then I might finally decide on Nuxt. So far everything seems to be really nice with Astro and Vue.. but now I've run into a roadblock.
State Management
Astro recommends Nanostores as the state management option for Astro projects. https://docs.astro.build/en/core-concepts/sharing-state/
So I've tried it and it seemed to work well but now I am realising that when I change the page the state is lost. I am using SSR as the rendering option and using netlify as the adapter.
For example I have this in my cartStore.js
import { atom, map } from 'nanostores';
export const cartItems = map({});
export function addCartItem({ id, name, imageSrc }) {
const existingEntry = cartItems.get()[id];
if (existingEntry) {
cartItems.setKey(id, {
...existingEntry,
quantity: existingEntry.quantity + 1,
})
} else {
cartItems.setKey(
id,
{ id, name, imageSrc, quantity: 1 }
);
}
}
and this in my AddToCartButton.vue
<template>
<button
type="button"
class="flex max-w-xs flex-1 items-center justify-center rounded-md border border-transparent bg-orange-600 py-3 px-8 text-base font-medium text-white hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 focus:ring-offset-gray-50 sm:w-full"
#click="addToCart"
>
Add to bag
</button>
<pre>{{ $cartItems }}</pre>
</template>
<script setup>
import { addCartItem, cartItems } from "./../../store/cartStore";
import { useStore } from "#nanostores/vue";
const $cartItems = useStore(cartItems);
const addToCart = function () {
// Do something before adding to cart like open the cart slider
//
// Adding Item to Cart
addCartItem(props.productItem);
};
const props = defineProps({
productItem: {
type: Object,
},
});
</script>
Now when I use this, I expect my CartIcon in my header to update itself to the $cartItem.length based on the code below for my CartIcon.vue
<template>
<div class="ml-4 flow-root lg:ml-8">
<a href="/cart/123" class="group -m-2 flex items-center p-2">
<ShoppingBagIcon
class="h-6 w-6 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
<span
class="ml-2 text-sm font-medium text-gray-700 group-hover:text-gray-800"
>{{ $cartItems.length ? cartItems.length : 0 }}</span
>
<span class="sr-only">items in cart, view bag</span>
</a>
</div>
</template>
<script setup>
import {
ShoppingBagIcon,
} from "#heroicons/vue/24/outline";
import { cartItems } from "./../../store/cartStore";
import { useStore } from "#nanostores/vue";
const $cartItems = useStore(cartItems);
</script>
but for some reason it doesn't seem to be in sync.. I tried to view the cartItems value using <pre>{{$cartItems}}</pre> and realised that the value of cartItems is not being stored.. and I can't figure out what I am doing wrong..
Please note that I am importing separate vue compoments into astro.. and the header and footer are part of all the pages in astro.. and the cartIcon is in the header.. so in some ways it's always part of the page (just mentioning this if there's a suggestion to ensure that the components exist on the same page in astro).. Frankly the point of a state management option would be to ensure that the state is maintained irrespective of whether the components are on the same page in astro.
However I am not very experienced with SSR and I'd like to understand if there's some differences that I should expect with state management in an SPA and in an SSR..
Can someone help me out on this?
Stores are just object stored clientside, if you need to make them persistent you can use the nanostores persistent library

Vue - display/create component from a function

One thing that I have been struggling to figure out how to do better is modals. Currently, I am registering the modal component on each Vue that needs it. However, this feels rather sloppy, as I am having to register the component several times. Even using mix-ins just does not feel like an elegant solution. What would be optimal to be able to do is to mimic JavaScript's alert() method on the Vue instance. For example, be able to call this.ShowModal(Header, Body)
However, from my understanding, there is no way to accomplish this
Take for example my Modal example. You could have a modal template like this:
<script type="text/x-template" id="modal-template">
<transition name="modal">
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
<slot name="header">
default header
</slot>
</div>
<div class="modal-body">
<slot>
</slot>
</div>
<div class="modal-footer">
<slot name="footer">
default footer
<button class="modal-default-button" #click="$emit('close')">
OK
</button>
</slot>
</div>
</div>
</div>
</div>
</transition>
</script>
Then you would have to reference the component over and over again like this
<template>
<button #click="displayModal">Display the Modal Alert</button>
<modal v-if="showModal" #close="showModal = false">
<h3 slot="header"> This is a good header </h3>
<p>
Look at me I am the body! You have seen me {{displayCount}} times!
</p>
</modal>
</template>
<script>
components: {modal},
data: {
showModal: false,
displayCount: 0
},
methods: {
displayModal(){
this.displayCount++
this.showModal = true;
}
}
</script>
If you wanted to reuse the component for several messages from within the parent you would then have to add several more variables to store things such as the header and body. You could put some of the logic into a mixin but you would still have to have the clutter of adding the modal component and possibly the mixin.
This brings us to the question. Is there a way to create a function in the Vue instance that would allow for us to dynamically create a Modal component and fill in the slots with arguments passed to the function? e.g. this.ShowModal("This is a good header", "Look at me I am the body!")
Use Vue.extend() create a "modal" constructor and create a instance,you can mount it to DOM dynamically by $mount or other ways
In Modal example:
modal.vue:
<template>
<div>
{{message}} //your modal content
</div>
</template>
<script>
export default {
name: 'modal',
data(){
return {
message: '',
}
},
methods:{
/************/
close () {
/****this.$destroy()****/
}
}
}
</script>
modal.js:
import myModal from 'modal.vue'
import Vue from 'vue'
const modalConstructor = Vue.extend(myModal)
const modal = (options,DOM)=>{
const modalInstance = new modalConstructor({data:options})
modalInstance.vm = modalInstance.$mount() //get vm
const dom = DOM || document.body // default DOM is body
dom.appendChild(modalInstance.vm.$el) // mount to DOM
return modalInstance.vm
}
export default modal
now you can create a Modal component by a function like this:
import showModal from 'modal.js'
showModal({message:"..."})

How to update properties of component for dynamic router-link in vuejs

So new to Vue and haven't found anything that specifically addresses my issue.
I have a simple Vue app using VueRouter where I am trying to generate a bracket-style sports tournament that records the outcomes of the different games in the tournament.
I need to make an asynchronous axios call to a server to get info on a specific game. I do not know how to update my component properly to get this info.
App.vue is very simple. The home page is just an overview of the bracket
<template>
<div id="app">
<div id="nav">
<router-link :to="{ name: 'bracket'}">Bracket</router-link>
</div>
<router-view />
</div>
</template>
I have a view component, Bracket.vue, and, for now, all I want this view do is provide links to the different matchups in the tourney.
I have made this work pretty well dynamically as follows:
<template>
<div class="home">
<div id="nav">
<div v-for="round in rounds" :key="round">
<router-link v-for="game in gamesPerRound" :key="matchupKey(round, game)" :to="{ name: 'matchup', params: {round: round, game: game} }">Matchup {{ round }} {{ game }}</router-link>
</div>
</div>
</div>
</template>
When link is clicked, I pull up another view component, Matchup.vue, which I would like to display certain data about the matchup. (MatchupService is an axios API instance)
I pass the round # and the game # as props via router-link. These load fine in the Matchup.vue.
However, when I try to make an asynchronous call to get the matchup data, nothing happens. the matchup property never updates, not on the link click or ever. So I either get nothing (when I use a ternary as per below) or an error if I just try to use the matchup value
Matchup.vue
<template>
<div class="matchup">
<h1>Round {{ round }}</h1>
<h2>Game {{ game }}</h2>
<h6>Team 1: {{matchup.teams ? matchup.teams[0]: 0}}</h6>
<h6>Team 2: {{matchup.teams ? matchup.teams[1] : 0}}</h6>
</div>
</template>
<script>
import MatchupService from '#/services/MatchupService.js'
export default {
props: ["round", "game"],
data() {
return {
matchup: {},
}
},
async updated() {
let matchups = await MatchupService.getMatchups()
this.matchup = matchups.data.rounds[this.round].games[this.game]
},
}
</script>
I have tried many different approaches on this: updated vs. created vs. other hooks, I've tried to update a property post-load and hook that to v-if, etc.
I am just totally stymied on this so hoping for some help.
Looks like you need to use navigation hook beforeEnter in your router.js file or you can use beforeRouteEnter hook directly in your compoennt file. (NOTICE! Using beforeRouteEnter in a component file you can't access 'this', so maybe, there is a reason to use Vuex if you want to store some data within serfing your app). There you can define/fetch/set any data before redirect user to a specific page. By this way you do any actions you want and set it before redirecting.
More about you can find here:
https://router.vuejs.org/guide/advanced/navigation-guards.html

Amplify Vue amplify-authenticator component not displaying

I have been using an <amplify-authenticator> component on the Home.vue view of a web application, via the HelloWorld.vue component. This was working as expected, with signedIn state monitored using store.js and Vuex.
I have now switched to routing to Home.vue or Login.vue depending on sign-in state.
However, when a signed out user correctly routes to Login.vue, the page displays as expected, minus the <amplify-authenticator> component. It is clear from the Styles that this is importing (amazonOrange is listed as a color), but for some reason the sign-in interface is no longer displaying correctly.
Login.vue
<template>
<div class="login">
<div>
<vue-headful title="Login"/>
</div>
<div>
<p>
<router-link to="/">Login</router-link> |
<router-link to="/about">About</router-link>
</p>
</div>
<div>
<p><img alt="Vue logo" src="../assets/logo.png" /></p>
</div>
<div>
<amplify-authenticator></amplify-authenticator>
</div>
</div>
</template>
<script>
import { Auth } from "aws-amplify";
import { AmplifyEventBus } from "aws-amplify-vue";
export default {
name: "Login",
props: {
msg: String
}
};
</script>
If I change amplify-authenticator to amplify-sign-in then I see a sign in interface, but this needs additional scripting to implement.
I'd like to know specifically why the Authenticator component isn't displaying, since I cannot see an obvious bug in my code.
Solved. If the user is signed in to Amazon Cognito, then the <amplify-authenticator> will not display. The problem was with a bug in the check on signedIn state, which was redirecting the user to the wrong page.

Components and Sub components

I'm new to Vue.js and I'm having a bit of trouble using components with sub-components. I have the following .vue files
app.vue
<template>
<section>
<menu></menu>
<h1>Create Your MIA</h1>
<div id="board"></div>
<slider>
<skin></skin>
</slider>
</section>
</template>
slider.vue
<template>
<div id="slider-panel">
<h3>{{* heading}}</h3>
<div class="slider">
<slot>
Some content
</slot>
</div>
</div>
</template>
<script>
import skin from "./skin";
export default {
components: {
skin: skin
}
};
</script>
skin.vue
<template>
<div v-for="colour in colours">
<div :style="{ backgroundColor: colour }">
<img src="../assets/images/MIA.png"/>
</div>
</div>
</template>
<script>
export default {
data() {
return {
heading: "Choose Skin Tone"
};
}
};
</script>
I'm trying to load the skin sub component into the component. Everything works well except for the skin sub component as it doesn't get compiled. I do not get any compile or vue related errors though. I also wanted to be able to have several instances of the slider component like this
app.vue
<template>
<section>
<menu></menu>
<h1>Create Your MIA</h1>
<div id="board"></div>
<slider>
<skin></skin>
</slider>
<slider>
<foo></foo>
</slider>
<slider>
<bar></bar>
</slider>
</section>
</template>
I'm not sure what I am doing wrong.
I'm not 100% sure of what you want to achieve here, but to compile a component inside a component, you need to add the child component inside the parent's template, like this:
Slider.vue (I've simplified the structure):
<template>
<div id="slider-panel">
<h3>{{* heading}}</h3>
<skin></skin>
</div>
</template>
<script>
import skin from './skin'
export default {
components : {
'skin': skin
}
}
</script>
App.vue:
<template>
<section>
<menu></menu>
<h1>Create Your MIA</h1>
<div id="board"></div>
<slider></slider>
</section>
</template>
Actually, if you add skin in the app's template inside of adding it in the slider component template, it gets included (and rendered) assuming that its scope is app, not slider. In order to add skin inside slider scope, it needs to be added to slider's template. Check this: https://vuejs.org/guide/components.html#Compilation-Scope
Some other things:
Use a hyphen-separated name for the components, with at least 2 words: <custom-slider> instead of <slider>, for example, following the Web Components API (otherwise it might collide with current or upcoming web components).
Slots are complicated to grasp, so read this carefully: https://vuejs.org/guide/components.html#Content-Distribution-with-Slots
Good luck!
Update:
If you want the slider component to be content agnostic and be able to insert anything you want inside it, you have two options (that I can think of):
Remove all the logic from the slider component and make skin a descendant from app. Then use slots in the slider component, as follows:
Slider.vue:
<template>
<div id="slider-panel">
<h3>{{* heading}}</h3>
<slot></slot>
</div>
</template>
<script>
export default {}
</script>
App.vue:
<template>
<section>
<menu></menu>
<h1>Create Your MIA</h1>
<div id="board"></div>
<slider>
<skin></skin>
</slider>
</section>
</template>
<script>
import skin from './skin'
export default {
skin: skin
}
</script>
If you know that the slider will always have a closed set of components inside, you can use dynamic components: https://vuejs.org/guide/components.html#Dynamic-Components
After some research I found this which refers to a is= attribute that will transclude the sub-component template
so in app.vue
<slider-component>
<div is="skin-component" v-for="colour in colours"></div>
</slider-component>
and then add child components