Skip to content

Commit

Permalink
fix(v-b-visible): fix type error in componentUpdated hook + minor d…
Browse files Browse the repository at this point in the history
…ocs update/fixes (#4327)
  • Loading branch information
tmorehouse committed Oct 31, 2019
1 parent 3ba1870 commit 5f3ba9e
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 33 deletions.
3 changes: 2 additions & 1 deletion src/components/table/helpers/mixin-items.js
@@ -1,5 +1,6 @@
import looseEqual from '../../../utils/loose-equal'
import { isArray, isFunction, isNull, isString, isUndefined } from '../../../utils/inspect'
import { clone } from '../../../utils/object'
import normalizeFields from './normalize-fields'

export default {
Expand Down Expand Up @@ -50,7 +51,7 @@ export default {
const parent = this.$parent
return this.computedFields.reduce((obj, f) => {
// We use object spread here so we don't mutate the original field object
obj[f.key] = { ...f }
obj[f.key] = clone(f)
if (f.formatter) {
// Normalize formatter to a function ref or `undefined`
let formatter = f.formatter
Expand Down
3 changes: 2 additions & 1 deletion src/components/table/helpers/mixin-provider.js
@@ -1,6 +1,7 @@
import looseEqual from '../../../utils/loose-equal'
import warn from '../../../utils/warn'
import { isArray, isFunction, isPromise } from '../../../utils/inspect'
import { clone } from '../../../utils/object'
import listenOnRootMixin from '../../../mixins/listen-on-root'

export default {
Expand Down Expand Up @@ -62,7 +63,7 @@ export default {
ctx.perPage = this.perPage
ctx.currentPage = this.currentPage
}
return { ...ctx }
return clone(ctx)
}
},
watch: {
Expand Down
6 changes: 3 additions & 3 deletions src/components/table/helpers/normalize-fields.js
@@ -1,6 +1,6 @@
import startCase from '../../../utils/startcase'
import { isArray, isFunction, isObject, isString } from '../../../utils/inspect'
import { keys } from '../../../utils/object'
import { clone, keys } from '../../../utils/object'
import { IGNORED_FIELD_KEYS } from './constants'

// Private function to massage field entry into common object format
Expand All @@ -13,7 +13,7 @@ const processField = (key, value) => {
// Formatter shortcut
field = { key: key, formatter: value }
} else if (isObject(value)) {
field = { ...value }
field = clone(value)
field.key = field.key || key
} else if (value !== false) {
// Fallback to just key
Expand All @@ -35,7 +35,7 @@ const normalizeFields = (origFields, items) => {
fields.push({ key: f, label: startCase(f) })
} else if (isObject(f) && f.key && isString(f.key)) {
// Full object definition. We use assign so that we don't mutate the original
fields.push({ ...f })
fields.push(clone(f))
} else if (isObject(f) && keys(f).length === 1) {
// Shortcut object (i.e. { 'foo_bar': 'This is Foo Bar' }
const key = keys(f)[0]
Expand Down
147 changes: 126 additions & 21 deletions src/directives/visible/README.md
@@ -1,49 +1,57 @@
# Visible

> The `v-b-visible` directive allows you to react when an element becomes visible in the viewport.
> `v-b-visible` is a lightweight directive that allows you to react when an element becomes visible
> in the viewport and/or when it moves out of the viewport (or is no longer visible).
The `v-b-visible` directive was added in version `2.1.0`.

## Overview

- `v-b-visible` will call your callback method with a boolean value indicating if the element is
visible (intersecting) with the viewport.
visible (intersecting with the viewport) or not.
- The directive can be placed on almost any element or component.
- Changes in visibility cqn also be detected (such as `display: none`), as long as the element is
within (or partially within) the viewport, or within the optional offset.
- Several BootstrapVue components use `v-b-visible`, such as `<b-img-lazy>`.
- Changes in visibility can also be detected (such as `display: none`), as long as the element is
within (or partially within) the viewport, or within the optional offset. Note: transitioning to a
non-visible state due to `v-if="false"` _cannot_ be detected.
- Internally, BootstrapVue uses this directive in several components, such as `<b-img-lazy>`.
- The `v-b-visible` directive requires browser support of
[`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
For older browsers that do not support `IntersectionObserver`, you will need to use a
[polyfill](/docs/#js).
- If `IntersectionObserver` support is not detected, then `v-b-visible` will assume the element is
_always visible_, and will call the callback once with the argument set to `true`.

## Directive syntax and usage

```html
<div v-b-visible.[mod].[...]="callback">content</div>
<div v-b-visible.[mod1].[mod2]="callback">content</div>
```

Where `callback` is required:

- A function reference that will be called whenever visibility changes. The callback is passed a
single boolean argument. `true` indicates that the element is intersecting (partially or entirely
visible) in the viewport, or `false` if the element is not visible/intersecting with the viewport.
The callback will be called each time the element's visibility changes (except when hte `once`
The callback will be called each time the element's visibility changes (except when the `once`
modifier is used. See below for details)

Where `[mod]` can be (all optional):
Where `[mod1]` or `[mod2]` can be (all optional):

- A positive number representing the offset (margin) in pixels _away_ from the edge of the viewport
to determine when the element is considered in (or just about to be in) the viewport. The value
adds a margin around the view port. The default value is `0`.
- The keyword `once`. When this modifier is present, the callback will be called once (with the
argument of `true` indicating the element is intersecting/visible) when the element is
intersecting with the viewport. Note the callback may be called prior to this with an argument of
`false` signifying the element is not intersecting/visible.
- A positive integer number representing the offset (margin) in pixels _away_ from the edge of the
_viewport_ to determine when the element is considered in (or just about to be in) the viewport.
The value adds a margin around the viewport. The default value is `0`.
- The keyword `once`. When this modifier is present, the callback will be called only once the first
time the element is visible (with the argument of `true` indicating the element is
intersecting/visible). Note the callback _may be_ called prior to this with an argument of `false`
signifying the element is not intersecting/visible.

### Usage examples
The order of the modifiers is not important.

Basic:
### Usage syntax examples

In all use cases, the callback function is required.

#### Basic (no modifiers)

```html
<template>
Expand All @@ -64,8 +72,10 @@ export default {
</script>
```

With viewport offset modifier of 350px (if the element is outside of the physical viewport by at
least 350px, then it will be considered "visible"):
#### With viewport offset modifier

In this example, the modifier value represents 350px (if the element is outside of the physical
viewport by at least 350px, then it will be considered "visible"):

```html
<template>
Expand All @@ -86,7 +96,7 @@ export default {
</script>
```

With `once` modifier:
#### With the `once` modifier

```html
<template>
Expand All @@ -110,7 +120,7 @@ export default {
</script>
```

With `once` and offset modifiers:
#### With both `once` and offset modifiers

```html
<template>
Expand All @@ -133,3 +143,98 @@ export default {
}
</script>
```

## Live examples

Here are two live examples showing two common use cases.

### Visibility of scrolled content

Scroll the container to see the reaction when the `<b-badge>` scrolls into view:

```html
<template>
<div>
<div
:class="[isVisible ? 'bg-info' : 'bg-light', 'border', 'p-2', 'text-center']"
style="height: 85px; overflow-y: scroll;"
>
<p>{{ text }}</p>
<b-badge v-b-visible="handleVisibility">Element with v-b-visible directive</b-badge>
<p>{{ text }}</p>
</div>
<p class="mt-2">
Visible: {{ isVisible }}
</p>
</div>
</template>

<script>
export default {
data() {
return {
isVisible: false,
text: `
Quis magna Lorem anim amet ipsum do mollit sit cillum voluptate ex nulla
tempor. Laborum consequat non elit enim exercitation cillum aliqua
consequat id aliqua. Esse ex consectetur mollit voluptate est in duis
laboris ad sit ipsum anim Lorem. Incididunt veniam velit elit elit veniam
Lorem aliqua quis ullamco deserunt sit enim elit aliqua esse irure. Laborum
nisi sit est tempor laborum mollit labore officia laborum excepteur commodo
non commodo dolor excepteur commodo. Ipsum fugiat ex est consectetur ipsum
commodo tempor sunt in proident. Non elixir food exorcism nacho tequila tasty.
`
}
},
methods: {
handleVisibility(isVisible) {
this.isVisible = isVisible
}
}
}
</script>

<!-- v-b-visible-scroll.vue -->
```

### CSS display visibility detection

Click the button to change the `<div>` visibility state:

```html
<template>
<div>
<b-button @click="show = !show" class="mb-2">Toggle display</b-button>
<p>Visible: {{ isVisible }}</p>
<div class="border p-3" style="height: 6em;">
<!-- We use Vue's `v-show` directive to control the CSS `display` of the div -->
<div v-show="show" class="bg-info p-3">
<b-badge v-b-visible="handleVisibility">Element with v-b-visible directive</b-badge>
</div>
</div>
</div>
</template>

<script>
export default {
data() {
return {
show: true,
isVisible: false
}
},
methods: {
handleVisibility(isVisible) {
this.isVisible = isVisible
}
}
}
</script>

<!-- v-b-visible-display.vue -->
```

## See also

For more details on `IntersectionObserver`, refer to the
[MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
13 changes: 8 additions & 5 deletions src/directives/visible/visible.js
Expand Up @@ -34,7 +34,7 @@
import looseEqual from '../../utils/loose-equal'
import { requestAF } from '../../utils/dom'
import { isFunction } from '../../utils/inspect'
import { keys } from '../../utils/object'
import { clone, keys } from '../../utils/object'

const OBSERVER_PROP_NAME = '__bv__visibility_observer'

Expand Down Expand Up @@ -149,18 +149,21 @@ const bind = (el, { value, modifiers }, vnode) => {
// Create new observer
el[OBSERVER_PROP_NAME] = new VisibilityObserver(el, options, vnode)
// Store the current modifiers on the object (cloned)
el[OBSERVER_PROP_NAME]._prevModifiers = { ...modifiers }
el[OBSERVER_PROP_NAME]._prevModifiers = clone(modifiers)
}

// When the directive options may have been updated (or element)
const componentUpdated = (el, { value, oldValue, modifiers }, vnode) => {
// Compare value/oldValue and modifiers to see if anything has changed
// and if so, destroy old observer and create new observer
/* istanbul ignore next */
modifiers = clone(modifiers)
/* istanbul ignore next */
if (
value !== oldValue ||
!el[OBSERVER_PROP_NAME] ||
!looseEqual(modifiers, el[OBSERVER_PROP_NAME]._prevModifiers)
el &&
(value !== oldValue ||
!el[OBSERVER_PROP_NAME] ||
!looseEqual(modifiers, el[OBSERVER_PROP_NAME]._prevModifiers))
) {
// Re-bind on element
bind(el, { value, modifiers }, vnode)
Expand Down
3 changes: 2 additions & 1 deletion src/utils/copy-props.js
@@ -1,5 +1,6 @@
import identity from './identity'
import { isArray, isObject } from './inspect'
import { clone } from './object'

/**
* Copies props from one array/object to a new array/object. Prop values
Expand All @@ -22,7 +23,7 @@ const copyProps = (props, transformFn = identity) => {
if (props.hasOwnProperty(prop)) {
// If the prop value is an object, do a shallow clone to prevent
// potential mutations to the original object.
copied[transformFn(prop)] = isObject(props[prop]) ? { ...props[prop] } : props[prop]
copied[transformFn(prop)] = isObject(props[prop]) ? clone(props[prop]) : props[prop]
}
}

Expand Down
15 changes: 14 additions & 1 deletion src/utils/object.js
Expand Up @@ -36,12 +36,25 @@ export const isObject = obj => obj !== null && typeof obj === 'object'
*/
export const isPlainObject = obj => Object.prototype.toString.call(obj) === '[object Object]'

// @link https://gist.github.com/bisubus/2da8af7e801ffd813fab7ac221aa7afc
/**
* Shallow copy an object. If the passed in object
* is null or undefined, returns an empty object
*/
export const clone = obj => ({ ...obj })

/**
* Return a shallow copy of object with
* the specified properties omitted
* @link https://gist.github.com/bisubus/2da8af7e801ffd813fab7ac221aa7afc
*/
export const omit = (obj, props) =>
keys(obj)
.filter(key => props.indexOf(key) === -1)
.reduce((result, key) => ({ ...result, [key]: obj[key] }), {})

/**
* Convenience method to create a read-only descriptor
*/
export const readonlyDescriptor = () => ({ enumerable: true, configurable: false, writable: false })

/**
Expand Down

0 comments on commit 5f3ba9e

Please sign in to comment.