Is it possible to call a component method in nuxt from a page? - vue.js

I'm starting to work with Vue.js and I'm using Nuxt.js.
I've created a component (a snackbar) and inside this component I created a method "showSnackbar" that works passing 2 parameters: color and text.
So when I call showSnackbar(color,text), it appears.
But, I want to call this method from a page. Because I want to use this snackbar in some pages and I don't want to write the same code all the time, so that's the reason why I decided to create a component. But I can't call from a page the method inside this component.
And that's why I wonder if is it that possible to call a component method from a page (where of course I import the component)

There might be a couple of ways to do it, I would create a plugin.
Then you have both <snackbar/> component for placement and a global API to call the invoking method this.$snackbar.open({someOptions: '...'})
For example:
Create a folder in ./plugins/snackbar and place the following in:
./plugins/snackbar/index.js
import Vue from "vue";
import snackbar from "~/plugins/snackbar/snackbar";
Vue.use(snackbar);
This is for nuxt.config.js, to load globally. Which looks like:
...
/*
** Plugins to load before mounting the App
** Doc: https://nuxtjs.org/guide/plugins
*/
plugins: ["~/plugins/snackbar/index.js"],
...
ok, now create
./plugins/snackbar/snackbar.js
This is the plugin which holds state for the component and acts as an event proxy
import snackbar from "~/plugins/snackbar/snackbar.vue";
const Plugin = {
install(Vue, options = {}) {
/**
* Makes sure that plugin can be installed only once
*/
if (this.installed) {
return;
}
this.installed = true;
/**
* Create event bus
*/
this.event = new Vue();
/**
* Plugin methods
*/
Vue.prototype.$snackbar = {
show(options = {}) {
Plugin.event.$emit("show", options, true);
}
};
/**
* Registration of <snackbar/> component
*/
Vue.component("snackbar", snackbar);
}
};
export default Plugin;
and now...
./plugins/snackbar/snackbar.vue
Where the magic happens...
<template>
<div>
<transition name="snackbar">
<div v-if="show" :class="['snackbar', 'box-shadow', type]">
<slot>{{ options.text }}</slot>
</div>
</transition>
<pre>options: {{ options }}</pre>
<pre>show: {{ show }}</pre>
<pre>type: {{ type }}</pre>
</div>
</template>
<script>
import snackbar from "~/plugins/snackbar/snackbar";
export default {
data: () => ({
options: {
text: "",
type: ""
},
show: false,
type: "",
timer: 0
}),
beforeMount() {
snackbar.event.$on("show", options => {
this.options = options;
this.type = options.type;
this.show = true;
this.close(this.options.closeWait || 3000);
});
},
methods: {
close(timeout) {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.show = false;
}, timeout);
}
}
};
</script>
<style>
.snackbar {
min-width: 300px;
margin-left: -150px;
background-color: #F48024;
color: #fff;
text-align: center;
border-radius: 5px;
padding: 16px;
position: fixed;
z-index: 1;
left: 50%;
bottom: 30px;
}
.snackbar.success {
background-color: rgb(71, 244, 36);
}
.snackbar.danger {
background-color: rgb(244, 36, 47);
}
.snackbar-enter-active {
animation: snackbar-in 0.8s;
}
.snackbar-leave-active {
animation: snackbar-in 0.8s reverse;
}
#keyframes snackbar-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
.box-shadow {
-webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3),
0 0 40px rgba(0, 0, 0, 0.1) inset;
-moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3),
0 0 40px rgba(0, 0, 0, 0.1) inset;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3), 0 0 40px rgba(0, 0, 0, 0.1) inset;
}
</style>
Then within any component/pages which uses it, you can place with <snackbar/>, and call the methods like:
this.$snackbar.show({
text: "Hello, snackbar!",
type: "success"
});
A working example of the above can be found here https://codesandbox.io/s/codesandbox-nuxt-oeo4h

