Skip to content

Instantly share code, notes, and snippets.

@gajus
Last active February 8, 2021 11:28
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save gajus/0bbc78135d88a02c18366f12237011a5 to your computer and use it in GitHub Desktop.
Save gajus/0bbc78135d88a02c18366f12237011a5 to your computer and use it in GitHub Desktop.
// @flow
/**
* @param history {@see https://www.npmjs.com/package/history}
* @param timeout A number of milliseconds to wait for the element to appear after PUSH action has been received.
*/
export default (history: *, timeout: number = 1000) => {
let observer;
let timeoutId;
if (!window.MutationObserver) {
return;
}
const reset = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (observer) {
observer.disconnect();
}
};
const createScrollToElement = (id: string) => {
return () => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView();
reset();
return true;
}
return false;
};
};
history.listen((location: *, action: *) => {
if (timeoutId) {
reset();
}
if (action !== 'PUSH') {
return;
}
if (typeof location.hash !== 'string') {
return;
}
const elementId = location.hash.slice(1);
if (!elementId) {
return;
}
const scrollToElement = createScrollToElement(elementId);
setTimeout(() => {
if (scrollToElement()) {
return;
}
observer = new MutationObserver(scrollToElement);
observer.observe(document, {
attributes: true,
childList: true,
subtree: true
});
timeoutId = setTimeout(reset, timeout);
});
});
};
@jkettmann
Copy link

jkettmann commented Jul 14, 2017

Very nice piece of code. Seems much less hacky than the other solutions.

I had a problem though, that nothing was happening, because of following check against action

if (action !== 'PUSH') {
  return;
}

I use your script like this createHistoryHashObserver(browserHistory), but action is always undefined. location.action is defined though. So following code works for me

if (location.action !== 'PUSH') {
  return;
}

Another problem is, that there is no scroll on page load. So if I go directly to a page with a hash it doesn't scroll to the expected element. If I introduce a flag for initial page load let initialPageLoad = true;

if (!initialPageLoad && location.action !== 'PUSH') {
  return;
}

....

setTimeout(() => {
    ....
}, initialPageLoad ? 200 : 1);

initialPageLoad = false;

});

Everything works as expected. The second part might not be necessary for server side rendering though. I didn't test that yet

@timsuchanek
Copy link

Great snippet!
Btw I adjusted this a littlebit to our needs and just wanted to share my solution with you.
2 adjustments I made:

  1. Scroll Up when page changed - we have a long docs site where you don't want that effect. We do this "in the main area"
  2. Scroll to Hash immediately after the page loaded (requetAnimationFrame)
  3. Conversion to untyped/basic Typescript
export default (history, timeout: number = 1000, mainSelector: string = 'right-side') => {
  let observer
  let timeoutId

  if (!(window as any).MutationObserver) {
    return
  }

  const reset = () => {
    if (timeoutId) {
      clearTimeout(timeoutId)

      timeoutId = null
    }

    if (observer) {
      observer.disconnect()
    }
  }

  const createScrollToElement = (id: string) => {
    return () => {
      const element = document.getElementById(id)

      if (element) {
        element.scrollIntoView()

        reset()

        return true
      }

      return false
    }
  }

  function scroll(location) {

    if (typeof location.hash !== 'string') {
      return
    }

    const elementId = location.hash.slice(1)

    if (!elementId) {
      const contentArea = document.getElementById(mainSelector)
      if (contentArea) {
        contentArea.scrollTop = 0
      }
      return
    }

    const scrollToElement = createScrollToElement(elementId)

    setTimeout(() => {
      if (scrollToElement()) {
        return
      }

      observer = new MutationObserver(scrollToElement)

      observer.observe(document, {
        attributes: true,
        childList: true,
        subtree: true,
      })

      timeoutId = setTimeout(reset, timeout)
    })
  }

  history.listen((location, action) => {
    if (timeoutId) {
      reset()
    }

    if (action !== 'PUSH') {
      return
    }

    scroll(location)
  })

  requestAnimationFrame(() => {
    scroll(location)
  })
}

@aivanov93
Copy link

I'm getting window is not defined error. Am I doing something wrong?

@aditya-padhi-kbl
Copy link

Nice information. But can you please guide me how to implement this in a react app ?

@prmichaelsen
Copy link

prmichaelsen commented Jun 10, 2020

@aditya-padhi-kbl it's simple

import React from 'react'; 
import { useHistory } from 'react-router-dom';
import Adventure from './Adventure';
// I named it `useAnchors`
import { useAnchors } from './useAnchors';

export const App = withContainer(() => {
  const history = useHistory();
  useAnchors(history, 750);
  return (
    <Adventure/>
  );
});

export default App;

@aditya-padhi-kbl
Copy link

aditya-padhi-kbl commented Jun 10, 2020 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment