So I am just trying to get my basic Vue navbar functions working such as changing class on scroll (which works fine) and changing class on resize, which I'm having a bit more trouble with.
Here is the content of my <template> tags:
<nav class="navbar is-fixed-top navbar-max">
{{windowWidth}}
</nav>
...and the relevant content of my <script> tags:
export default {
name: "Navbar",
data () {
return {
windowWidth: window.innerWidth
}
},
created () {
window.addEventListener('resize', this.onResize);
},
mounted () {
this.windowWidth = window.innerWidth
},
beforeDestroy () {
window.removeEventListener('resize', this.onResize);
},
methods: {
onResize() {
let navbar = document.querySelector(".navbar");
if (this.windowWidth > 768) {
console.log(this.windowWidth),
navbar.classList.remove("nav-mobile"),
navbar.classList.add("nav-desktop")
}
else {
console.log(this.windowWidth),
navbar.classList.remove("nav-desktop"),
navbar.classList.add("nav-mobile")
}
}
}
}
My issue is really odd - all my console.log()'s output the correct width, and so does {{windowWidth}} in the navbar, and even the adding and removing classes works! It's just that the changed classes don't seem to have any effect until windowWidth = 1024 and I have no idea why...
Any help?
Cheers :)
You never set this.windowWidth after mount.
Simply add the following:
onResize() {
this.windowWidth = window.innerWidth // add this line
...
}
I would also like to point out that what it looks like you're trying to achieve (different nav-bar-styling on mobile and desktop) is very doable without any vue-/js-magic using CSS #media-queries.
And if you still wish to do it with vue, you should do it the vue way:
Make a computed method like so:
computed: {
isMobile() {
return this.windowWidth <= 768
}
}
and then update the class directly on the nav-tag using class-binding.
Related
Can someone explain to me in simple terms why the following example doesn't work?
I'm trying to run a function that captures the viewport/window width and then runs code based on how wide the viewport or window is (responsive design).
I'm a beginner so it's entirely possible I'm misunderstanding how Watch and Computed works... but it's my understanding that both Watch and Computed monitors a data property and if my data changes, watch and computed should react and trigger their code right?
So if I have a value called viewportWidth in my data, and I run an onresize to continually update it, I am updating my data which should trigger my watcher right? Shouldn't the continually updating value also trigger my computed property since it also relies on changing data?
So far I'm not seeing either of them react to my data changing.. if I'm misunderstanding please ELI5 and show me the better way to approach this and why..
(quick sidenote: I understand I can just run my handler inside of my onresize listener, but I assumed it would be smarter to instead setup a watcher or computed so that my method since they cache(?) and not trigger too often when it doesn't need to and only update conditions when it needs to.. is that right?)
Thank you!
<template>
<main>
<section>
<h2>viewport width: {{viewportWidth}}px</h2>
<h2>computed: {{rackClass}}</h2>
<h2>Does it work? {{doesItWork}}</h2>
</section>
</main>
</template>
<script>
export default {
data() {
return {
viewportWidth: window.innerWidth,
doesItWork: 'no it does not'
}
},
mounted() {
window.onresize = function(e) {
this.viewportWidth = window.innerWidth;
console.log(window.innerWidth)
}
},
watch: {
viewportWidth: function() {
console.log('>> value changed')
this.handleViewPortChange();
}
},
computed: {
rackClass: function(){
let theValue = "greater";
if(this.viewportWidth > 1000) theValue = "less than"
console.log('>> viewportWidth changed = ',this.viewportWidth)
return theValue
},
methods:{
handleViewportChange: function() {
this.doesItWork = 'it works!';
}
}
}
}
</script>
https://codepen.io/cmaxster/pen/rNyZLXG
Well aren't you in a pickle!
You are putting your curly braces and the commas in all the wrong places!
I updated the code so that it can be added as a snippet here. I have also put comments where you had messed up.
const app = new Vue({
el: "#app",
data() {
return {
viewportWidth: window.innerWidth,
doesItWork: 'no it does not'
}
},
mounted() {
const self = this;
window.onresize = (e) => {
this.viewportWidth = window.innerWidth;
//console.log(window.innerWidth)
}
},
watch: {
viewportWidth: function() {
console.log('>> value changed')
this.handleViewportChange(); // you were calling the wrong method! spellings and case was messed up
}
},
computed: {
rackClass: function(){
let theValue = "greater";
if(this.viewportWidth > 1000) theValue = "less than"
console.log('>> viewportWidth changed = ',this.viewportWidth)
return theValue
}
},
// you had your methods inside computed!
methods:{
handleViewportChange() {
this.doesItWork = 'it works!';
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.12/dist/vue.js"></script>
<main id="app">
<section>
<h2>viewport width: {{viewportWidth}}px</h2>
<h2>computed: {{rackClass}}</h2>
<h2>Does it work? {{doesItWork}}</h2>
</section>
</main>
Have you tried transforming your watch into an arrow function?
i decided to learn Vue.js on a project and i can't get how i can track window scroll and dynamicly change CSS after some distance similar, like i did it with JQuery:
$(window).scroll(function() {
if($(window).scrollTop() >= 100) {
$('header').addClass('fixed');
} else {
$('header').removeClass('fixed');
}
});
You can use a Custom Directive and bind it to any component or element in your Vue instance.
So say you have a <div> and name your directive on-scroll you would update your div: <div on-scroll="handleScroll"> where handleScroll is the method in your Vue instance that is going to, well, handle the scroll event.
Directive:
Vue.directive('on-scroll', {
inserted: function (el, binding) {
let f = function (evt) {
if (binding.value(evt, el)) {
window.removeEventListener('scroll', f)
}
}
window.addEventListener('scroll', f)
}
})
Vue Instance:
new Vue({
el: '#el',
methods: {
handleScroll: (event, el) => {
if ( window.scrollY >= 300 ) {
el.classList.add('fixed');
} else {
el.classList.remove('fixed');
}
}
}
});
I have a theme that I am converting into an Angular 4 admin panel. The theme has a file called app.js containing a class which I am trying to convert into my layout.component.ts In app.js there are several blocks of code that try to access functions outside the class but I have converted all functions into methods of my layout.component.ts;
$(window).resize(function(){
this.resizePageContent();
});
Running this gives a Javascript error. However there is a method on my layout.component.ts that should be used in this case.;
this.resizePageContent is not a function
So I am wondering which is the best way to convert this such that the layout.component.ts method is called instead. This is what I have tried but I am not so sure whether this is the best way to do it and why it works.
$(window).resize(()=>{
this.resizePageContent();
});
After replacing it with the above code the errors have disappeared.
This is a preview of app.js It has over 700 lines of code so I will not be able to paste it all here;
Update:
var App = function() {
/* Helper variables - set in uiInit() */
var page, pageContent, header, footer, sidebar, sScroll, sidebarAlt, sScrollAlt;
/* Initialization UI Code */
var uiInit = function() {...};
/* Page Loading functionality */
var pageLoading = function(){..};
/* Gets window width cross browser */
var getWindowWidth = function(){...};
/* Sidebar Navigation functionality */
var handleNav = function() {..};
/* Scrolls the page (static layout) or the sidebar scroll element (fixed header/sidebars layout) to a specific position - Used when a submenu opens */
var handlePageScroll = function(sElem, sHeightDiff, sSpeed) {...};
/* Sidebar Functionality */
var handleSidebar = function(mode, extra) {...};
/* Resize #page-content to fill empty space if exists */
var resizePageContent = function() {...};
/* Interactive blocks functionality */
var interactiveBlocks = function() {...};
/* Scroll to top functionality */
var scrollToTop = function() {...};
/* Demo chat functionality (in sidebar) */
var chatUi = function() {...};
/* Template Options, change features functionality */
var templateOptions = function() {...};
/* Datatables basic Bootstrap integration (pagination integration included under the Datatables plugin in plugins.js) */
var dtIntegration = function() {...};
/* Print functionality - Hides all sidebars, prints the page and then restores them (To fix an issue with CSS print styles in webkit browsers) */
var handlePrint = function() {...};
return {
init: function() {
uiInit(); // Initialize UI Code
pageLoading(); // Initialize Page Loading
},
sidebar: function(mode, extra) {
handleSidebar(mode, extra); // Handle sidebars - access functionality from everywhere
},
datatables: function() {
dtIntegration(); // Datatables Bootstrap integration
},
pagePrint: function() {
handlePrint(); // Print functionality
}
};
}();
/* Initialize app when page loads */
$(function(){ App.init(); });
This is my layout.component.ts;
import { Component, OnInit } from '#angular/core';
declare var jQuery: any;
declare var $: any;
declare var window: any;
declare var document: any;
declare var Cookies: any;
#Component({
selector: 'app-layout',
templateUrl: './layout.component.html',
styleUrls: ['./layout.component.css']
})
export class LayoutComponent implements OnInit {
public page;
public pageContent;
public header;
public footer;
public sidebar;
public sScroll;
public sidebarAlt;
public sScrollAlt;
constructor() { }
ngOnInit() {
this.init();
}
uiInit (): void {...}
pageLoading (): void {...}
getWindowWidth (): any {...}
handleNav (): any {...}
handlePageScroll (sElem, sHeightDiff, sSpeed): void {...}
handleSidebar (mode, extra?:any): any {
if (mode === 'init') {
// Init sidebars scrolling functionality
this.handleSidebar('sidebar-scroll');
this.handleSidebar('sidebar-alt-scroll');
// Close the other sidebar if we hover over a partial one
// In smaller screens (the same applies to resized browsers) two visible sidebars
// could mess up our main content (not enough space), so we hide the other one :-)
$('.sidebar-partial #sidebar')
.mouseenter(function(){ this.handleSidebar('close-sidebar-alt'); });
$('.sidebar-alt-partial #sidebar-alt')
.mouseenter(function(){ this.handleSidebar('close-sidebar'); });
} else {
var windowW = this.getWindowWidth();
if (mode === 'toggle-sidebar') {
if ( windowW > 991) { // Toggle main sidebar in large screens (> 991px)
this.page.toggleClass('sidebar-visible-lg');
if (this.page.hasClass('sidebar-mini')) {
this.page.toggleClass('sidebar-visible-lg-mini');
}
if (this.page.hasClass('sidebar-visible-lg')) {
this.handleSidebar('close-sidebar-alt');
}
// If 'toggle-other' is set, open the alternative sidebar when we close this one
if (extra === 'toggle-other') {
if (!this.page.hasClass('sidebar-visible-lg')) {
this.handleSidebar('open-sidebar-alt');
}
}
} else { // Toggle main sidebar in small screens (< 992px)
this.page.toggleClass('sidebar-visible-xs');
if (this.page.hasClass('sidebar-visible-xs')) {
this.handleSidebar('close-sidebar-alt');
}
}
// Handle main sidebar scrolling functionality
this.handleSidebar('sidebar-scroll');
}
else if (mode === 'toggle-sidebar-alt') {
if ( windowW > 991) { // Toggle alternative sidebar in large screens (> 991px)
this.page.toggleClass('sidebar-alt-visible-lg');
if (this.page.hasClass('sidebar-alt-visible-lg')) {
this.handleSidebar('close-sidebar');
}
// If 'toggle-other' is set open the main sidebar when we close the alternative
if (extra === 'toggle-other') {
if (!this.page.hasClass('sidebar-alt-visible-lg')) {
this.handleSidebar('open-sidebar');
}
}
} else { // Toggle alternative sidebar in small screens (< 992px)
this.page.toggleClass('sidebar-alt-visible-xs');
if (this.page.hasClass('sidebar-alt-visible-xs')) {
this.handleSidebar('close-sidebar');
}
}
}
else if (mode === 'open-sidebar') {
if ( windowW > 991) { // Open main sidebar in large screens (> 991px)
if (this.page.hasClass('sidebar-mini')) { this.page.removeClass('sidebar-visible-lg-mini'); }
this.page.addClass('sidebar-visible-lg');
} else { // Open main sidebar in small screens (< 992px)
this.page.addClass('sidebar-visible-xs');
}
// Close the other sidebar
this.handleSidebar('close-sidebar-alt');
}
else if (mode === 'open-sidebar-alt') {
if ( windowW > 991) { // Open alternative sidebar in large screens (> 991px)
this.page.addClass('sidebar-alt-visible-lg');
} else { // Open alternative sidebar in small screens (< 992px)
this.page.addClass('sidebar-alt-visible-xs');
}
// Close the other sidebar
this.handleSidebar('close-sidebar');
}
else if (mode === 'close-sidebar') {
if ( windowW > 991) { // Close main sidebar in large screens (> 991px)
this.page.removeClass('sidebar-visible-lg');
if (this.page.hasClass('sidebar-mini')) { this.page.addClass('sidebar-visible-lg-mini'); }
} else { // Close main sidebar in small screens (< 992px)
this.page.removeClass('sidebar-visible-xs');
}
}
else if (mode === 'close-sidebar-alt') {
if ( windowW > 991) { // Close alternative sidebar in large screens (> 991px)
this.page.removeClass('sidebar-alt-visible-lg');
} else { // Close alternative sidebar in small screens (< 992px)
this.page.removeClass('sidebar-alt-visible-xs');
}
}
else if (mode === 'sidebar-scroll') { // Handle main sidebar scrolling
if (this.page.hasClass('sidebar-mini') && this.page.hasClass('sidebar-visible-lg-mini') && (windowW > 991)) { // Destroy main sidebar scrolling when in mini sidebar mode
if (this.sScroll.length && this.sScroll.parent('.slimScrollDiv').length) {
this.sScroll
.slimScroll({destroy: true});
this.sScroll
.attr('style', '');
}
}
else if ((this.page.hasClass('header-fixed-top') || this.page.hasClass('header-fixed-bottom'))) {
var sHeight = $(window).height();
if (this.sScroll.length && (!this.sScroll.parent('.slimScrollDiv').length)) { // If scrolling does not exist init it..
this.sScroll
.slimScroll({
height: sHeight,
color: '#fff',
size: '3px',
touchScrollStep: 100
});
// Handle main sidebar's scrolling functionality on resize or orientation change
var sScrollTimeout;
$(window).on('resize orientationchange', function(){
clearTimeout(sScrollTimeout);
sScrollTimeout = setTimeout(function(){
this.handleSidebar('sidebar-scroll');
}, 150);
});
}
else { // ..else resize scrolling height
this.sScroll
.add(this.sScroll.parent())
.css('height', sHeight);
}
}
}
else if (mode === 'sidebar-alt-scroll') { // Init alternative sidebar scrolling
if ((this.page.hasClass('header-fixed-top') || this.page.hasClass('header-fixed-bottom'))) {
var sHeightAlt = $(window).height();
if (this.sScrollAlt.length && (!this.sScrollAlt.parent('.slimScrollDiv').length)) { // If scrolling does not exist init it..
this.sScrollAlt
.slimScroll({
height: sHeightAlt,
color: '#fff',
size: '3px',
touchScrollStep: 100
});
// Resize alternative sidebar scrolling height on window resize or orientation change
var sScrollAltTimeout;
$(window).on('resize orientationchange', function(){
clearTimeout(sScrollAltTimeout);
sScrollAltTimeout = setTimeout(function(){
this.handleSidebar('sidebar-alt-scroll');
}, 150);
});
}
else { // ..else resize scrolling height
this.sScrollAlt
.add(this.sScrollAlt.parent())
.css('height', sHeightAlt);
}
}
}
}
return false;
}
resizePageContent (): void {...}
interactiveBlocks (): void {...}
scrollToTop (): any{...}
chatUi (): any {...}
templateOptions (): void {...}
dtIntegration (): any {...}
handlePrint (): void {...}
//Methods from original object
init (): void {
this.uiInit(); // Initialize UI Code
this.pageLoading(); // Initialize Page Loading
}
//Originally sidebar
CallhandleSidebar(mode, extra): void {
this.handleSidebar(mode, extra); // Handle sidebars - access functionality from everywhere
}
datatables() :void {
this.dtIntegration(); // Datatables Bootstrap integration
}
pagePrint (): void {
this.handlePrint(); // Print functionality
}
}
Also could you give me an example of how TypeScript might output something like this;
$(window).resize(()=>{
this.resizePageContent();
});
...just to get a good idea of how it works.
In your case, using the code
$(window).resize(()=>{
this.resizePageContent();
});
is the correct way for accessing functions and variables outside of the scope of the callback function.
When using
function() {
this.something;
}
the 'this' is bound to the scope of the function, rather than of the class.
When using
() => {
this.something
}
that is using ecmascript 6 arrow notation. Ecmascript 6 sees the introduction of the lexical this, where in the latter case the 'this' keyword refers to the class defined.
For more information see http://es6-features.org/#Lexicalthis and https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions#No_binding_of_this .
If you have to do just delegate or one line coder in the callback, go for this:
$(window).resize(()=> this.resizePageContent());
Or you can use the following too resize the page content:
another question on stackoverflow to catch the window resizing event in Angular context
and make your resizePageContent() method to listen for that event.
Opinion based question, but personally the best way is to use a class field
and assign an arrow function definition, and to -not- use jQuery while using angular:
window.addEventListener('resize', this.resizePageContent);
Within your class you should then define the method like this:
private resizePageContent: EventListener = (event: UIEvent): void => {
};
The big advantage of this, is that you can remove the event listener, which is impossible if you use an anonymous arrow function.
plunkr
Another option is to use bind(this) to retain the this context:
window.addEventListener('resize', this.resizePageContent.bind(this));
This way you do not need to assign your callback to a field of the class. But using bind creates a copy of the method, which makes it harder (not impossible) to remove the event listener
You can also just immediately listen to it from within your component using #HostListener. Removing of the event is done internally by angular on destroy of the component
#HostListener('window:resize', ['$event'])
public resizePageContent(event: Event) {}
The reason you were getting an error is because you were using the function keyword. This changes the this context to the anonymous function you are defining as callback
I'm trying to launch different functions depending on screen size after load, resize and scroll. To prevent effect of calculations in other functions I need to stop them.
Here if example with dummy functions from Jsfiddle
Here is example with real functions from Jsfiddle (it doesn't show html or css, just functions for your understanding)
If you look at console you will see that after first rezise first function runs, but when you get another window size breakpoint another function will run together with first one. the same with third function if you get third breakpoint. I want to stop function after window changed size to another breakpoint
here is my jquery:
function sticker1220() {
$(window).on("load resize scroll",function(){
console.log('sticker1220');
});
};
function sticker950() {
$(window).on("load resize scroll",function(){
console.log('sticker950');
});
};
function sticker320() {
$(window).on("load resize scroll",function(){
console.log('sticker320');
});
};
function checksize() {
if ( $(window).width() > 1220 ) {
sticker1220();
} else if ( $(window).width() > 640 & $(window).width() < 1219 ) {
sticker950();
} else if ( $(window).width() < 639 ) {
sticker320();
}
};
checksize();
$(window).resize(function() {
checksize();
$('p').text($(window).width());
});
You could do something like this.
I have abstracted the media queries into an object that can be iterated to find the applicable callback if one is available.
There is also now only on resize callback which is easier to manage as you don't need to run separate resize events which could lock up your ui thread. And the events will work properly if you change the screen resolution, whereas previously were binding a new resize event each time the screen resized.
If you need to make it faster you could change the resize event to a recursive timer using setTimeout.
function checksize(mediaQueries) {
return function(e) {
const width = window.innerWidth
$('p').text(width)
const query = mediaQueries.find(x => {
switch (true) {
case !!x.min && !!x.max:
return width >= x.min && width < x.max
case !!x.min:
return width >= x.min
case !!x.max:
return width <= x.max
default:
return false
}
})
if (typeof query.cb === 'function') {
return query.cb(e)
}
}
}
var atMedia = [
{
max: 639,
cb: function(e) {
console.log('sticker320')
}
},
{
min: 640,
max: 1219,
cb: function(e) {
console.log('sticker950')
}
},
{
min: 1220,
cb: function(e) {
console.log('sticker1220')
}
}
]
$(window).resize(checksize(atMedia)).trigger('resize')
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p></p>
I've created a React component for a fixed nav that I would like to remain hidden, until I scroll past a certain point on the page, then slides into view. Medium has a header similar to what I'm describing.
This is a relatively trivial task in jQuery, with scrollmagic or waypoints but is there an idiomatic way of accomplishing this with React and vanilla JS?
React Way with vanilla JS jsfiddle;
don't forget to remove EventListener. In this example component will render if only it is neccessary
class TopBar extends React.Component {
state = { isHide: false };
hideBar = () => {
const { isHide } = this.state
window.scrollY > this.prev ?
!isHide && this.setState({ isHide: true })
:
isHide && this.setState({ isHide: false });
this.prev = window.scrollY;
}
componentDidMount(){
window.addEventListener('scroll', this.hideBar);
}
componentWillUnmount(){
window.removeEventListener('scroll', this.hideBar);
}
render(){
const classHide = this.state.isHide ? 'hide' : '';
return <div className={`topbar ${classHide}`}>topbar</div>;
}
}
You could use a component such as react-headroom to do the heavy lifting for you. Or, you can still use waypoints in React, setting it up in the componentDidMount lifecycle method and removing it using componentWillUnmount.
In the componentDidMount lifecycle hook, do the same thing as in the jQuery link you have given:
class Navbar extends React.component {
let delta = 5;
render() {
return (
<div ref=header></div>
);
}
componentDidMount() {
$(window).scroll(function(event){
var st = $(this).scrollTop();
if(Math.abs(this.state.lastScrollTop - st) <= delta)
return;
if (st > lastScrollTop){
// downscroll code
// $(this.refs.header).css('visibility','hidden').hover ()
this.setState({
navbarVisible: false
});
} else {
// upscroll code
$(this.refs.header).css('visibility','visible');
this.setState({
navbarVisible: true
});
}
lastScrollTop = st;
}.bind(this));
}
}
I created a react component for this same exact need as I could not find any other implementations that matched what I needed. Even react-headroom did not give you something that would just scroll in after reaching a certain point on the page.
The gist is here: https://gist.github.com/brthornbury/27531e4616b68131e512fc622a61baba
I don't see any reason to copy the component code here. The code is largely based off of the react-headroom code but does less and is therefore simpler.
The component is the first piece of code, you could simply copy/paste then import it. After importing your code with the navbar would look something like this:
class MyScrollInNavBar extends Component {
render() {
return (
<ScrollInNav scrollInHeight={150}>
<MyNavBar />
</ScrollInNav>
);
}
}