Skip to content

Commit

Permalink
feat(b-link): add support 3rd party router links such as Gridsome's `…
Browse files Browse the repository at this point in the history
…<g-link>` (closes #2627) (#5358)

* feat(b-link): add support 3rd party router links such as Gridsome's `<g-link>`

* Update link.js

* Update router.js

* Update link.js

* Update router.js

* Update link.js

* Update link.spec.js

* Update link.spec.js

* Update link.spec.js

* Update link.spec.js

* Update router.js

* Update router.js

* Update link.spec.js

* Update link.spec.js

* Update common-props.json

* Update link.js

* Update link.js

* Update common-props.json

* Update README.md

* Update README.md

* Update avatar.js

* Update common-props.json

* Update README.md

* Update README.md

* Update README.md

* Update common-props.json

* Update package.json

* Update common-props.json

* Update package.json

* Update README.md

* Update avatar.js

* Update README.md

* Merge remote-tracking branch 'origin/dev' into blink-gridsome

* Make sure to always omit `<b-link>`'s `event` prop for other components

* Add `routerComponentName` to global config

* Update pagination-nav.js

* Update pagination-nav.js

* Omit `routerTag` for all other components

* Unify link detection in other components

* Update common-props.json

Co-authored-by: Jacob Müller <jacob.mueller.elz@gmail.com>
  • Loading branch information
tmorehouse and jacobmllr95 authored May 14, 2020
1 parent 8f3ca30 commit 6d29e1c
Show file tree
Hide file tree
Showing 22 changed files with 251 additions and 142 deletions.
35 changes: 20 additions & 15 deletions docs/common-props.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,44 +197,49 @@
"active": {
"description": "When set to 'true', places the component in the active state with active styling"
},
"href": {
"description": "<b-link> prop: Denotes the target URL of the link for standard a links"
},
"rel": {
"description": "Sets the 'rel' attribute on the rendered link"
"description": "<b-link> prop: Sets the 'rel' attribute on the rendered link"
},
"target": {
"description": "Sets the 'target' attribute on the rendered link"
},
"href": {
"description": "Denotes the target URL of the link for standard a links"
"description": "<b-link> prop: Sets the 'target' attribute on the rendered link"
},
"to": {
"description": "router-link prop: Denotes the target route of the link. When clicked, the value of the to prop will be passed to router.push() internally, so the value can be either a string or a Location descriptor object"
"description": "<router-link> prop: Denotes the target route of the link. When clicked, the value of the to prop will be passed to router.push() internally, so the value can be either a string or a Location descriptor object"
},
"replace": {
"description": "router-link prop: Setting the replace prop will call 'router.replace()' instead of 'router.push()' when clicked, so the navigation will not leave a history record"
"description": "<router-link> prop: Setting the replace prop will call 'router.replace()' instead of 'router.push()' when clicked, so the navigation will not leave a history record"
},
"append": {
"description": "router-link prop: Setting append prop always appends the relative path to the current path"
"description": "<router-link> prop: Setting append prop always appends the relative path to the current path"
},
"exact": {
"description": "router-link prop: The default active class matching behavior is inclusive match. Setting this prop forces the mode to exactly match the route"
"description": "<router-link> prop: The default active class matching behavior is inclusive match. Setting this prop forces the mode to exactly match the route"
},
"activeClass": {
"description": "router-link prop: Configure the active CSS class applied when the link is active. Typically you will want to set this to class name 'active'"
"description": "<router-link> prop: Configure the active CSS class applied when the link is active. Typically you will want to set this to class name 'active'"
},
"exactActiveClass": {
"description": "router-link prop: Configure the active CSS class applied when the link is active with exact match. Typically you will want to set this to class name 'active'"
"description": "<router-link> prop: Configure the active CSS class applied when the link is active with exact match. Typically you will want to set this to class name 'active'"
},
"routerTag": {
"description": "router-link prop: Specify which tag to render, and it will still listen to click events for navigation. 'router-tag' translates to the tag prop on the final rendered router-link. Typically you should use the default value"
"description": "<router-link> prop: Specify which tag to render, and it will still listen to click events for navigation. 'router-tag' translates to the tag prop on the final rendered router-link. Typically you should use the default value"
},
"event": {
"description": "router-link prop: Specify the event that triggers the link. In most cases you should leave this as the default"
"description": "<router-link> prop: Specify the event that triggers the link. In most cases you should leave this as the default"
},
"prefetch": {
"description": "nuxt-link prop: To improve the responsiveness of your Nuxt.js applications, when the link will be displayed within the viewport, Nuxt.js will automatically prefetch the code splitted page. Setting 'prefetch' to 'true' or 'false' will overwrite the default value of 'router.prefetchLinks'",
"description": "<nuxt-link> prop: To improve the responsiveness of your Nuxt.js applications, when the link will be displayed within the viewport, Nuxt.js will automatically prefetch the code splitted page. Setting 'prefetch' to 'true' or 'false' will overwrite the default value of 'router.prefetchLinks'",
"version": "2.15.0"
},
"noPrefetch": {
"description": "nuxt-link prop: To improve the responsiveness of your Nuxt.js applications, when the link will be displayed within the viewport, Nuxt.js will automatically prefetch the code splitted page. Setting 'no-prefetch' will disabled this feature for the specific link"
"description": "<nuxt-link> prop: To improve the responsiveness of your Nuxt.js applications, when the link will be displayed within the viewport, Nuxt.js will automatically prefetch the code splitted page. Setting 'no-prefetch' will disabled this feature for the specific link"
},
"routerComponentName": {
"description": "<b-link> prop: BootstrapVue auto detects between `<router-link>` and `<nuxt-link>`. In cases where you want to use a 3rd party link component based on `<router-link>`, set this prop to the component name. e.g. set it to 'g-link' if you are using Gridsome (note only `<router-link>` specific props are passed to the component)",
"version": "2.15.0",
"settings": true
}
}
32 changes: 31 additions & 1 deletion docs/markdown/reference/router-links/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
In the following sections, we are using the `<b-link>` component to render router links. `<b-link>`
is the building block of most of BootstrapVue's _actionable_ components. You could use any other
component that supports link generation such as [`<b-link>`](/docs/components/link),
[`<b-button>`](/docs/components/button), [`<b-breadcrumb-item>`](/docs/components/breadcrumb),
[`<b-button>`](/docs/components/button), [`<b-avatar>`](/docs/components/avatar),
[`<b-breadcrumb-item>`](/docs/components/breadcrumb),
[`<b-list-group-item>`](/docs/components/list-group), [`<b-nav-item>`](/docs/components/nav),
[`<b-dropdown-item>`](/docs/components/dropdown), and
[`<b-pagination-nav>`](/docs/components/pagination-nav). Note that not all props are available on
Expand Down Expand Up @@ -203,3 +204,32 @@ disabled this feature for the specific link.
**Note:** If you have prefetching disabled in your `nuxt.config.js` configuration
(`router: { prefetchLinks: false }`), or are using a version of Nuxt.js `< 2.4.0`, then this prop
will have no effect.

## Third-party router link support

<span class="badge badge-info small">v2.15.0+</span>

BootstrapVue auto detects using `<router-link>` and `<nuxt-link>` link components. Some 3rd party
frameworks also provide customized versions of `<router-link>`, such as
[Gridsome's `<g-link>` component](https://gridsome.org/docs/linking/). BootstrapVue can support
these third party `<router-link>` compatible components via the use of the `router-component-name`
prop. All `vue-router` props (excluding `<nuxt-link>` specific props) will be passed to the
specified router link component.

**Notes:**

- The 3rd party component will only be used when the `to` prop is set.
- Not all 3rd party components support all props supported by `<router-link>`, nor do not support
fully qualified domain name URLs, nor hash only URLs. Refer to the 3rd party component
documentation for details.

### `router-component-name`

- type: `string`
- default: `undefined`
- availability: BootstrapVue 2.15.0+

Set this prop to the name of the `<router-link>` compatible component, e.g. `'g-link'` for
[Gridsome](https://gridsome.org/).

If left at the default, BootstrapVue will automatically select `<router-link>` or `<nuxt-link>`.
32 changes: 9 additions & 23 deletions src/components/avatar/avatar.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import pluckProps from '../../utils/pluck-props'
import { getComponentConfig } from '../../utils/config'
import { isNumber, isString, isUndefinedOrNull } from '../../utils/inspect'
import { toFloat } from '../../utils/number'
import { omit } from '../../utils/object'
import { isLink } from '../../utils/router'
import { BButton } from '../button/button'
import { BLink, props as BLinkProps } from '../link/link'
import { BIcon } from '../../icons/icon'
Expand All @@ -25,23 +27,7 @@ const DEFAULT_SIZES = {
}

// --- Props ---
const linkProps = pluckProps(
[
'href',
'rel',
'target',
'disabled',
'to',
'append',
'replace',
'activeClass',
'exact',
'exactActiveClass',
'prefetch',
'noPrefetch'
],
BLinkProps
)
const linkProps = omit(BLinkProps, ['active', 'event', 'routerTag'])

const props = {
src: {
Expand Down Expand Up @@ -208,14 +194,14 @@ export const BAvatar = /*#__PURE__*/ Vue.extend({
fontStyle,
marginStyle,
computedSize: size,
button: isButton,
button,
buttonType: type,
badge,
badgeVariant,
badgeStyle
} = this
const isBLink = !isButton && (this.href || this.to)
const tag = isButton ? BButton : isBLink ? BLink : 'span'
const link = !button && isLink(this)
const tag = button ? BButton : link ? BLink : 'span'
const alt = this.alt || null
const ariaLabel = this.ariaLabel || null

Expand Down Expand Up @@ -261,7 +247,7 @@ export const BAvatar = /*#__PURE__*/ Vue.extend({
staticClass: CLASS_NAME,
class: {
// We use badge styles for theme variants when not rendering `BButton`
[`badge-${variant}`]: !isButton && variant,
[`badge-${variant}`]: !button && variant,
// Rounding/Square
rounded: rounded === true,
[`rounded-${rounded}`]: rounded && rounded !== true,
Expand All @@ -270,8 +256,8 @@ export const BAvatar = /*#__PURE__*/ Vue.extend({
},
style: { width: size, height: size, ...marginStyle },
attrs: { 'aria-label': ariaLabel || null },
props: isButton ? { variant, disabled, type } : isBLink ? pluckProps(linkProps, this) : {},
on: isBLink || isButton ? { click: this.onClick } : {}
props: button ? { variant, disabled, type } : link ? pluckProps(linkProps, this) : {},
on: button || link ? { click: this.onClick } : {}
}

return h(tag, componentData, [$content, $badge])
Expand Down
16 changes: 11 additions & 5 deletions src/components/badge/badge.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import Vue from '../../utils/vue'
import pluckProps from '../../utils/pluck-props'
import { mergeData } from 'vue-functional-data-merge'
import { getComponentConfig } from '../../utils/config'
import { clone } from '../../utils/object'
import { omit } from '../../utils/object'
import { isLink } from '../../utils/router'
import { BLink, props as BLinkProps } from '../link/link'

// --- Constants ---

const NAME = 'BBadge'

const linkProps = clone(BLinkProps)
// --- Props ---

const linkProps = omit(BLinkProps, ['event', 'routerTag'])
delete linkProps.href.default
delete linkProps.to.default

Expand All @@ -27,14 +32,15 @@ export const props = {
...linkProps
}

// --- Main component ---
// @vue/component
export const BBadge = /*#__PURE__*/ Vue.extend({
name: NAME,
functional: true,
props,
render(h, { props, data, children }) {
const isBLink = props.href || props.to
const tag = isBLink ? BLink : props.tag
const link = isLink(props)
const tag = link ? BLink : props.tag

const componentData = {
staticClass: 'badge',
Expand All @@ -46,7 +52,7 @@ export const BBadge = /*#__PURE__*/ Vue.extend({
disabled: props.disabled
}
],
props: isBLink ? pluckProps(linkProps, props) : {}
props: link ? pluckProps(linkProps, props) : {}
}

return h(tag, mergeData(data, componentData), children)
Expand Down
5 changes: 3 additions & 2 deletions src/components/breadcrumb/breadcrumb-link.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Vue from '../../utils/vue'
import { mergeData } from 'vue-functional-data-merge'
import Vue from '../../utils/vue'
import pluckProps from '../../utils/pluck-props'
import { htmlOrText } from '../../utils/html'
import { omit } from '../../utils/object'
import { BLink, props as BLinkProps } from '../link/link'

export const props = {
Expand All @@ -17,7 +18,7 @@ export const props = {
type: String,
default: 'location'
},
...BLinkProps
...omit(BLinkProps, ['event', 'routerTag'])
}

// @vue/component
Expand Down
17 changes: 7 additions & 10 deletions src/components/button/button.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import KeyCodes from '../../utils/key-codes'
import pluckProps from '../../utils/pluck-props'
import { concat } from '../../utils/array'
import { getComponentConfig } from '../../utils/config'
import { addClass, removeClass } from '../../utils/dom'
import { addClass, isTag, removeClass } from '../../utils/dom'
import { isBoolean, isEvent, isFunction } from '../../utils/inspect'
import { clone } from '../../utils/object'
import { toString } from '../../utils/string'
import { omit } from '../../utils/object'
import { isLink as isLinkStrict } from '../../utils/router'
import { BLink, props as BLinkProps } from '../link/link'

// --- Constants ---
Expand All @@ -16,7 +16,7 @@ const NAME = 'BButton'

// --- Props ---

const linkProps = clone(BLinkProps)
const linkProps = omit(BLinkProps, ['event', 'routerTag'])
delete linkProps.href.default
delete linkProps.to.default

Expand Down Expand Up @@ -65,9 +65,6 @@ export const props = { ...btnProps, ...linkProps }

// --- Helper methods ---

// Returns `true` if a tag's name equals `name`
const tagIs = (tag, name) => toString(tag).toLowerCase() === toString(name).toLowerCase()

// Focus handler for toggle buttons
// Needs class of 'focus' when focused
const handleFocus = evt => {
Expand All @@ -80,13 +77,13 @@ const handleFocus = evt => {

// Is the requested button a link?
// If tag prop is set to `a`, we use a <b-link> to get proper disabled handling
const isLink = props => props.href || props.to || tagIs(props.tag, 'a')
const isLink = props => isLinkStrict(props) || isTag(props.tag, 'a')

// Is the button to be a toggle button?
const isToggle = props => isBoolean(props.pressed)

// Is the button "really" a button?
const isButton = props => !(isLink(props) || (props.tag && !tagIs(props.tag, 'button')))
const isButton = props => !(isLink(props) || (props.tag && !isTag(props.tag, 'button')))

// Is the requested tag not a button or link?
const isNonStandardTag = props => !isLink(props) && !isButton(props)
Expand All @@ -105,7 +102,7 @@ const computeClass = props => [
]

// Compute the link props to pass to b-link (if required)
const computeLinkProps = props => (isLink(props) ? pluckProps(linkProps, props) : null)
const computeLinkProps = props => (isLink(props) ? pluckProps(linkProps, props) : {})

// Compute the attributes for a button
const computeAttrs = (props, data) => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/dropdown/dropdown-item.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import Vue from '../../utils/vue'
import { requestAF } from '../../utils/dom'
import { clone } from '../../utils/object'
import { omit } from '../../utils/object'
import attrsMixin from '../../mixins/attrs'
import normalizeSlotMixin from '../../mixins/normalize-slot'
import { BLink, props as BLinkProps } from '../link/link'

export const props = clone(BLinkProps)
export const props = omit(BLinkProps, ['event', 'routerTag'])

// @vue/component
export const BDropdownItem = /*#__PURE__*/ Vue.extend({
Expand Down
2 changes: 1 addition & 1 deletion src/components/dropdown/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
},
{
"prop": "splitTo",
"description": "router-link prop: Denotes the target route of the split button. When clicked, the value of the to prop will be passed to router.push() internally, so the value can be either a string or a Location descriptor object"
"description": "<router-link> prop: Denotes the target route of the split button. When clicked, the value of the to prop will be passed to router.push() internally, so the value can be either a string or a Location descriptor object"
},
{
"prop": "splitVariant",
Expand Down
11 changes: 11 additions & 0 deletions src/components/link/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ If your app is running under [Nuxt.js](https://nuxtjs.org), the
`<router-link>`. The `<nuxt-link>` component supports all the same features as `<router-link>` (as
it is a wrapper component for `<router-link>`) and more.

### Third party rounter links

BootstrapVue auto detects using `<router-link>` and `<nuxt-link>` link components. Some 3rd party
frameworks also provide customized versions of `<router-link>`, such as
[Gridsome's `<g-link>` component](https://gridsome.org/docs/linking/). `<b-link>` can support these
third party `<router-link>` compatible components via the use of the `router-component-name` prop.
All `vue-router` props (excluding `<nuxt-link>` specific props) will be passed to the specified
router link component.

Note that the 3rd party component will only be used when the `to` prop is set.

## Links with `href="#"`

Typically `<a href="#">` will cause the document to scroll to the top of page when clicked.
Expand Down
20 changes: 17 additions & 3 deletions src/components/link/link.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import Vue from '../../utils/vue'
import pluckProps from '../../utils/pluck-props'
import { concat } from '../../utils/array'
import { getComponentConfig } from '../../utils/config'
import { attemptBlur, attemptFocus } from '../../utils/dom'
import { isBoolean, isEvent, isFunction, isUndefined } from '../../utils/inspect'
import { computeHref, computeRel, computeTag, isRouterLink } from '../../utils/router'
import attrsMixin from '../../mixins/attrs'
import listenersMixin from '../../mixins/listeners'
import normalizeSlotMixin from '../../mixins/normalize-slot'

// --- Constants ---

const NAME = 'BLink'

// --- Props ---

// <router-link> specific props
Expand Down Expand Up @@ -87,7 +92,15 @@ export const props = {
default: false
},
...routerLinkProps,
...nuxtLinkProps
...nuxtLinkProps,
// To support 3rd party router links based on `<router-link>` (i.e. `g-link` for Gridsome)
// Default is to auto choose between `<router-link>` and `<nuxt-link>`
// Gridsome doesn't provide a mechanism to auto detect and has caveats
// such as not supporting FQDN URLs or hash only URLs
routerComponentName: {
type: String,
default: () => getComponentConfig(NAME, 'routerComponentName')
}
}

// --- Main component ---
Expand All @@ -101,7 +114,8 @@ export const BLink = /*#__PURE__*/ Vue.extend({
computed: {
computedTag() {
// We don't pass `this` as the first arg as we need reactivity of the props
return computeTag({ to: this.to, disabled: this.disabled }, this)
const { to, disabled, routerComponentName } = this
return computeTag({ to, disabled, routerComponentName }, this)
},
isRouterLink() {
return isRouterLink(this.computedTag)
Expand All @@ -118,7 +132,7 @@ export const BLink = /*#__PURE__*/ Vue.extend({
const prefetch = this.prefetch
return this.isRouterLink
? {
...pluckProps({ ...routerLinkProps, ...nuxtLinkProps }, this.$props),
...pluckProps({ ...routerLinkProps, ...nuxtLinkProps }, this),
// Coerce `prefetch` value `null` to be `undefined`
prefetch: isBoolean(prefetch) ? prefetch : undefined,
// Pass `router-tag` as `tag` prop
Expand Down
Loading

0 comments on commit 6d29e1c

Please sign in to comment.