Nuxt/Vue/Bootstrap-vue shrink navbar on scroll - vue.js

Learning Vue with Nuxt. Want to change navbar classes depending on the page scroll position.
Looked in several places, but haven't found a working solution.
Here's what I have to work with:
``` default.vue
<template lang="pug">
div(v-on:scroll="shrinkNav", v-on:click="shrinkNav")
b-navbar.text-center(toggleable="sm" type="light" sticky v-b-scrollspy)
#myNav.mx-auto.bg-white
b-navbar-toggle(target="nav_collapse")
b-navbar-brand.mx-auto(href="#")
| Example.org
b-collapse#nav_collapse.mx-auto(is-nav='')
b-navbar-nav(justified, style="min-width: 600px").vertical-center
b-nav-item.my-auto(href='#home') Home
b-nav-item.my-auto(href='/how') How
i.fab.fa-earlybirds.fa-2x.mt-2.mb-3
b-nav-item.my-auto(href='/values') Values
b-nav-item.my-auto(href='/join-us') Join Us
#content.container(v-on:scroll="shrinkNav", v-on:click="shrinkNav")
nuxt
nuxt
</template>
<script>
// resize navbar on scroll
export default {
methods: {
shrinkNav() {
var nav = document.getElementById('nav')
var content = document.getElementById('content')
if (nav && content) {
if(content.scrollTop >= 150) {
nav.classList.add('shrink')
} else {
nav.classList.remove('shrink')
}
}
console.log(document.documentElement.scrollTop || document.body.scrollTop)
}
}
}
</script>
```
shrinkNav runs twice on click, but nothing on scroll. What is the Vue/Nuxt way to do this?

In your .vue:
<template> section:
<nav id="nav" class="navbar is-transparent is-fixed-top">
<script> section:
export default {
mounted() {
this.$nextTick(function(){
window.addEventListener("scroll", function(){
var navbar = document.getElementById("nav");
var nav_classes = navbar.classList;
if(document.documentElement.scrollTop >= 150) {
if (nav_classes.contains("shrink") === false) {
nav_classes.toggle("shrink");
}
}
else {
if (nav_classes.contains("shrink") === true) {
nav_classes.toggle("shrink");
}
}
})
})
},
}
Live Demo on codesandbox

Ok, here's a solution using Plugins. There may be a better way:
1) Define a directive in plugins/scroll.js
``` javascript
// https://vuejs.org/v2/cookbook/creating-custom-scroll-directives.html#Base-Example
import Vue from 'vue'
Vue.directive('scroll', {
inserted: function (el, binding) {
let f = function (evt) {
if (binding.value(evt, el)) {
window.removeEventListener('scroll', f)
}
}
window.addEventListener('scroll', f)
}
})
```
2) Add plugin to project in nuxt.config.js
``` javascript
module.exports = {
head: { },
plugins: [
{ src: '~/plugins/scroll.js', },
]
}
```
3) Use the v-scroll directive to define custom behavior in menu in /layouts/default.vue
``` javascript
<template lang="pug">
div(v-scroll="shrinkNav")
b-navbar.text-center(toggleable="sm" type="light" sticky)
#myNav.mx-auto.bg-white
b-navbar-toggle(target="nav_collapse")
b-navbar-brand.mx-auto(href="/#home") Example.org
b-collapse#nav_collapse.mx-auto(is-nav='')
b-navbar-nav(justified, style="min-width: 600px").vertical-center
b-nav-item.my-auto(to='/#home') Home
#content.container
nuxt
</template>
<script>
export default {
methods: {
shrinkNav() {
var scrollPosition = document.documentElement.scrollTop || document.body.scrollTop
var nav = document.getElementById('myNav')
console.log(scrollPosition, nav)
if(scrollPosition >= 150) {
nav.classList.add('shrink')
} else {
nav.classList.remove('shrink')
}
},
},
}
</script>
<style>
nav.navbar {
margin: 0;
padding: 0;
}
#myNav {
border-radius: 0 0 10px 10px;
border: 2px solid purple;
border-top: none;
}
#myNav.shrink {
border: none;
}
</style>
```

Related

Styling component nested in opened new window