Related

Using wheelnav.js on Vue 3 project

So I have seen this pie menu generator which gives you an HTML, CSS and JS code and I wanted to use it in my Vue 3 project. I am new to vue and this was how I imported it.
here is the link to the pie menu generator: http://pmg.softwaretailoring.net/
I did this in my wrapper component. Somehow this bought many errors in which I think is because of how I imported the JS library.
<template>
<div class="context-menu" v-show="show" :style="style" ref="context" tabindex="0" #blur="close">
<div id='piemenu' data-wheelnav data-wheelnav-slicepath='DonutSlice' data-wheelnav-marker
data-wheelnav-markerpath='PieLineMarker' data-wheelnav-rotateoff data-wheelnav-navangle='270'
data-wheelnav-cssmode data-wheelnav-init>
<div data-wheelnav-navitemtext='0' onmouseup='alert("Place your logic here.");'></div>
<div data-wheelnav-navitemtext='1' onmouseup='alert("Place your logic here.");'></div>
</div>
</div>
</template>
<script>
import '../assets/raphael.min.js'
import '../assets/raphael.icons.min.js'
import '../assets/wheelnav.min.js'
var piemenu = new wheelnav('piemenu');
piemenu.wheelRadius = piemenu.wheelRadius * 0.83;
piemenu.createWheel();
export default {
name: 'CmpContextMenu',
props: {
display: Boolean, // prop detect if we should show context menu
},
data() {
return {
left: 0, // left position
top: 0, // top position
show: false, // affect display of context menu
};
},
computed: {
// get position of context menu
style() {
return {
top: this.top + 'px',
left: this.left + 'px',
};
},
},
methods: {
// closes context menu
close() {
this.show = false;
this.left = 0;
this.top = 0;
this.myTrigger = false;
console.log('trigger false');
},
open(evt) {
this.show = true;
// updates position of context menu
this.left = evt.pageX || evt.clientX;
this.top = evt.pageY || evt.clientY;
},
},
};
</script>
<style>
.context-menu {
position: fixed;
z-index: 999;
cursor: pointer;
}
#piemenu>svg {
width: 100%;
height: 100%;
}
#piemenu {
height: 400px;
width: 400px;
margin: auto;
}
#media (max-width: 400px) {
#piemenu {
height: 300px;
width: 300px;
}
}
[class|=wheelnav-piemenu-slice-basic] {
fill: #497F4C;
stroke: none;
}
[class|=wheelnav-piemenu-slice-selected] {
fill: #497F4C;
stroke: none;
}
[class|=wheelnav-piemenu-slice-hover] {
fill: #497F4C;
stroke: none;
fill-opacity: 0.77;
cursor: pointer;
}
[class|=wheelnav-piemenu-title-basic] {
fill: #333;
stroke: none;
}
[class|=wheelnav-piemenu-title-selected] {
fill: #fff;
stroke: none;
}
[class|=wheelnav-piemenu-title-hover] {
fill: #222;
stroke: none;
cursor: pointer;
}
[class|=wheelnav-piemenu-title]>tspan {
font-family: Impact, Charcoal, sans-serif;
font-size: 24px;
}
.wheelnav-piemenu-marker {
stroke: #444;
stroke-width: 2;
}
</style>

Reinitialize matterjs after page changes in nuxtjs

