Skip to content

Commit 6d29e1c

Browse files
feat(b-link): add support 3rd party router links such as Gridsome's <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>
1 parent 8f3ca30 commit 6d29e1c

File tree

22 files changed

+251
-142
lines changed

22 files changed

+251
-142
lines changed

docs/common-props.json

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -197,44 +197,49 @@
197197
"active": {
198198
"description": "When set to 'true', places the component in the active state with active styling"
199199
},
200+
"href": {
201+
"description": "<b-link> prop: Denotes the target URL of the link for standard a links"
202+
},
200203
"rel": {
201-
"description": "Sets the 'rel' attribute on the rendered link"
204+
"description": "<b-link> prop: Sets the 'rel' attribute on the rendered link"
202205
},
203206
"target": {
204-
"description": "Sets the 'target' attribute on the rendered link"
205-
},
206-
"href": {
207-
"description": "Denotes the target URL of the link for standard a links"
207+
"description": "<b-link> prop: Sets the 'target' attribute on the rendered link"
208208
},
209209
"to": {
210-
"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"
210+
"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"
211211
},
212212
"replace": {
213-
"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"
213+
"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"
214214
},
215215
"append": {
216-
"description": "router-link prop: Setting append prop always appends the relative path to the current path"
216+
"description": "<router-link> prop: Setting append prop always appends the relative path to the current path"
217217
},
218218
"exact": {
219-
"description": "router-link prop: The default active class matching behavior is inclusive match. Setting this prop forces the mode to exactly match the route"
219+
"description": "<router-link> prop: The default active class matching behavior is inclusive match. Setting this prop forces the mode to exactly match the route"
220220
},
221221
"activeClass": {
222-
"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'"
222+
"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'"
223223
},
224224
"exactActiveClass": {
225-
"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'"
225+
"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'"
226226
},
227227
"routerTag": {
228-
"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"
228+
"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"
229229
},
230230
"event": {
231-
"description": "router-link prop: Specify the event that triggers the link. In most cases you should leave this as the default"
231+
"description": "<router-link> prop: Specify the event that triggers the link. In most cases you should leave this as the default"
232232
},
233233
"prefetch": {
234-
"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'",
234+
"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'",
235235
"version": "2.15.0"
236236
},
237237
"noPrefetch": {
238-
"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"
238+
"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"
239+
},
240+
"routerComponentName": {
241+
"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)",
242+
"version": "2.15.0",
243+
"settings": true
239244
}
240245
}

