Skip to content

Commit d3f31a5

Browse files
feat: add slash forcing components and option
1 parent f07b3ff commit d3f31a5

File tree

3 files changed

+298
-22
lines changed

3 files changed

+298
-22
lines changed

README.md

+19-7
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ $ npm install vue-link
3333

3434
```js
3535
import VueLink from 'vue-link'
36+
// or the slash-forcing variants
37+
// import { VueLink, VueLinkAddSlash, VueLinkStripSlash } from 'vue-link'
3638

3739
export default {
3840
components: {
@@ -75,14 +77,23 @@ You can use the `external` prop to force treating it as external link as well.
7577
### Prop overview
7678

7779

78-
| Prop | External only? | Comment |
79-
| --- | --- | --- |
80-
| to | :x: | The target of the link. If not set, the link will not be bound (no "empty href")|
81-
| rel |:white_check_mark:| Will be passed as `rel` attribute to the anchor tag|
82-
|newTab|:white_check_mark:| If truthy, set `target` attribute to `_blank`|
83-
|target|:white_check_mark:| Will be passed as `target` attribute to the anchor tag|
84-
|external|:white_check_mark:| Force to treat the link as external link (use anchor instead of vue-router tag)|
80+
| Prop | External only? | Comment |
81+
| --- | --- | --- |
82+
| to | :x: | The target of the link. If not set, the link will not be bound (no "empty href")|
83+
| rel |:white_check_mark:| Will be passed as `rel` attribute to the anchor tag|
84+
|newTab |:white_check_mark:| If truthy, set `target` attribute to `_blank`|
85+
|target |:white_check_mark:| Will be passed as `target` attribute to the anchor tag|
86+
|slashes | Internal only! | Settings: `'strip'`, `'add'` or `false`(default). Will force slash endings to either strip or add trailing slashes to your url (`/a` -> `/a/` in `add` mode, vice-versa in `strip`. **Only for internal links**! Also, this will not take query strings into account. Please use `router-link`'s `query` option for them|
87+
|external |:white_check_mark:| Force to treat the link as external link (use anchor instead of vue-router tag)|
8588

89+
### Types
90+
91+
With `v.1.4.0` two extra components were introduced that reflect the `slashes` settings.
92+
You can import them (like the normal `VueLink` component as named imports).
93+
The `default` export of the package is still the normal `VueLink` component so no breaking changes
94+
have been introduced
95+
96+
`import { VueLink, VueLinkAddSlash, VueLinkStripSlash } from '../lib'`
8697

8798
### Example usage
8899

@@ -95,6 +106,7 @@ You can use the `external` prop to force treating it as external link as well.
95106
This is the link text ;)
96107
</vue-link>
97108
```
109+
98110
## :gear: Contributing
99111

100112
Please see our [CONTRIBUTING.md](./CONTRIBUTING.md)

lib/index.js

