From 784604d7431074b25fac6b0ad5a2c678d67c1775 Mon Sep 17 00:00:00 2001 From: Joshua Comeau Date: Sun, 26 Nov 2017 10:03:26 -0500 Subject: [PATCH] v2.10.0 - Wrapperless mode tweaks (#204) * Tweaks to new Wrapperless mode Some small tweaks to @tobilen's excellent work creating a wrapperless mode for React Flip Move with React 16. - Removed the anchor, as this will be computed based on the parent - Removed the warning for not supplying an anchor - Update README - Add details to API reference - Fix bug with the first transition being buggy, by setting the parentData on mount - Extract findDOMContainer method - Switch from false to null for typeName - Update flow-typed defs - Update package.json version number --- .vscode/settings.json | 2 +- README.md | 111 ++++++++++++------ documentation/api_reference.md | 8 +- ...x.js => react-flip-move_v2.9.x-v2.10.x.js} | 37 +++--- package.json | 4 +- src/FlipMove.js | 49 +++----- src/error-messages.js | 20 +--- stories/helpers/FlipMoveWrapper.js | 5 - stories/primary.stories.js | 20 +--- test/index.spec.js | 4 +- yarn.lock | 2 +- 11 files changed, 131 insertions(+), 131 deletions(-) rename flow-typed/{react-flip-move_v2.9.x.js => react-flip-move_v2.9.x-v2.10.x.js} (73%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 282a929..37b0b78 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,4 +2,4 @@ { "javascript.validate.enable": false, "eslint.enable": true -} \ No newline at end of file +} diff --git a/README.md b/README.md index 5230079..8ced8c1 100644 --- a/README.md +++ b/README.md @@ -24,21 +24,6 @@ Flip Move uses the [_FLIP technique_](https://aerotwist.com/blog/flip-your-anima * __Scrabble__ * __Laboratory__ -## Table of Contents - -* [Installation](#installation) -* [Features](#features) -* [Quickstart](#quickstart) -* [Compatibility](#compatibility) -* [Enter/Leave Animations](https://github.com/joshwcomeau/react-flip-move/blob/master/documentation/enter_leave_animations.md) -* [API Reference](https://github.com/joshwcomeau/react-flip-move/blob/master/documentation/api_reference.md) -* [Gotchas](#gotchas) -* [Known Issues](#known-issues) -* [Contributions](#contributions) -* [Development](#development) -* [Flow support](#flow-support) -* [License](#license) - ## Installation @@ -114,45 +99,95 @@ Curious how this works, under the hood? [__Read the Medium post__](https://mediu --- -### HTML Attributes +### Wrapping Element -FlipMove creates its own DOM node to wrap the children it needs to animate. Sometimes, you'll want to be able to pass specific HTML attributes to this node. +By default, FlipMove wraps the children you pass it in a `
`: -All props other than the ones listed above will be delegated to this new node, so you can apply them directly to FlipMove. For example: +```jsx +// JSX + +
Hello
+
World
+
-```html +// HTML
- -
  • Column 1
  • -
  • Column 2
  • -
    +
    Hello
    +
    World
    ``` -FlipMove passes the `className` and `style` props along to the `ul` that needs to be created. Here's how it renders: +Any unrecognized props to `` will be delegated to this wrapper element: -```html -
    -
      -
    • Column 1
    • -
    • Column 2
    • -
    +```jsx +// JSX + +
    Hello
    +
    World
    +
    + +// HTML +
    +
    Hello
    +
    World
    ``` -This works for all HTML props - there's no validation. +You can supply a different element type with the `typeName` prop: -You can pass `false` to `typeName` and a valid HTMLElement reference as `anchor` to render the children without a wrapping DOM node. Note that you will have to use react 16.0.0 or higher, and that all other props (such as `className` or `style`) will have no effect. -```html -
    this.myCustomParent = node}> - -
  • Column 1
  • -
  • Column 2
  • +```jsx +// JSX + +
  • Hello
  • +
  • World
  • +
    + +// HTML +
      +
    • Hello
    • +
    • World
    • +
    +``` + +Finally, if you're using React 16 or higher, and Flip Move 2.10 or higher, you can use the new "wrapperless" mode. This takes advantage of a React Fiber feature, which allows us to omit this wrapping element: + +```jsx +// JSX +
    + +
    Hello
    +
    World
    + +// HTML +
    +
    Hello
    +
    World
    +
    ``` -If no anchor is provided, react-flip-move will try to use the components parent node. This, however, is a costly operation that violates component separation principles and thus is [highly discouraged](https://reactjs.org/docs/react-dom.html#finddomnode). +Wrapperless mode is nice, because it makes FlipMove more "invisible", and makes it easier to integrate with parent-child CSS properties like flexbox. However, there are some things to note: + +- This is a new feature in FlipMove, and isn't as battle-tested as the traditional method. Please test thoroughly before using in production, and report any bugs! +- FlipMove does some positioning magic for enter/exit animations - specifically, it temporarily applies `position: absolute` to its children. For this to work correctly, you'll need to make sure that `` is within a container that has a non-static position (eg. `position: relative`), and no padding: + +```jsx +// BAD - this will cause children to jump to a new position before exiting: +
    + +
    Hello world
    +
    +
    + +// GOOD - a non-static position and a tight-fitting wrapper means children will +// stay in place while exiting: +
    + +
    Hello world
    +
    +
    +``` --- diff --git a/documentation/api_reference.md b/documentation/api_reference.md index bee2ca6..376580c 100644 --- a/documentation/api_reference.md +++ b/documentation/api_reference.md @@ -277,15 +277,17 @@ In general, it is advisable to ignore the `domNodes` argument and work with the ### `typeName` -| **Accepted Types:** | **Default Value** | -|---------------------|-------------------| -| `String` | 'div' | +| **Accepted Types:** | **Default Value** | +|----------------------|-------------------| +| `String` | `null` | 'div' | Flip Move wraps your children in a container element. By default, this element is a `div`, but you may wish to provide a custom HTML element (for example, if your children are list items, you may wish to set this to `ul`). Any valid HTML element type is accepted, but peculiar things may happen if you use an unconventional element. +With React 16, Flip Move can opt not to use a container element: set `typeName` to `null` to use this new "wrapperless" behaviour. [Read more](https://github.com/joshwcomeau/react-flip-move/blob/master/README.md#wrapping-elements). + --- ### `disableAllAnimations` diff --git a/flow-typed/react-flip-move_v2.9.x.js b/flow-typed/react-flip-move_v2.9.x-v2.10.x.js similarity index 73% rename from flow-typed/react-flip-move_v2.9.x.js rename to flow-typed/react-flip-move_v2.9.x-v2.10.x.js index 30ae006..b102c33 100644 --- a/flow-typed/react-flip-move_v2.9.x.js +++ b/flow-typed/react-flip-move_v2.9.x-v2.10.x.js @@ -1,15 +1,15 @@ -declare module "react-flip-move" { +declare module 'react-flip-move' { declare export type Styles = { - [key: string]: string + [key: string]: string, }; declare type ReactStyles = { - [key: string]: string | number + [key: string]: string | number, }; declare export type Animation = { from: Styles, - to: Styles + to: Styles, }; declare export type Presets = { @@ -17,7 +17,7 @@ declare module "react-flip-move" { fade: Animation, accordionVertical: Animation, accordionHorizontal: Animation, - none: null + none: null, }; declare export type AnimationProp = $Keys | boolean | Animation; @@ -28,27 +28,27 @@ declare module "react-flip-move" { bottom: number, left: number, height: number, - width: number + width: number, }; // can't use $Shape> here, because we use it in intersection declare export type ElementShape = { - +type: $PropertyType, "type">, - +props: $PropertyType, "props">, - +key: $PropertyType, "key">, - +ref: $PropertyType, "ref"> + +type: $PropertyType, 'type'>, + +props: $PropertyType, 'props'>, + +key: $PropertyType, 'key'>, + +ref: $PropertyType, 'ref'>, }; declare type ChildHook = (element: ElementShape, node: ?HTMLElement) => mixed; declare export type ChildrenHook = ( elements: Array, - nodes: Array + nodes: Array, ) => mixed; declare export type GetPosition = (node: HTMLElement) => ClientRect; - declare export type VerticalAlignment = "top" | "bottom"; + declare export type VerticalAlignment = 'top' | 'bottom'; declare export type Child = void | null | boolean | React$Element<*>; @@ -61,7 +61,7 @@ declare module "react-flip-move" { disableAllAnimations: boolean, getPosition: GetPosition, maintainContainerHeight: boolean, - verticalAlignment: VerticalAlignment + verticalAlignment: VerticalAlignment, }; declare type PolymorphicProps = { @@ -70,37 +70,36 @@ declare module "react-flip-move" { staggerDurationBy: string | number, staggerDelayBy: string | number, enterAnimation: AnimationProp, - leaveAnimation: AnimationProp + leaveAnimation: AnimationProp, }; declare type Hooks = { onStart?: ChildHook, onFinish?: ChildHook, onStartAll?: ChildrenHook, - onFinishAll?: ChildrenHook + onFinishAll?: ChildrenHook, }; declare export type DelegatedProps = { style?: ReactStyles, - anchor?: HTMLElement }; declare export type FlipMoveDefaultProps = BaseProps & PolymorphicProps; declare export type CommonProps = BaseProps & Hooks & { - children?: ChildrenArray + children?: ChildrenArray, }; declare export type FlipMoveProps = FlipMoveDefaultProps & CommonProps & DelegatedProps & { appearAnimation?: AnimationProp, - disableAnimations?: boolean // deprecated, use disableAllAnimations instead + disableAnimations?: boolean, // deprecated, use disableAllAnimations instead }; declare class FlipMove extends React$Component { - static defaultProps: FlipMoveDefaultProps + static defaultProps: FlipMoveDefaultProps; } declare export default typeof FlipMove diff --git a/package.json b/package.json index 95e3816..3e6f254 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flip-move", - "version": "2.9.17", + "version": "2.10.0", "description": "Effortless animation between DOM changes (eg. list reordering) using the FLIP technique.", "main": "lib/index.js", "typings": "typings/react-flip-move.d.ts", @@ -95,7 +95,7 @@ "mocha": "3.5.3", "preact": "8.1.0", "preact-compat": "3.16.0", - "prettier": "^1.6.1", + "prettier": "1.8.2", "prop-types": "15.5.8", "react": "16.0.0", "react-dom": "16.0.0", diff --git a/src/FlipMove.js b/src/FlipMove.js index 64076ad..c88c2fc 100644 --- a/src/FlipMove.js +++ b/src/FlipMove.js @@ -114,10 +114,14 @@ class FlipMove extends Component { remainingAnimations = 0; childrenToAnimate: Array = []; - // Make sure we dont spam warnings in the console - wasWarned = false; - componentDidMount() { + // Because React 16 no longer requires wrapping elements, Flip Move can opt + // to not wrap the children in an element. In that case, find the parent + // element using `findDOMNode`. + if (this.props.typeName === null) { + this.findDOMContainer(); + } + // Run our `appearAnimation` if it was requested, right after the // component mounts. const shouldTriggerFLIP = @@ -155,23 +159,8 @@ class FlipMove extends Component { } componentDidUpdate(previousProps: ConvertedProps) { - // If wrapperless mode was activated, we need to make sure we still have a - // valid parentNode to properly animate. - // If no anchor was provided we fall back to using findDomNode. - if (!this.props.typeName) { - if (!this.props.delegated.anchor) { - // Render warning if wrapperless mode is activated but no anchor has - // been provided - this.logAnchorWarning(); - - this.parentData.domNode = - /* eslint-disable react/no-find-dom-node */ - // $FlowFixMe we because we now parentNode has to be HTMLElement - ReactDOM.findDOMNode(this) && ReactDOM.findDOMNode(this).parentNode; - /* eslint-enable react/no-find-dom-node */ - } else { - this.parentData.domNode = this.props.delegated.anchor; - } + if (this.props.typeName === null) { + this.findDOMContainer(); } // If the children have been re-arranged, moved, or added/removed, // trigger the main FLIP animation. @@ -197,17 +186,17 @@ class FlipMove extends Component { } } - logAnchorWarning = () => { - if (!this.wasWarned) { - console.warn(` - >> Error, via react-flip-move << - - Wrapperless mode was activated but no anchor has been provided to react-flip-move. - - Please use either the 'typeName' prop to pass a wrapper type (such as 'ul') or make sure a valid HTMLElement is passed in 'anchor' - `); - this.wasWarned = true; + findDOMContainer = () => { + // eslint-disable-next-line react/no-find-dom-node + const domNode = ReactDOM.findDOMNode(this); + const parentNode = domNode && domNode.parentNode; + + // This ought to be impossible, but handling it for Flow's sake. + if (!parentNode || !(parentNode instanceof HTMLElement)) { + return; } + + this.parentData.domNode = parentNode; }; runAnimation = () => { diff --git a/src/error-messages.js b/src/error-messages.js index dcd04e2..1168587 100644 --- a/src/error-messages.js +++ b/src/error-messages.js @@ -32,18 +32,13 @@ export const invalidTypeForTimingProp = (args: { value: string | number, defaultValue: number, }) => + // prettier-ignore console.error(` >> Error, via react-flip-move << -The prop you provided for '${ - args.prop - }' is invalid. It needs to be a positive integer, or a string that can be resolved to a number. The value you provided is '${ - args.value - }'. +The prop you provided for '${args.prop}' is invalid. It needs to be a positive integer, or a string that can be resolved to a number. The value you provided is '${args.value}'. -As a result, the default value for this parameter will be used, which is '${ - args.defaultValue - }'. +As a result, the default value for this parameter will be used, which is '${args.defaultValue}'. `); export const deprecatedDisableAnimations = warnOnce(` @@ -59,14 +54,11 @@ export const invalidEnterLeavePreset = (args: { acceptableValues: string, defaultValue: $Keys, }) => + // prettier-ignore console.error(` >> Error, via react-flip-move << -The enter/leave preset you provided is invalid. We don't currently have a '${ - args.value - } preset.' +The enter/leave preset you provided is invalid. We don't currently have a '${args.value} preset.' -Acceptable values are ${args.acceptableValues}. The default value of '${ - args.defaultValue - }' will be used. +Acceptable values are ${args.acceptableValues}. The default value of '${args.defaultValue}' will be used. `); diff --git a/stories/helpers/FlipMoveWrapper.js b/stories/helpers/FlipMoveWrapper.js index e774a83..8111592 100644 --- a/stories/helpers/FlipMoveWrapper.js +++ b/stories/helpers/FlipMoveWrapper.js @@ -167,7 +167,6 @@ class FlipMoveWrapper extends Component { ...baseStyles.bodyContainerStyles, ...this.props.bodyContainerStyles, }} - ref={node => (this.anchor = node)} > {this.renderItems()}
    @@ -212,7 +209,6 @@ FlipMoveWrapper.propTypes = { PropTypes.string, // for DOM types like 'div' PropTypes.func, // for composite components ]), - typeName: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), bodyContainerStyles: PropTypes.object, flipMoveContainerStyles: PropTypes.object, listItemStyles: PropTypes.object, @@ -251,7 +247,6 @@ FlipMoveWrapper.defaultProps = { }, ], itemType: 'div', - typeName: 'div', }; export default FlipMoveWrapper; diff --git a/stories/primary.stories.js b/stories/primary.stories.js index ec15570..da966a2 100644 --- a/stories/primary.stories.js +++ b/stories/primary.stories.js @@ -25,9 +25,6 @@ storiesOf('Basic Behaviour', module) itemType={FlipMoveListItem} flipMoveProps={{ delay: 500 }} /> - )) - .add('without wrapper (react 16+)', () => ( - )); const easings = ['linear', 'ease-in', 'ease-out', 'cubic-bezier(1,0,0,1)']; @@ -110,18 +107,11 @@ storiesOf('Disabled animations', module) storiesOf('Type names', module) .add('ul/li', () => ( - + )) .add('ol/li', () => ( - + + )) + .add('null', () => ( + )); diff --git a/test/index.spec.js b/test/index.spec.js index 9a575fa..baecd85 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -85,7 +85,6 @@ describe('FlipMove', () => { }; count = 0; - customAnchor = null; onFinishHandler = () => { this.count += 1; @@ -111,7 +110,7 @@ describe('FlipMove', () => { render() { return ( -
      (this.customAnchor = node)}> +
        { onFinish={this.onFinishHandler} onFinishAll={finishAllStub} typeName={this.props.withoutWrapper ? false : 'div'} - anchor={this.props.withoutAnchor ? undefined : this.customAnchor} > {this.renderArticles()} diff --git a/yarn.lock b/yarn.lock index 3111b01..a4d0c93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5703,7 +5703,7 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" -prettier@^1.6.1: +prettier@1.8.2: version "1.8.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.8.2.tgz#bff83e7fd573933c607875e5ba3abbdffb96aeb8"