docs/markdown/reference/router-links/README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
In the following sections, we are using the `<b-link>` component to render router links. `<b-link>`
1111
is the building block of most of BootstrapVue's _actionable_ components. You could use any other
1212
component that supports link generation such as [`<b-link>`](/docs/components/link),
13-
[`<b-button>`](/docs/components/button), [`<b-breadcrumb-item>`](/docs/components/breadcrumb),
13+
[`<b-button>`](/docs/components/button), [`<b-avatar>`](/docs/components/avatar),
14+
[`<b-breadcrumb-item>`](/docs/components/breadcrumb),
1415
[`<b-list-group-item>`](/docs/components/list-group), [`<b-nav-item>`](/docs/components/nav),
1516
[`<b-dropdown-item>`](/docs/components/dropdown), and
1617
[`<b-pagination-nav>`](/docs/components/pagination-nav). Note that not all props are available on
@@ -203,3 +204,32 @@ disabled this feature for the specific link.
203204
**Note:** If you have prefetching disabled in your `nuxt.config.js` configuration
204205
(`router: { prefetchLinks: false }`), or are using a version of Nuxt.js `< 2.4.0`, then this prop
205206
will have no effect.
207+
208+
## Third-party router link support
209+
210+
<span class="badge badge-info small">v2.15.0+</span>
211+
212+
BootstrapVue auto detects using `<router-link>` and `<nuxt-link>` link components. Some 3rd party
213+
frameworks also provide customized versions of `<router-link>`, such as
214+
[Gridsome's `<g-link>` component](https://gridsome.org/docs/linking/). BootstrapVue can support
215+
these third party `<router-link>` compatible components via the use of the `router-component-name`
216+
prop. All `vue-router` props (excluding `<nuxt-link>` specific props) will be passed to the
217+
specified router link component.
218+
219+
**Notes:**
220+
221+
- The 3rd party component will only be used when the `to` prop is set.
222+
- Not all 3rd party components support all props supported by `<router-link>`, nor do not support
223+
fully qualified domain name URLs, nor hash only URLs. Refer to the 3rd party component
224+
documentation for details.
225+
226+
### `router-component-name`
227+
228+
- type: `string`
229+
- default: `undefined`
230+
- availability: BootstrapVue 2.15.0+
231+
232+
Set this prop to the name of the `<router-link>` compatible component, e.g. `'g-link'` for
233+
[Gridsome](https://gridsome.org/).
234+
235+
If left at the default, BootstrapVue will automatically select `<router-link>` or `<nuxt-link>`.

src/components/avatar/avatar.js

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import pluckProps from '../../utils/pluck-props'
33
import { getComponentConfig } from '../../utils/config'
44
import { isNumber, isString, isUndefinedOrNull } from '../../utils/inspect'
55
import { toFloat } from '../../utils/number'
6+
import { omit } from '../../utils/object'
7+
import { isLink } from '../../utils/router'
68
import { BButton } from '../button/button'
79
import { BLink, props as BLinkProps } from '../link/link'
810
import { BIcon } from '../../icons/icon'
@@ -25,23 +27,7 @@ const DEFAULT_SIZES = {
2527
}
2628

2729
// --- Props ---
28-
const linkProps = pluckProps(
29-
[
30-
'href',
31-
'rel',
32-
'target',
33-
'disabled',
34-
'to',
35-
'append',
36-
'replace',
37-
'activeClass',
38-
'exact',
39-
'exactActiveClass',
40-
'prefetch',
41-
'noPrefetch'
42-
],
43-
BLinkProps
44-
)
30+
const linkProps = omit(BLinkProps, ['active', 'event', 'routerTag'])
4531

4632
const props = {
4733
src: {
@@ -208,14 +194,14 @@ export const BAvatar = /*#__PURE__*/ Vue.extend({
208194
fontStyle,
209195
marginStyle,
210196
computedSize: size,
211-
button: isButton,
197+
button,
212198
buttonType: type,
213199
badge,
214200
badgeVariant,
215201
badgeStyle
216202
} = this
217-
const isBLink = !isButton && (this.href || this.to)
218-
const tag = isButton ? BButton : isBLink ? BLink : 'span'
203+
const link = !button && isLink(this)
204+
const tag = button ? BButton : link ? BLink : 'span'
219205
const alt = this.alt || null
220206
const ariaLabel = this.ariaLabel || null
221207

@@ -261,7 +247,7 @@ export const BAvatar = /*#__PURE__*/ Vue.extend({
261247
staticClass: CLASS_NAME,
262248
class: {
263249
// We use badge styles for theme variants when not rendering `BButton`
264-
[`badge-${variant}`]: !isButton && variant,
250+
[`badge-${variant}`]: !button && variant,
265251
// Rounding/Square
266252
rounded: rounded === true,
267253
[`rounded-${rounded}`]: rounded && rounded !== true,
@@ -270,8 +256,8 @@ export const BAvatar = /*#__PURE__*/ Vue.extend({
270256
},
271257
style: { width: size, height: size, ...marginStyle },
272258
attrs: { 'aria-label': ariaLabel || null },
273-
props: isButton ? { variant, disabled, type } : isBLink ? pluckProps(linkProps, this) : {},
274-
on: isBLink || isButton ? { click: this.onClick } : {}
259+
props: button ? { variant, disabled, type } : link ? pluckProps(linkProps, this) : {},
260+
on: button || link ? { click: this.onClick } : {}
275261
}
276262

277263
return h(tag, componentData, [$content, $badge])

src/components/badge/badge.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import Vue from '../../utils/vue'
22
import pluckProps from '../../utils/pluck-props'
33
import { mergeData } from 'vue-functional-data-merge'
44
import { getComponentConfig } from '../../utils/config'
5-
import { clone } from '../../utils/object'
5+
import { omit } from '../../utils/object'
6+
import { isLink } from '../../utils/router'
67
import { BLink, props as BLinkProps } from '../link/link'
78

9+
// --- Constants ---
10+
811
const NAME = 'BBadge'
912

10-
const linkProps = clone(BLinkProps)
13+
// --- Props ---
14+
15+
const linkProps = omit(BLinkProps, ['event', 'routerTag'])
1116
delete linkProps.href.default
1217
delete linkProps.to.default
1318

@@ -27,14 +32,15 @@ export const props = {
2732
...linkProps
2833
}
2934

35+
// --- Main component ---
3036
// @vue/component
3137
export const BBadge = /*#__PURE__*/ Vue.extend({
3238
name: NAME,
3339
functional: true,
3440
props,
3541
render(h, { props, data, children }) {
36-
const isBLink = props.href || props.to
37-
const tag = isBLink ? BLink : props.tag
42+
const link = isLink(props)
43+
const tag = link ? BLink : props.tag
3844

3945
const componentData = {
4046
staticClass: 'badge',
@@ -46,7 +52,7 @@ export const BBadge = /*#__PURE__*/ Vue.extend({
4652
disabled: props.disabled
4753
}
4854
],
49-
props: isBLink ? pluckProps(linkProps, props) : {}
55+
props: link ? pluckProps(linkProps, props) : {}
5056
}
5157

5258
return h(tag, mergeData(data, componentData), children)

src/components/breadcrumb/breadcrumb-link.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import Vue from '../../utils/vue'
21
import { mergeData } from 'vue-functional-data-merge'
2+
import Vue from '../../utils/vue'
33
import pluckProps from '../../utils/pluck-props'
44
import { htmlOrText } from '../../utils/html'
5+
import { omit } from '../../utils/object'
56
import { BLink, props as BLinkProps } from '../link/link'
67

78
export const props = {
@@ -17,7 +18,7 @@ export const props = {
1718
type: String,
1819
default: 'location'
1920
},
20-
...BLinkProps
21+
...omit(BLinkProps, ['event', 'routerTag'])
2122
}
2223

2324
// @vue/component

src/components/button/button.js

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import KeyCodes from '../../utils/key-codes'
44
import pluckProps from '../../utils/pluck-props'
55
import { concat } from '../../utils/array'
66
import { getComponentConfig } from '../../utils/config'
7-
import { addClass, removeClass } from '../../utils/dom'
7+
import { addClass, isTag, removeClass } from '../../utils/dom'
88
import { isBoolean, isEvent, isFunction } from '../../utils/inspect'
9-
import { clone } from '../../utils/object'
10-
import { toString } from '../../utils/string'
9+
import { omit } from '../../utils/object'
10+
import { isLink as isLinkStrict } from '../../utils/router'
1111
import { BLink, props as BLinkProps } from '../link/link'
1212

1313
// --- Constants ---
@@ -16,7 +16,7 @@ const NAME = 'BButton'
1616

1717
// --- Props ---
1818

19-
const linkProps = clone(BLinkProps)
19+
const linkProps = omit(BLinkProps, ['event', 'routerTag'])
2020
delete linkProps.href.default
2121
delete linkProps.to.default
2222

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

6666
// --- Helper methods ---
6767

68-
// Returns `true` if a tag's name equals `name`
69-
const tagIs = (tag, name) => toString(tag).toLowerCase() === toString(name).toLowerCase()
70-
7168
// Focus handler for toggle buttons
7269
// Needs class of 'focus' when focused
7370
const handleFocus = evt => {
@@ -80,13 +77,13 @@ const handleFocus = evt => {
8077

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

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

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

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

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

110107
// Compute the attributes for a button
111108
const computeAttrs = (props, data) => {

src/components/dropdown/dropdown-item.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import Vue from '../../utils/vue'
22
import { requestAF } from '../../utils/dom'
3-
import { clone } from '../../utils/object'
3+
import { omit } from '../../utils/object'
44
import attrsMixin from '../../mixins/attrs'
55
import normalizeSlotMixin from '../../mixins/normalize-slot'
66
import { BLink, props as BLinkProps } from '../link/link'
77

8-
export const props = clone(BLinkProps)
8+
export const props = omit(BLinkProps, ['event', 'routerTag'])
99

1010
// @vue/component
1111
export const BDropdownItem = /*#__PURE__*/ Vue.extend({

src/components/dropdown/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
},
9898
{
9999
"prop": "splitTo",
100-
"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"
100+
"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"
101101
},
102102
{
103103
"prop": "splitVariant",

src/components/link/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ If your app is running under [Nuxt.js](https://nuxtjs.org), the
2626
`<router-link>`. The `<nuxt-link>` component supports all the same features as `<router-link>` (as
2727
it is a wrapper component for `<router-link>`) and more.
2828

29+
### Third party rounter links
30+
31+
BootstrapVue auto detects using `<router-link>` and `<nuxt-link>` link components. Some 3rd party
32+
frameworks also provide customized versions of `<router-link>`, such as
33+
[Gridsome's `<g-link>` component](https://gridsome.org/docs/linking/). `<b-link>` can support these
34+
third party `<router-link>` compatible components via the use of the `router-component-name` prop.
35+
All `vue-router` props (excluding `<nuxt-link>` specific props) will be passed to the specified
36+
router link component.
37+
38+
Note that the 3rd party component will only be used when the `to` prop is set.
39+
2940
## Links with `href="#"`
3041

3142
Typically `<a href="#">` will cause the document to scroll to the top of page when clicked.

src/components/link/link.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import Vue from '../../utils/vue'
22
import pluckProps from '../../utils/pluck-props'
33
import { concat } from '../../utils/array'
4+
import { getComponentConfig } from '../../utils/config'
45
import { attemptBlur, attemptFocus } from '../../utils/dom'
56
import { isBoolean, isEvent, isFunction, isUndefined } from '../../utils/inspect'
67
import { computeHref, computeRel, computeTag, isRouterLink } from '../../utils/router'
78
import attrsMixin from '../../mixins/attrs'
89
import listenersMixin from '../../mixins/listeners'
910
import normalizeSlotMixin from '../../mixins/normalize-slot'
1011

12+
// --- Constants ---
13+
14+
const NAME = 'BLink'
15+
1116
// --- Props ---
1217

1318
// <router-link> specific props
@@ -87,7 +92,15 @@ export const props = {
8792
default: false
8893
},
8994
...routerLinkProps,
90-
...nuxtLinkProps
95+
...nuxtLinkProps,
96+
// To support 3rd party router links based on `<router-link>` (i.e. `g-link` for Gridsome)
97+
// Default is to auto choose between `<router-link>` and `<nuxt-link>`
98+
// Gridsome doesn't provide a mechanism to auto detect and has caveats
99+
// such as not supporting FQDN URLs or hash only URLs
100+
routerComponentName: {
101+
type: String,
102+
default: () => getComponentConfig(NAME, 'routerComponentName')
103+
}
91104
}
92105

93106
// --- Main component ---
@@ -101,7 +114,8 @@ export const BLink = /*#__PURE__*/ Vue.extend({
101114
computed: {
102115
computedTag() {
103116
// We don't pass `this` as the first arg as we need reactivity of the props
104-
return computeTag({ to: this.to, disabled: this.disabled }, this)
117+
const { to, disabled, routerComponentName } = this
118+
return computeTag({ to, disabled, routerComponentName }, this)
105119
},
106120
isRouterLink() {
107121
return isRouterLink(this.computedTag)
@@ -118,7 +132,7 @@ export const BLink = /*#__PURE__*/ Vue.extend({
118132
const prefetch = this.prefetch
119133
return this.isRouterLink
120134
? {
121-
...pluckProps({ ...routerLinkProps, ...nuxtLinkProps }, this.$props),
135+
...pluckProps({ ...routerLinkProps, ...nuxtLinkProps }, this),
122136
// Coerce `prefetch` value `null` to be `undefined`
123137
prefetch: isBoolean(prefetch) ? prefetch : undefined,
124138
// Pass `router-tag` as `tag` prop

0 commit comments

Comments
 (0)