+56-14
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,64 @@
1-
export default {
1+
const slashOptions = {
2+
STRIP: 'strip',
3+
ADD: 'add'
4+
}
5+
6+
const modifySlashes = (option, url) => {
7+
if (!option) {
8+
return url
9+
}
10+
11+
if (option === slashOptions.ADD) {
12+
// Hash with slash before is already present
13+
if (/\/#/.test(url)) {
14+
return url
15+
}
16+
// Just a hash, it should add slash before
17+
if (/[^/]#/.test(url)) {
18+
return url.replace(/([^/])#/, '$1/#')
19+
}
20+
21+
// Has a trailing slash? If not, add one
22+
return /\/$/.test(url) ? url : `${url}/`
23+
}
24+
25+
// Strip
26+
return /\/$/.test(url) ? url.slice(0, -1) : url.replace('/#', '#')
27+
}
28+
29+
// If no `to` is set, treat as "external" to remove "link style" and bindings.
30+
// Quite useful if you want to bind a link dynamically and don't want to have it clicked and styled when not bound
31+
const isExternal = props => !props.to ||
32+
/^(http|\/\/)/.test(props.to) ||
33+
props.external
34+
35+
const vueLinkFactory = slashes => ({
236
functional: true,
3-
render (h, { data, children, props }) {
4-
// If no to is set, treat as "external" to remove "link style" and bindings.
5-
// Quite useful if you want to bind a link dynamically and don't want to have it clicked and styled when not bound
6-
const isExternal = props => !props.to ||
7-
props.to.startsWith('http') ||
8-
props.to.startsWith('//') ||
9-
props.external
10-
11-
return isExternal(props)
37+
render: (h, { data, children }) => {
38+
data.props = data.props || (data.props = {})
39+
data.props.slashes = data.props.slashes || slashes
40+
41+
const isLinkedToExternal = isExternal(data.props)
42+
43+
if (!isLinkedToExternal) {
44+
data.props.to = modifySlashes(data.props.slashes, data.props.to)
45+
}
46+
47+
return isLinkedToExternal
1248
? h('a', {
1349
...data,
1450
attrs: {
15-
href: props.to || undefined,
16-
rel: props.rel || undefined,
17-
target: props.target || (props.newTab ? '_blank' : undefined)
51+
href: data.props.to || undefined,
52+
rel: data.props.rel || undefined,
53+
target: data.props.target || (data.props.newTab ? '_blank' : undefined)
1854
}
1955
}, children)
2056
: h('router-link', data, children)
2157
}
22-
}
58+
})
59+
60+
export const VueLink = vueLinkFactory()
61+
export const VueLinkAddSlash = vueLinkFactory(slashOptions.ADD)
62+
export const VueLinkStripSlash = vueLinkFactory(slashOptions.STRIP)
63+
64+
export default VueLink

test/VueLink.spec.js

+223-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable no-console */
22
import { createLocalVue, mount, RouterLinkStub } from '@vue/test-utils'
33
import VueRouter from 'vue-router'
4-
import VueLink from '../lib'
4+
import { VueLink, VueLinkAddSlash, VueLinkStripSlash } from '../lib'
55

66
const localVue = createLocalVue()
77
localVue.use(VueRouter)
@@ -32,6 +32,30 @@ describe('VueLink', () => {
3232

3333
expect(link.vm.$props.to).toBe('/test')
3434
})
35+
it('adds slashes', () => {
36+
const wrapper = mount(VueLinkAddSlash, {
37+
localVue,
38+
attachToDocument: true,
39+
stubs: {
40+
RouterLink: RouterLinkStub
41+
},
42+
context: {
43+
props: {
44+
to: '/test'
45+
}
46+
},
47+
slots: {
48+
default: '<div>Hi</div>'
49+
}
50+
})
51+
52+
expect(wrapper.isVueInstance()).toBe(true)
53+
expect(wrapper.contains(RouterLinkStub)).toBe(true)
54+
55+
const link = wrapper.find(RouterLinkStub)
56+
57+
expect(link.vm.$props.to).toBe('/test/')
58+
})
3559
})
3660
describe('external', () => {
3761
it('does trigger external on http link', () => {
@@ -211,3 +235,201 @@ describe('VueLink', () => {
211235
})
212236
})
213237
})
238+
239+
describe('VueLinkStripSlash', () => {
240+
it('strips slashes', () => {
241+
const wrapper = mount(VueLinkStripSlash, {
242+
localVue,
243+
attachToDocument: true,
244+
stubs: {
245+
RouterLink: RouterLinkStub
246+
},
247+
context: {
248+
props: {
249+
to: '/test/'
250+
}
251+
},
252+
slots: {
253+
default: '<div>Hi</div>'
254+
}
255+
})
256+
257+
expect(wrapper.isVueInstance()).toBe(true)
258+
expect(wrapper.contains(RouterLinkStub)).toBe(true)
259+
260+
const link = wrapper.find(RouterLinkStub)
261+
262+
expect(link.vm.$props.to).toBe('/test')
263+
})
264+
it('strips from link with hash', () => {
265+
const wrapper = mount(VueLinkStripSlash, {
266+
localVue,
267+
attachToDocument: true,
268+
stubs: {
269+
RouterLink: RouterLinkStub
270+
},
271+
context: {
272+
props: {
273+
to: '/test/'
274+
}
275+
},
276+
slots: {
277+
default: '<div>Hi</div>'
278+
}
279+
})
280+
281+
expect(wrapper.isVueInstance()).toBe(true)
282+
expect(wrapper.contains(RouterLinkStub)).toBe(true)
283+
284+
const link = wrapper.find(RouterLinkStub)
285+
286+
expect(link.vm.$props.to).toBe('/test')
287+
})
288+
it('does not strip anything if no slash present', () => {
289+
const wrapper = mount(VueLinkStripSlash, {
290+
localVue,
291+
attachToDocument: true,
292+
stubs: {
293+
RouterLink: RouterLinkStub
294+
},
295+
context: {
296+
props: {
297+
to: '/test#abc'
298+
}
299+
},
300+
slots: {
301+
default: '<div>Hi</div>'
302+
}
303+
})
304+
305+
expect(wrapper.isVueInstance()).toBe(true)
306+
expect(wrapper.contains(RouterLinkStub)).toBe(true)
307+
308+
const link = wrapper.find(RouterLinkStub)
309+
310+
expect(link.vm.$props.to).toBe('/test#abc')
311+
})
312+
it('does not strip anything from link with hash without slash', () => {
313+
const wrapper = mount(VueLinkStripSlash, {
314+
localVue,
315+
attachToDocument: true,
316+
stubs: {
317+
RouterLink: RouterLinkStub
318+
},
319+
context: {
320+
props: {
321+
to: '/test#abc'
322+
}
323+
},
324+
slots: {
325+
default: '<div>Hi</div>'
326+
}
327+
})
328+
329+
expect(wrapper.isVueInstance()).toBe(true)
330+
expect(wrapper.contains(RouterLinkStub)).toBe(true)
331+
332+
const link = wrapper.find(RouterLinkStub)
333+
334+
expect(link.vm.$props.to).toBe('/test#abc')
335+
})
336+
})
337+
338+
describe('VueLinkAddSlash', () => {
339+
it('adds slashes', () => {
340+
const wrapper = mount(VueLinkAddSlash, {
341+
localVue,
342+
attachToDocument: true,
343+
stubs: {
344+
RouterLink: RouterLinkStub
345+
},
346+
context: {
347+
props: {
348+
to: '/test'
349+
}
350+
},
351+
slots: {
352+
default: '<div>Hi</div>'
353+
}
354+
})
355+
356+
expect(wrapper.isVueInstance()).toBe(true)
357+
expect(wrapper.contains(RouterLinkStub)).toBe(true)
358+
359+
const link = wrapper.find(RouterLinkStub)
360+
361+
expect(link.vm.$props.to).toBe('/test/')
362+
})
363+
it('adds slashes before hash', () => {
364+
const wrapper = mount(VueLinkAddSlash, {
365+
localVue,
366+
attachToDocument: true,
367+
stubs: {
368+
RouterLink: RouterLinkStub
369+
},
370+
context: {
371+
props: {
372+
to: '/test#abc'
373+
}
374+
},
375+
slots: {
376+
default: '<div>Hi</div>'
377+
}
378+
})
379+
380+
expect(wrapper.isVueInstance()).toBe(true)
381+
expect(wrapper.contains(RouterLinkStub)).toBe(true)
382+
383+
const link = wrapper.find(RouterLinkStub)
384+
385+
expect(link.vm.$props.to).toBe('/test/#abc')
386+
})
387+
it('does not add second slash', () => {
388+
const wrapper = mount(VueLinkAddSlash, {
389+
localVue,
390+
attachToDocument: true,
391+
stubs: {
392+
RouterLink: RouterLinkStub
393+
},
394+
context: {
395+
props: {
396+
to: '/test/'
397+
}
398+
},
399+
slots: {
400+
default: '<div>Hi</div>'
401+
}
402+
})
403+
404+
expect(wrapper.isVueInstance()).toBe(true)
405+
expect(wrapper.contains(RouterLinkStub)).toBe(true)
406+
407+
const link = wrapper.find(RouterLinkStub)
408+
409+
expect(link.vm.$props.to).toBe('/test/')
410+
})
411+
it('adds second slash before hash', () => {
412+
const wrapper = mount(VueLinkAddSlash, {
413+
localVue,
414+
attachToDocument: true,
415+
stubs: {
416+
RouterLink: RouterLinkStub
417+
},
418+
context: {
419+
props: {
420+
to: '/test/#abc'
421+
}
422+
},
423+
slots: {
424+
default: '<div>Hi</div>'
425+
}
426+
})
427+
428+
expect(wrapper.isVueInstance()).toBe(true)
429+
expect(wrapper.contains(RouterLinkStub)).toBe(true)
430+
431+
const link = wrapper.find(RouterLinkStub)
432+
433+
expect(link.vm.$props.to).toBe('/test/#abc')
434+
})
435+
})

0 commit comments

Comments
 (0)