I'm trying to style component mounted in new window but it seems like it's not readable. However inline styling is working. It is possible to style with style section in component?
Code explain:
When component is mounted i'm opening new window with this component as argument.
<template>
<!-- Inline styling works -->
<div style="background-color: red" class="window-wrapper" v-if="open">
<slot />
</div>
</template>
<script>
export default {
name: 'window-portal',
props: {
open: {
type: Boolean,
default: false,
}
},
data() {
return {
windowRef: null,
}
},
watch: {
open(newOpen) {
if(newOpen) {
this.openPortal();
} else {
this.closePortal();
}
}
},
methods: {
openPortal() {
this.windowRef = window.open("", "", "width=800,height=600,left=200,top=200");
this.windowRef.addEventListener('beforeunload', this.closePortal);
//here i'm opening new window with this compomnent
this.windowRef.document.body.appendChild(this.$el);
},
closePortal() {
if(this.windowRef) {
this.windowRef.close();
this.windowRef = null;
this.$emit('close');
}
}
},
mounted() {
if(this.open) {
this.openPortal();
}
},
beforeUnmounted() {
if (this.windowRef) {
this.closePortal();
}
}
}
</script>
<style scoped>
.body {
box-sizing: border-box;
}
/* here styling is not working */
.window-wrapper {
background-color: red;
}
</style>
Also components passed as slot cannot read styles from styles section.

Nuxt.js infinite-loading triggered immediately after page load