I have a matterjs instance in my nuxt app that drops items on the floor. Everything works when I visit the page for the first time or do a page refresh. But when I change the pages (routes) inside my app, so I come back to the page with the matterjs instance, the instance is gone. I always have to do a page refresh...
How can I reinitialize matterjs?
Fallbox
<section class="fallbox">
<div class="fallbox-content">
<nuxt-link to="/"><h1>Index</h1></nuxt-link>
</div>
<div class="fallbox-scene">
<div v-for="item in items" :key="item.className">
<span :class="item.className" class="item"></span>
</div>
</div>
</section>
export default {
data() {
return {
items: [
{
className: "-i1",
},
{
className: "-i2",
},
{
className: "-i3",
},
{
className: "-i4",
},
{
className: "-i5",
},
],
};
},
mounted() {
window.addEventListener("DOMContentLoaded", () => {
this.startFallbox();
});
},
methods: {
startFallbox() {
const Engine = Matter.Engine;
const Render = Matter.Render;
const Runner = Matter.Runner;
const Bodies = Matter.Bodies;
const Body = Matter.Body;
const Composite = Matter.Composite;
const MouseConstraint = Matter.MouseConstraint;
const engine = Engine.create();
const world = engine.world;
engine.gravity.y = 1;
const fallbox = document.querySelector(".fallbox-scene");
const render = Render.create({
element: fallbox,
engine,
options: {
width: fallbox.offsetWidth,
height: fallbox.offsetHeight,
},
});
// Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);
const itemArray = this.items;
itemArray.forEach((i) => {
const get = document.getElementsByClassName(i.className)[0];
get.style.opacity = 1;
const item = {
w: get.clientWidth,
h: get.clientHeight,
body: Bodies.rectangle(
Math.random() * window.innerWidth,
Math.random() * -1000,
get.clientWidth,
get.clientHeight,
{
restitution: 0.5,
angle: Math.random() * 360,
}
),
elem: get,
render() {
const { x, y } = this.body.position;
this.elem.style.top = `${y - this.h / 2}px`;
this.elem.style.left = `${x - this.w / 2}px`;
this.elem.style.transform = `rotate(${this.body.angle}rad)`;
},
};
Body.rotate(item.body, Math.random() * 360);
Composite.add(world, [item.body]);
(function rerender() {
item.render();
requestAnimationFrame(rerender);
})();
});
const ground = Bodies.rectangle(
fallbox.offsetWidth / 2,
fallbox.offsetHeight,
2000,
1,
{
isStatic: true,
}
);
const left = Bodies.rectangle(
0,
fallbox.offsetHeight / 2,
1,
fallbox.offsetHeight,
{
isStatic: true,
}
);
const right = Bodies.rectangle(
fallbox.offsetWidth,
fallbox.offsetHeight / 2,
1,
fallbox.offsetHeight,
{
isStatic: true,
}
);
Composite.add(world, [ground, left, right]);
const mouseConstraint = MouseConstraint.create(engine, {
element: fallbox,
constraint: {
stiffness: 0.2,
},
});
mouseConstraint.mouse.element.removeEventListener(
"mousewheel",
mouseConstraint.mouse.mousewheel
);
mouseConstraint.mouse.element.removeEventListener(
"DOMMouseScroll",
mouseConstraint.mouse.mousewheel
);
Composite.add(world, mouseConstraint);
Render.lookAt(render, {
min: { x: 0, y: 0 },
max: { x: fallbox.offsetWidth, y: fallbox.offsetHeight },
});
},
},
};
.fallbox {
position: relative;
height: 100vh;
width: 100%;
margin: auto;
background: black;
overflow: hidden;
.fallbox-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 95%;
max-width: 1050px;
z-index: 2;
// -khtml-user-select: none;
// -moz-user-select: none;
// -ms-user-select: none;
// user-select: none;
// pointer-events: none;
h1 {
font-size: 160px;
font-weight: 500;
line-height: 140px;
margin-bottom: 150px;
text-align: center;
color: white;
}
}
.fallbox-scene {
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
contain: strict;
.item {
height: 120px;
width: 120px;
background: red;
position: absolute;
opacity: 0;
user-select: none;
will-change: transform;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
}
}
}
activated() {
this.startFallbox()
}
Use the activated hook to restart the animation. The code when mounted() is still needed.
I assume the problem is that when you navigate away and you some back the initial animation is finished, but as Nuxt caches some pages they are served from cache and therefore the mounted() hook is not called. This is where the activated() hooks comes in. It's called when a page/component was keept alive and is reactivated.

