Choo: supporting anchor links

Created on 21 Jun 2016  Â·  11Comments  Â·  Source: choojs/choo

Using the default sheet-router/href module results in not being able to jump to anchor links when clicked or when first loading a page with a hash that's meant to point to an anchor somewhere on the page.

Supporting anchor links might not be something we want to have in choo by default, but it would be nice to have docs for it somewhere.

This is also potentially a topic for sheet-router.

Here's where I'm at currently with a solution that overrides the default href behavior:

var choo = require('choo')
var el = choo.view
var app = choo()

app.model({
  namespace: 'app',
  subscriptions: [catchLinks]
})

function catchLinks (send) {
  window.onclick = (e) => {
    const node = (function traverse (node) {
      if (!node) return
      if (node.localName !== 'a') return traverse(node.parentNode)
      if (node.href === undefined) return traverse(node.parentNode)
      if (window.location.host !== node.host) return traverse(node.parentNode)
      return node
    })(e.target)
    if (!node) return
    e.preventDefault()
    var href = node.href

    if (location.pathname !== node.pathname) {
      send('app:location', { location: href.replace(/#$/, '') })
      window.history.pushState(null, null, href)
    } else {
      if (window.location.hash !== node.hash) {
        window.location.hash = node.hash
      }
      scrollToHash(node.hash)
    }
  }
}

app.router(function (route) {
  return [
    route('/', home),
    route('/example', example)
  ]
})

var tree = app.start({ href: false })
document.body.appendChild(tree)

function home (params, state, send) {
  return el`
    <main>
    <h1>home</h1>
    <a href="#home-anchor">home anchor link</a>

    <p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p>

    <h2 id="home-anchor">home anchor target</h2>
    <a href="/example#example-anchor">link to example anchor</a>
    <p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p>
    </main>
  `
}

function example (params, state, send) {
  return el`
    <main>
    <h1>example page</h1>
    <a href="#example-anchor">example anchor link</a>

    <p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p>

    <h2 id="example-anchor">example anchor target</h2>
    <a href="/#home-anchor">link to home anchor</a>
    <p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p><p>some text</p>
    </main>
  `
}

function scrollToHash (hash) {
  if (hash) {
    var el = document.querySelector(hash)
    window.scrollTo(0, el.offsetTop)
  }
}

Most helpful comment

Just had a discussion about this with @juliangruber, and we came up with the
following for hash routing:

Inline anchor tags and hash routing cannot co-exist in the same application.
You can choose one, but hash routing should be the default. This is chosen because choo is primarily intended for single page apps, and using hash routing is more in line with that.

Hash routes should be explicit in the application. They're enumerable, and
dynamic hashing can be done through partials:

// enumerable
app.router('/404', [
  [ '/foo', () => {} ],
  [ '/foo#bar', () => {} ],
  [ '/foo#baz', () => {} ],
])
// '/foo#baz' => OK
// '/foo#ohno' => ERROR
// dynamic
app.router('/404', [
  [ '/foo', () => {} ],
  [ '/foo#:baz', () => {} ],
])
// '/foo#baz' => OK
// '/foo#ohno' => OK

An option should be available to enable anchor tag routing, through:
{ anchors: true}.

Browser routing updates can be disabled as a whole through the existing option
of { history: false } - which is useful for widget-like situations.

Thoughts? - should this proposal perhaps be a separate issue?

All 11 comments

agreed - think this is related to https://github.com/yoshuawuyts/sheet-router/issues/15; we should def think of a way to resolve this

We should probs do a document.querySelector("[href='href_value']") and not handle the link if it returns true, cause then we've confirmed it's actually an element on the page and we kinda want the behavior to work as expected. What do you reckon?

Actually https://github.com/yoshuawuyts/sheet-router/issues/15#issuecomment-229798345 might have a better solution. Hmmm, I might need some breathing room from choo for a bit, and while this is definitely a priority it's not super pressing for the stuff I'm doing - any PR to add this would be heaps welcome!

Apparently it's possible - but should probs still fix hah - http://regl.party/api

Yeah I got that api page working with roughly the code from the example I pasted above 😄 .

I'm starting to wonder if this is a bug that needs to be fixed or if this is reasonable default behavior and we should have a good guide for showing how to override the location object. Or maybe both.

Working on adventuretron today I encountered a similar but different problem, where electron puts file:///whatever/path/to/the/index.html in location.pathname.

Did this to override it:

app.model({
    namespace: 'location',
    state: {
      pathname: '/'
    },
    reducers: {
      pathname: function (data, state) {
        return { pathname: data.pathname }
      }
    },
    subscriptions: [
      function catchLinks (send, done) {
        window.onclick = function (e) {
          var node = (function traverse (node) {
            if (!node) return
            if (node.localName !== 'a') return traverse(node.parentNode)
            if (node.href === undefined) return traverse(node.parentNode)
            if (window.location.host !== node.host) return traverse(node.parentNode)
            return node
          })(e.target)

          if (!node) return
          e.preventDefault()
          var href = node.href.replace('file://', '')

          send('location:pathname', { pathname: href.replace(/#$/, '') }, done)
        }
      }
    ]
  })

I'm thinking a separate package could be created from that traverse function (which i stole from sheetify 😸) plus maybe a little more logic, and in the choo guide we might say something like:

"hey, need to override the location model? you can use whatever-link-catching-module like this, and here's examples for overrides for electron & supporting anchor links using a subscription"

That subscription could then look a little simpler:

var catchLinks = require('whatever-link-catching-module')

subscriptions: [
  function (send, done) {
    catchLinks(function (node, location) {
      var href = node.href.replace('file://', '')
      send('location:pathname', { pathname: href.replace(/#$/, '') }, done)
    })
  }
]

Turns out there are modules that do mostly the right thing with catching links already, like: https://www.npmjs.com/catch-links

FWIW, this is a current need in my work. I'm getting around the limitation with scrollIntoView.

I'll try to find where the change belongs next I can. I gather this cuts across packages, and perhaps a few other checkboxes for 4.0.0.

So did we decide to fix this as a bug or instead decide this is a browser feature and documentation is needed to explain how to work around it?

Just had a discussion about this with @juliangruber, and we came up with the
following for hash routing:

Inline anchor tags and hash routing cannot co-exist in the same application.
You can choose one, but hash routing should be the default. This is chosen because choo is primarily intended for single page apps, and using hash routing is more in line with that.

Hash routes should be explicit in the application. They're enumerable, and
dynamic hashing can be done through partials:

// enumerable
app.router('/404', [
  [ '/foo', () => {} ],
  [ '/foo#bar', () => {} ],
  [ '/foo#baz', () => {} ],
])
// '/foo#baz' => OK
// '/foo#ohno' => ERROR
// dynamic
app.router('/404', [
  [ '/foo', () => {} ],
  [ '/foo#:baz', () => {} ],
])
// '/foo#baz' => OK
// '/foo#ohno' => OK

An option should be available to enable anchor tag routing, through:
{ anchors: true}.

Browser routing updates can be disabled as a whole through the existing option
of { history: false } - which is useful for widget-like situations.

Thoughts? - should this proposal perhaps be a separate issue?

Had a think.

No applications mixing anchor links and hash routing in a crucial way come to mind. At the same time, IMHO, I've seen plenty of applications using hash routing that should use path routing instead, and then "enable" anchor links again.

Conceptually, I don't see why an application couldn't dynamically differentiate hash-bearing history states with routing implications and hash-bearing history states with anchor links. But practically, that sounds like a nightmare. Not likely worth the effort ... or whatever weight it might add to the bundles.

I honestly feel like it should use default browser behavior when using an
a-tag, and use the router only when using a js api.

On Tue, Aug 2, 2016 at 5:04 PM, Kyle Mitchell [email protected]
wrote:

Had a think.

No applications mixing anchor links and hash routing in a crucial way come
to mind. At the same time, IMHO, I've seen plenty of applications using
hash routing that should use path routing instead, and then "enable" anchor
links again.

Conceptually, I don't see why an application couldn't dynamically
differentiate hash-bearing history states with routing implications and
hash-bearing history states with anchor links. But practically, that sounds
like a nightmare. Not likely worth the effort ... or whatever weight it
might add to the bundles.

—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/yoshuawuyts/choo/issues/65#issuecomment-237043443,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAmDudvsj9KRrng4zcWPlbBbv4oRNY4Sks5qb7DqgaJpZM4I7MX8
.

Matt McFarland

Software Engineer | @docodemore https://twitter.com/docodemore | LinkedIn
http://www.linkedin.com/pub/matthew-mcfarland/6a/b9a/135/en | Mobile:
513.638.8685 | Office: 937.865.6800 xt: 125010

I honestly feel like it should use default browser behavior when using an a-tag, and use the router only when using a js api.

@MattMcFarland Hmm, yeah that does make sense - seems in line with the rest of choo.


Conceptually, I don't see why an application couldn't dynamically differentiate hash-bearing history states with routing implications and hash-bearing history states with anchor links. But practically, that sounds like a nightmare. Not likely worth the effort ... or whatever weight it might add to the bundles.

@kemitchell Yeah, exactly - this is what I initially thought, but yeah I don't feel like the benefits outweigh the costs

Was this page helpful?
0 / 5 - 0 ratings

Related issues

depoulo picture depoulo  Â·  3Comments

ungoldman picture ungoldman  Â·  3Comments

yoshuawuyts picture yoshuawuyts  Â·  5Comments

NetOperatorWibby picture NetOperatorWibby  Â·  5Comments

corderophi678 picture corderophi678  Â·  5Comments