I'm using Nuxt.js with Infinite loading in order to serve more list articles as the users scrolls down the page. I've placed the infinite-loading plugin at the bottom of my list of articles (which lists, from the very beginning, at least 10 articles, so we have to scroll down a lot before reaching the end of the initial list).
The problem is that as soon as I open the page (without scrolling the page), the infiniteScroll method is triggered immediately and more articles are loaded in the list (I'm debugging printing in the console "I've been called").
I don't understand why this happens.
<template>
<main class="mdl-layout__content mdl-color--grey-50">
<subheader :feedwebsites="feedwebsites" />
<div class="mdl-grid demo-content">
<transition-group name="fade" tag="div" appear>
<feedarticle
v-for="feedArticle in feedArticles"
:key="feedArticle.id"
:feedarticle="feedArticle"
#delete-article="updateArticle"
#read-article="updateArticle"
#write-article="updateArticle"
#read-later-article="updateArticle"
></feedarticle>
</transition-group>
</div>
<infinite-loading spinner="circles" #infinite="infiniteScroll">
<div slot="no-more"></div>
<div slot="no-results"></div
></infinite-loading>
</main>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import subheader from '~/components/subheader.vue'
import feedarticle from '~/components/feedarticle.vue'
export default {
components: {
subheader,
feedarticle,
},
props: {
feedwebsites: {
type: Array,
default() {
return []
},
},
},
computed: {
...mapState({
feedArticles: (state) => state.feedreader.feedarticles,
}),
...mapGetters({
getInfiniteEnd: 'feedreader/getInfiniteEnd',
}),
},
methods: {
async updateArticle(id, status) {
try {
const payload = { id, status }
await this.$store.dispatch('feedreader/updateFeedArticle', payload)
} catch (e) {
window.console.log('Problem with uploading post')
}
},
infiniteScroll($state) {
window.console.log('I've been called')
setTimeout(() => {
this.$store.dispatch('feedreader/increasePagination')
try {
this.$store.dispatch('feedreader/fetchFeedArticles')
if (this.getInfiniteEnd === false) $state.loaded()
else $state.complete()
} catch (e) {
window.console.log('Error ' + e)
}
}, 500)
},
},
}
</script>
<style scoped>
.fade-leave-to {
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease-out;
}
</style>
Put <infinite-loading> in <client-only> tag :
<client-only>
<infinite-loading></infinite-loading>
</client-only>

Error in mounted hook: "RangeError: Maximum call stack size exceeded" VUEJS

I have following router.js in my vuejs app
import Vue from 'vue'
import Router from 'vue-router'
import {client_auth, annotator_auth} from './middleware/auth'
import {reroute_home} from '#/middleware/reroute'
import Editor from './components/editor/Editor.vue'
Vue.use(Router)
const router = new Router({
{
path: '/annotator/studio',
name: 'annotator_studio',
component: Editor,
props: (route) => ({
image_id: route.query.q
}),
meta: {
title: "Annotation Studio",
middleware: annotator_auth
}
}
]
})
function nextFactory(context, middleware, index) {
const subsequentMiddleware = middleware[index];
if (!subsequentMiddleware) return context.next;
return (...parameters) => {
context.next(...parameters);
const nextMiddleware = nextFactory(context, middleware, index + 1);
subsequentMiddleware({ ...context, next: nextMiddleware });
};
}
router.beforeEach((to, from, next) => {
if (to.meta.middleware) {
const middleware = Array.isArray(to.meta.middleware)
? to.meta.middleware
: [to.meta.middleware];
const context = {
from,
next,
router,
to,
};
const nextMiddleware = nextFactory(context, middleware, 1);
return middleware[0]({ ...context, next: nextMiddleware });
}
return next();
});
export default router;
and following is my Editor.uve
<template>
<div id="container">
<div id="view-item">
<app-view/>
</div>
</div>
</template>
<script>
import View from './view/View.vue'
export default {
components: {
'app-view': View,
}
}
</script>
<style scoped>
#container {
display: flex;
flex-flow: column;
height: 100%;
}
#stepper-item {
flex: 0 1 auto;
margin: 5px;
}
#view-item {
display: flex;
flex: 1 1 auto;
margin: 0px 5px 5px 5px;
}
</style>
In this Editor.uve i am importing a child view called View.vue. Following is my View.vue
<template lang="html">
<div
id="view"
class="elevation-2 pointers-please">
<app-osd-canvas/>
<app-annotation-canvas/>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
import OSDCanvas from './OSDCanvas.vue'
import AnnotationCanvas from './AnnotationCanvas.vue'
export default {
components: {
'app-osd-canvas': OSDCanvas,
'app-annotation-canvas': AnnotationCanvas
},
computed: {
...mapState({
projectImageName: state => state.image.projectImageName
})
},
async mounted () {
await this.setProjectImageName('demo')
this.loadDemo()
},
methods: {
...mapActions({
setProjectImageName: 'image/setProjectImageName',
loadDemo: 'editor/loadDemo',
loadProject: 'editor/loadProject'
})
}
}
</script>
<style scoped>
#view {
position: relative;
display: flex;
flex: 1 1 auto;
}
</style>
In this View.vue again i am importing a child component called OSDCanvas.
My OSDCanvas.uve looks like following.
<template lang="html">
<div id="osd-canvas" />
</template>
<script>
import { mapActions } from 'vuex'
export default {
mounted () {
this.setupOsdCanvas('osd-canvas')
},
methods: {
...mapActions({
setupOsdCanvas: 'image/setupOsdCanvas'
})
}
}
</script>
<style>
#osd-canvas {
position: absolute;
height: 100%;
width: 100%;
}
</style>
I am facing following error when i hit /annotator/studio route
[Vue warn]: Error in mounted hook: "RangeError: Maximum call stack size exceeded" found in
---> <AppOsdCanvas> at src/components/editor/view/OSDCanvas.vue
<AppView> at src/components/editor/view/View.vue
<Editor> at src/components/editor/Editor.vue
<VContent>
<VApp>
<App> at src/App.vue
<Root>
I have tried some online solutions as well but the issue is still the same. Any help is appreciated.
UPDATE
following is image/setupOsdCanvas in actions.js in store
setupOsdCanvas: ({
commit
}, payload) => {
commit('setupOsdCanvas', payload)
},
and foloowing is setupOsdCanvas in mutations.js in store
setupOsdCanvas: (state, payload) => {
state.OSDviewer = new openseadragon.Viewer({
id: payload,
showNavigationControl: false,
showNavigator: true,
navigatorId: 'navigator',
maxZoomPixelRatio: 2
})
// Prevent rotation and 'flipping' of the image through the default keybaord
// shortcuts, R and F. (these were conflicting with other annotation tool
// shortcuts when the image was open)
state.OSDviewer.addHandler('canvas-key', e => {
if (e.originalEvent.code === 'KeyR' || e.originalEvent.code === 'KeyF') {
e.preventDefaultAction = true
}
})
}

How to implement a derived render implementation in Vue.js using ES6 classes

Apologies in advance for the length of this query..
In React I can have a render function on any standard ES6 class that returns a JSX.Element. This is brilliant because it allows me to have derived ES6 classes where each derived class implements its own JSX rendering implementation - see (ts) example below:
export class Colour {
public name: string = "";
constructor(name) {
this.name = name;
}
public render(): JSX.Element {
return <span>Error, no colour specified</span>;
}
}
export class Red extends Colour {
constructor() {
super("red");
}
public render(): JSX.Element {
return <div style={{ color: this.name }}>Hi, I am {this.name}!</div>;
}
}
export class Blue extends Colour {
constructor() {
super("blue");
}
public render(): JSX.Element {
return <h2 style={{ color: this.name }}>Hi, I am {this.name}!</h2>;
}
}
Then in a React component I can create a list of Colour objects that I can render easily like so:
function App() {
const list = [];
list.push(new Red());
list.push(new Blue());
list.push(new Red());
const listItems = list.map(item => <li key={item}>{item.render()}</li>);
return (
<div className="App">
<h1>React derived rendering </h1>
<ul>{listItems}</ul>
</div>
);
}
Resulting in output like this:
and all is well...
Now my question is: Is there any way to do this as easily in Vue.js?
Please NOTE - it is important to me to create a number of derived instances in code and add these to a list to be rendered!
Right now, the closest I can make this work is to create a dummy Vue component and when it renders I actually call the derived implementation handing over the render handle 'h'.
Vue.component("fusion-toolbar", {
props: ["items"],
template: `
<div>
<div v-for="item in items"
v-bind:key="item.code" >
<f-dummy v-bind:item='item'></f-dummy>
</div>
</div>
`
});
Vue.component("f-dummy", {
props: ["item"],
render(h) {
return this.item.render(h);
}
});
export class Colour {
public colour: string = "";
constructor(colour: string) {
this.colour = colour;
}
render(h: any) {
return h("h1", "Hello, I am " + this.colour);
}
}
export class Red extends Colour {
constructor() {
super("red");
}
render(h: any) {
return h("h2", "Hello, I am " + this.colour);
}
}
export class Blue extends Colour {
private _isDisabled = true;
constructor(disabled: boolean) {
super("blue");
this._isDisabled = disabled;
}
render(h: any) {
return h('div',
[
h('h4', {
class: ['example-class', { 'conditional-class': this._isDisabled }],
style: { backgroundColor: this.colour }
}, "Hello, I am " + this.colour)
]
)
}
}
Then in my parent listing component I have:
<template>
<div id="app"><fusion-toolbar v-bind:items="myitems"> </fusion-toolbar></div>
</template>
<script>
import { Red, Blue } from "./Colour";
export default {
name: "App",
data: function() {
return {
myitems: []
};
},
mounted() {
this.myitems.push(new Red());
this.myitems.push(new Blue(false));
this.myitems.push(new Blue(true));
}
};
</script>
Is this the best way to go about it? Any feedback is welcomed...
I've done this solution freehand, so it may require some tweaks. Since the only variable is the color/name, it seems like you could easily make a catch-all component Color like this:
<template>
<div>
<span v-if="name === ''">Error, no colour specified</span>
<h2 v-else :style="{ color: name }">Hi, I am {{name}}!</h2>
</div>
</template>
<script>
export default {
data() {
return {
name: ''
}
}
}
</script>
You'd create a parent element with a slot.
<template>
<div>
<slot></slot>
</div>
</template>
Then you'd bring it into the parent with a v-for directive, below. You wouldn't need myItems to be a list of components--just a list of string color names:
<template>
<parent-element>
<Color v-for="item in myItems" :name="item" />
</parent-element>
</template>
<script>
import ParentElement from './ParentElement.vue'
import Color from './Color.vue'
export default {
components: {
Color, ParentElement
},
data() {
return {
myItems: ['red', 'red', 'blue']
}
}
}
</script>
Updated: if all you really want is inheritability, create a mixin:
var myColorMixin = {
data() {
return {
name: ''
}
},
computed: {
message() {
return 'Error, no colour specified';
}
}
}
Then override the properties you want when you inherit it:
export default {
mixins: [myColorMixin],
computed: {
message() {
return `Hi, I am ${this.name}`;
}
}
}
Having had some time to think it over I have to conclude my annoyance came from having to introduce the 'f-dummy' intermediate component just so I was able to capture Vue's render function which I could then pass on to the pojo.
What I have changed now is that I render the list itself using the lower level 'render(createElement)' (or 'h') handle that I can then pass on to the child without having to resort to an intermediate component.
Here is the new code, first off I define the ES6 classes (Note that Colour, Orange, Blue are just examples):
export class Colour {
public colour: string = "";
constructor(colour: string) {
this.colour = colour;
}
render(h: any) {
return h("h1", "Error - please provide your own implementation!");
}
}
export class Orange extends Colour {
constructor() {
super("orange");
}
public getStyle() {
return {
class: ['example-class'],
style: { backgroundColor: this.colour, border: '3px solid red' }
}
}
render(h: any) {
return h('h4', this.getStyle(), "This is my colour: " + this.colour)
}
}
export class Blue extends Colour {
private _isDisabled = true;
constructor(disabled: boolean) {
super("lightblue");
this._isDisabled = disabled;
}
render(h: any) {
return h('div',
[
h('h4', {
class: ['example-class', { 'is-disabled': this._isDisabled }],
style: { backgroundColor: this.colour }
}, "Hello, I am " + this.colour)
]
)
}
}
To render the list I now do this:
Vue.component("fusion-toolbar", {
props: ["items"],
render(h: any) {
return h('div', this.items.map(function (item: any) {
return item.render(h);
}))
}
});
Now I can simply add Orange or Blue 'pojo' instances to my list and they will be rendered correctly. See content of App.vue below:
<template>
<div id="app">
<fusion-toolbar v-bind:items="myitems"> </fusion-toolbar>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import { Colour, Orange, Blue } from "./components/DropDownItem";
export default Vue.extend({
name: "app",
data: function() {
return {
myitems: [] as Colour[]
}
},
mounted() {
this.myitems.push(new Orange());
this.myitems.push(new Blue(false));
this.myitems.push(new Blue(true));
}
});
</script>
<style lang="scss">
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
text-align: center;
}
.example-class {
width: 250px;
padding: 3px;
}
.is-disabled {
color: gray;
}
</style>
This is what the sample then looks like:
Very neat. I realise I can use a Babel plugin to use JSX instead of Vue's lower level rendering but I am quite enjoying the power it gives me!