Vue 3 cli-service app: "Slot "default" invoked outside of the render function" warning when component with slots is imported from other component

MCVE
I have a Tabpane component that takes slots as input. When imported from the template it works as expected.
<Tabpane>
<div caption="I am div 1">Div 1</div>
<div caption="I am div 2">Div 2</div>
</Tabpane>
However when imported from an other component ( Composite in the example ), then it triggers the following warning:
Slot "default" invoked outside of the render function:
this will not track dependencies used in the slot. Invoke the slot function inside the render function instead.
// src/components/Composite.js
import { defineComponent, h } from "vue";
import Tabpane from "./Tabpane.vue";
export default defineComponent({
name: "Composite",
setup() {
const slots = [
h("div", { caption: "I am div 1" }, ["Div 1"]),
h("div", { caption: "I am div 2" }, ["Div 2"])
];
return () => h(Tabpane, {}, () => slots);
}
});
Solved.
The problem was that I called slots.default() from within setup, but not within the returned render function.
Also this component reflected a very beginner approach to reactivity. By now I know better. The old problematic solution is still there in src/components/Tabpane.vue.
The right solution that triggers no warning is:
// src/components/Tabpane2.vue
<script>
import { defineComponent, h, reactive } from "vue";
export default defineComponent({
name: "Tabpane2",
props: {
width: {
type: Number,
default: 400,
},
height: {
type: Number,
default: 200,
},
},
setup(props, { slots }) {
const react = reactive({
selectedTab: 0,
});
return () =>
h("div", { class: ["vertcont"] }, [
h(
"div",
{
class: ["tabs"],
},
slots.default().map((tab, i) =>
h(
"div",
{
class: {
tab: true,
selected: i === react.selectedTab,
},
onClick: () => {
react.selectedTab = i;
},
},
[tab.props.caption]
)
)
),
h(
"div",
{
class: ["slotscont"],
style: {
width: `${props.width}px`,
height: `${props.height}px`,
},
},
slots.default().map((slot, i) =>
h(
"div",
{
class: {
slot: true,
active: react.selectedTab === i,
},
},
[slot]
)
)
),
]);
},
});
</script>
<style>
.tab.selected {
background-color: #efe;
border: solid 2px #afa !important;
border-bottom: transparent !important;
}
.tab {
background-color: #eee;
}
.tabs .tab {
padding: 5px;
margin: 2px;
border: solid 2px #aaa;
border-radius: 8px;
border-bottom: transparent;
cursor: pointer;
user-select: none;
transition: all 0.5s;
color: #007;
}
.tabs {
display: flex;
align-items: center;
margin-left: 5px;
}
.vertcont {
display: flex;
flex-direction: column;
margin: 3px;
}
.slotscont {
position: relative;
overflow: scroll;
padding: 5px;
border: solid 1px #777;
}
.slot {
visibility: hidden;
position: absolute;
}
.slot.active {
visibility: visible;
}
</style>
Slots need to be invoked within the render function and or the <template> box to ensure they keep their reactivity.
A full explanation can be found in this post: https://zelig880.com/how-to-fix-slot-invoked-outside-of-the-render-function-in-vue-3

Vue 3 Composition API component doesn't work reactivate class

