Skip to content

Commit

Permalink
Can disable links with use:link (#163)
Browse files Browse the repository at this point in the history
Co-authored-by: Alessandro (Ale) Segala <[email protected]>
  • Loading branch information
omerman and ItalyPaleAle authored Jun 16, 2021
1 parent 864312e commit 7a7ba80
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 18 deletions.
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ You can navigate between pages with normal anchor (`<a>`) tags. For example:
<a href="#/book/123">Thus Spoke Zarathustra</a>
````

#### The `use:link` action

Rather than having to type `#` before each link, you can also use the `use:link` action:

````svelte
Expand All @@ -228,7 +230,25 @@ import {link} from 'svelte-spa-router'
<a href="/book/321" use:link>The Little Prince</a>
````

You can also use the `use:link={variable}` action to have your link set to a variable and updated reactively (this will always take precedence over `href` attributes, if present):
The `use:link` action accepts an optional parameter `opts`, which can be one of:

- A dictionary `{href: '/foo', disabled: false}` where both keys are optional:
- If you set a value for `href`, your link will be updated to point to that address, reactively (this will always take precedence over `href` attributes, if present)
- Setting `disabled: true` disables the link, so clicking on that would have no effect
- A string with a destination (e.g. `/foo`), which is a shorthand to setting `{href: '/foo'}`.

For example:

````svelte
<script>
import {link} from 'svelte-spa-router'
let myLink = "/book/456"
</script>
<!-- Note the {{...}} notation because we're passing an object as parameter for a Svelte action -->
<a use:link={{href: myLink, disabled: false}}>The Biggest Princess</a>
````

The above is equivalent to:

````svelte
<script>
Expand All @@ -238,6 +258,10 @@ let myLink = "/book/456"
<a use:link={myLink}>The Biggest Princess</a>
````

Changing the value of `myLink` will reactively update the link's `href` attribute.

#### Navigating programmatically

You can navigate between pages programmatically too:

````js
Expand Down
18 changes: 16 additions & 2 deletions Router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ export function pop(): Promise<void>
*/
export function replace(location: string): Promise<void>

/** Type for the opts parameter of the link action */
export type LinkActionOpts = {
/** A string to use in place of the link's href attribute. Using this allows for updating link's targets reactively. */
href?: string
/** If true, link is disabled */
disabled?: boolean
}

/** Type for the update function of the link action */
export type LinkActionUpateFunc = ((opts?: LinkActionOpts) => void) |
((hrefVar?: string) => void)

/**
* Svelte Action that enables a link element (`<a>`) to use our history management.
*
Expand All @@ -114,9 +126,11 @@ export function replace(location: string): Promise<void>
* ````
*
* @param node - The target node (automatically set by Svelte). Must be an anchor tag (`<a>`) with a href attribute starting in `/`
* @param hrefVar - A string to use in place of the link's href attribute. Using this allows for updating link's targets reactively.
* @param opts - Dictionary with options for the link
* @param hrefVar - A string to use in place of the link's href attribute. Using this allows for updating link's targets reactively. This is a shorthand for opts.href
*/
export function link(node: HTMLElement, hrefVar?: string): {update: (hrefVar?: string) => void}
export function link(node: HTMLElement, opts?: LinkActionOpts): {update: LinkActionUpateFunc}
export function link(node: HTMLElement, hrefVar?: string): {update: LinkActionUpateFunc}

/** Full location from the hash: page and querystring */
interface Location {
Expand Down
60 changes: 45 additions & 15 deletions Router.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ export async function replace(location) {
window.dispatchEvent(new Event('hashchange'))
}
/**
* Dictionary with options for the link action.
* @typedef {Object} LinkActionOpts
* @property {string} href - A string to use in place of the link's href attribute. Using this allows for updating link's targets reactively.
* @property {boolean} disabled - If true, link is disabled
*/
/**
* Svelte Action that enables a link element (`<a>`) to use our history management.
*
Expand All @@ -165,45 +172,68 @@ export async function replace(location) {
* ````
*
* @param {HTMLElement} node - The target node (automatically set by Svelte). Must be an anchor tag (`<a>`) with a href attribute starting in `/`
* @param {string} hrefVar - A string to use in place of the link's href attribute. Using this allows for updating link's targets reactively.
* @param {string|LinkActionOpts} opts - Options object. For legacy reasons, we support a string too which will be the value for opts.href
*/
export function link(node, hrefVar) {
export function link(node, opts) {
opts = linkOpts(opts)
// Only apply to <a> tags
if (!node || !node.tagName || node.tagName.toLowerCase() != 'a') {
throw Error('Action "link" can only be used with <a> tags')
}
updateLink(node, hrefVar || node.getAttribute('href'))
updateLink(node, opts)
return {
update(updated) {
updated = linkOpts(updated)
updateLink(node, updated)
}
}
}
// Internal function used by the link function
function updateLink(node, href) {
// Destination must start with '/'
if (!href || href.length < 1 || href.charAt(0) != '/') {
function updateLink(node, opts) {
let href = opts.href || node.getAttribute('href')
// Destination must start with '/' or '#/'
if (href && href.charAt(0) == '/') {
// Add # to the href attribute
href = '#' + href
}
else if (!href || href.length < 2 || href.slice(0, 2) != '#/') {
throw Error('Invalid value for "href" attribute: ' + href)
}
// Add # to the href attribute
node.setAttribute('href', '#' + href)
node.addEventListener('click', scrollstateHistoryHandler)
node.setAttribute('href', href)
node.addEventListener('click', (event) => {
// Prevent default anchor onclick behaviour
event.preventDefault()
if (!opts.disabled) {
scrollstateHistoryHandler(event.currentTarget.getAttribute('href'))
}
})
}
// Internal function that ensures the argument of the link action is always an object
function linkOpts(val) {
if (val && typeof val == 'string') {
return {
href: val
}
}
else {
return val || {}
}
}
/**
* The handler attached to an anchor tag responsible for updating the
* current history state with the current scroll state
*
* @param {HTMLElementEventMap} event - an onclick event attached to an anchor tag
* @param {string} href - Destination
*/
function scrollstateHistoryHandler(event) {
// Prevent default anchor onclick behaviour
event.preventDefault()
const href = event.currentTarget.getAttribute('href')
function scrollstateHistoryHandler(href) {
// Setting the url (3rd arg) to href will break clicking for reasons, so don't try to do that
history.replaceState({...history.state, __svelte_spa_router_scrollX: window.scrollX, __svelte_spa_router_scrollY: window.scrollY}, undefined, undefined)
// This will force an update as desired, but this time our scroll state will be attached
Expand Down Expand Up @@ -375,7 +405,7 @@ class RouteItem {
* Executes all conditions (if any) to control whether the route can be shown. Conditions are executed in the order they are defined, and if a condition fails, the following ones aren't executed.
*
* @param {RouteDetail} detail - Route detail
* @returns {bool} Returns true if all the conditions succeeded
* @returns {boolean} Returns true if all the conditions succeeded
*/
async checkConditions(detail) {
for (let i = 0; i < this.conditions.length; i++) {
Expand Down
49 changes: 49 additions & 0 deletions test/app/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@
{/each}
</ul>

<!-- Testing links that can be disabled -->
<h2>Dynamic links</h2>
<ul class="navigation-disable-links">
{#each disableLinks as dl, i (dl.id)}
<li>
<a id="disable-link-{dl.id}" href="/foo" use:link={dl.opts} use:active>Dynamic Link {dl.id}</a>
-
<i id="toggle-link-{dl.id}" on:click={dl.toggle}>
{#if dl.opts.disabled}
enable link
{:else}
disable link
{/if}
</i>
</li>
{/each}
</ul>

<!-- Test use:active with a regular expression -->
<p><a href="#/" use:active={/\/*\/hi/}>This link</a> is active when you're matching <code>/*/hi</code></p>

Expand Down Expand Up @@ -137,4 +155,35 @@ let dynamicLinks = [
link: '/hello/dynamic-link-3'
}
]
// List of links that can be disabled
let disableLinks = [
{
id: 1,
opts: {
//href: '/hello/disable-link-1',
disabled: false
}
},
{
id: 2,
opts: {
//href: '/hello/disable-link-2',
disabled: false
}
},
{
id: 3,
opts: {
href: '/hello/disable-link-3',
disabled: false
}
}
].map((el) => {
el.toggle = () => {
el.opts.disabled = !el.opts.disabled
disableLinks = disableLinks
}
return el
})
</script>

0 comments on commit 7a7ba80

Please sign in to comment.