Custom vue directive to render only if present

Frequently, I want to render a div (or other element) only if it has content. This means repeating the reference to the content in the tag, and in v-if, like this...
<div v-if="store.sometimesIWillBeEmpty">{{store.sometimesIWillBeEmpty}}</div>
With custom directives, I want to create a directive, v-fill, that behaves just like the code above, but with simplified syntax...
<div v-fill="store.sometimesIWillBeEmpty"></div>
updated The following works when message is not empty. What do I set or clear to render nothing when message is empty?
var store = {message: "hello cobber"}
Vue.directive('fill',
function (el, binding, vnode) {
if(binding.value)
el.innerHTML = binding.value
else
el = null
}
);
new Vue({
el: '#fill-example',
data: {
store: store
}
})
I'm one line away. Here's my fiddle. Anyone have any ideas?
It is possible to make a straightforward component to do what you want. A directive requires a bit more manipulation to be able to remove the element and put it back in the right place.
const vm = new Vue({
el: '#fill-example',
data: {
empty: '',
notEmpty: 'I have content'
},
components: {
renderMaybe: {
props: ['value'],
template: `<div v-if="value" class="boxy">{{value}}</div>`
}
},
directives: {
fill: {
bind(el, binding) {
Vue.nextTick(() => {
el.vFillMarkerNode = document.createComment('');
el.parentNode.insertBefore(el.vFillMarkerNode, el.nextSibling);
if (binding.value) {
el.textContent = binding.value;
} else {
el.parentNode.removeChild(el);
}
});
},
update(el, binding) {
if (binding.value) {
el.vFillMarkerNode.parentNode.insertBefore(el, el.vFillMarkerNode);
el.textContent = binding.value;
} else {
if (el.parentNode) {
el.parentNode.removeChild(el);
}
}
}
}
}
});
setTimeout(() => {
vm.empty = "Now I have content, too.";
}, 1500);
.boxy {
border: thin solid black;
padding: 1em;
}
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="fill-example">
Components:
<render-maybe :value="empty"></render-maybe>
<render-maybe :value="notEmpty"></render-maybe>
Directives:
<div class="boxy" v-fill="empty"></div>
<div class="boxy" v-fill="notEmpty"></div>
</div>