I have a button and I just want it animate on a click - make as it been pressed. It works if I make optional component as below:
<template>
<button class="button" :class="{'shadow__click': classButton}" #click="buttonClass">
Tell me already!
</button>
</template>
<script>
export default {
data(){
return {
classButton : false
}
},
methods: {
buttonClass(){
this.classButton = true;
setTimeout(() => {
this.classButton = false
}, 1000)
}
}
}
</script>
<style lang="less">
.button{
cursor: pointer;
padding: 12px 24px;
position: relative;
border-radius: 5px;
background-color: #38b2ac;
border: none;
color: #fff8e1;
}
.shadow__click{
animation: click 1s
}
#keyframes click {
0% {
top: 0;
left: 0;
}
25% {
top: 5px;
left: 1px;
}
50% {
top: 10px;
left: 3px;
}
75% {
top: 5px;
left: 1px;
}
100% {
top: 0;
left: 0;
}
}
</style>
but it doesn't want to work when I do Composition way and I don't see a problem but it simply doesn't work (( I console.loged function and it goes to function changes the value of a variable, but class is not applying. Is it a Vue 3 bug?
<script>
import { ref } from "vue"
setup(){
let classButton = ref(false);
function buttonClass(){
classButton = true;
setTimeout(() => {
classButton = false
}, 1000)
}
return { classButton, buttonClass}
}
</script>
You should mutate the ref using the value field :
let classButton = ref(false);
function buttonClass(){
classButton.value = true;
setTimeout(() => {
classButton.value = false
}, 1000)
}

How to display a component when page is loading in nuxt

I am quite new to nuxt, and I need help here.
async asyncData({ params, route }) {
const { data } = await axios.get(
`${process.env.baseUrl}/homes/?search=${
params.search
}&home_status=${1}`
)
return {
homes: data.results,
}
}
I am trying to populate my component with data(using asyncData), but I want my skeleton loader to show if my page is loading. How do I do that in nuxt?
Here is the code for my skeleton loader;
<template>
<div class="placeholder-container">
<div class="placeholder wave">
<div class="square"></div>
<div class="line"></div>
<div class="line"></div>
<div class="line"></div>
</div>
</div>
</template>
<style scoped>
.placeholder-container {
width: 35rem;
margin: 15px auto 15px auto;
}
.placeholder {
padding: 10px;
width: 100%;
// border: 1px solid lightgrey;
display: flex;
flex-direction: column;
}
.placeholder div {
background: #e8e8e8;
}
.placeholder .square {
width: 100%;
height: 22rem;
border-radius: 1rem;
margin: 0 0 10px;
}
.placeholder .line {
height: 12px;
margin: 0 0 10px 0;
}
.placeholder .line:nth-child(2) {
width: 120px;
}
.placeholder .line:nth-child(3) {
width: 180px;
}
.placeholder .line:nth-child(4) {
width: 150px;
}
.placeholder.wave div {
animation: wave 1s infinite linear forwards;
-webkit-animation: wave 1s infinite linear forwards;
background: #f6f7f8;
background: linear-gradient(to right, #eeeeee 8%, #dddddd 18%, #eeeeee 33%);
background-size: 800px 104px;
}
#keyframes wave {
0% {
background-position: -468px 0;
}
100% {
background-position: 468px 0;
}
}
#-webkit-keyframes wave {
0% {
background-position: -468px 0;
}
100% {
background-position: 468px 0;
}
}
</style>
What I normally do without using nuxt, is to create a data variable(loading=true), and change it to false after I finish making the api call, but since asyncData runs in the server, how do I make that work? I will also appreciate it if there is a better way of doing something like this
Placeholder
To display a placeholder component on a particular page
during loading, switch from asyncData to the fetch hook, which exposes the $fetchState.pending flag that is set to true when complete:
<template>
<div>
<MyLoading v-if="$fetchState.pending" />
<MyContent v-else :posts="posts" />
</div>
</template>
<script>
export default {
data() {
return {
posts: []
}
},
async fetch() {
const { data } = await this.$axios.get(...)
this.posts = data
}
}
</script>
Customizing loading progress bar
Nuxt provides a default loading progress bar that appears at the top of the app while a page is loading. You could customize the progress bar's appearance:
// nuxt.config.js
export default {
loading: {
color: 'blue',
height: '5px'
}
}
Or you could specify your own custom loading component instead:
// nuxt.config.js
export default {
loading: '~/components/MyLoading.vue'
}
demo