Morphdom only accepts a single config object when updating the DOM. This makes it difficult to encapsulate a morphdom config specific to a single feature. To solve this problem, we created the @retailmenot/morphdom-plugins
module, which can stitch together multiple morphdom configurations into a single morphdom configuration.
There are a few pieces of functionality we've isolated as it relates to morphdom, and we've encapsulated them into different plugins which we combine using applyMorphdomPlugins
.
{{TOC}}
View the live demo here.
Alternately, you can build and run the demo using express locally:
cd example
npm install
npm run start
And visit http://www.localhost:3000.
Developers, if you make changes to the demo that you want reflected on GitHub, run ./build-static-demo.sh
, commit the changes, and push them to the master branch.
applyMorphdomPlugins
lets you stitch multiple morphdom configs together into a single one. Think of this method as the analog to redux's applyMiddleware
.
applyMorphdomPlugins
wraps each method in a morphdom config with another method that will invoke a plugin's code when appropriate.
For morphdom methods that return false
, the code for plugins that follow later in the plugins list will not be executed. This is analogous to swallowing an action in a piece of redux middleware, or choosing not to call next()
in express middleware.
Note: Where morphdom allows you to return either a node or false
, applyMorphdomPlugins
will enforce that you return either true
, false
, or a dom node. The extra strictness is required so that the middleware can determine if it should run the next plugin.
var morphdomPlugins = require('@retailmenot/morphdom-plugins');
var app = document.getElementById( '#app' );
var html = '<div id="app" class="app"> ... </div>';
var plugins = [
morphdomPlugins.inputPersistence.plugin(),
morphdomPlugins.cssTransitionGroup.plugin(),
morphdomPlugins.transitionElementMovement.plugin(),
morphdomPlugins.transitionHeight.plugin(),
morphdomPlugins.inputPersistence.plugin()
];
morphdom( app, html, morphdomPlugins.applyMorphdomPlugins( plugins, {
// morphdom config
} ) );
Plugins are functions that return morphdom configurations. Each plugin/configuration encapsulates a set of functionality related to re-rendering with morphdom. The existing plugins are described below.
cssTransitionGroup
enables you to easily transition DOM elements when they are being added or deleted from your DOM tree. This plugin was inspired by React's CSSTransitionGroup
. You should read those docs to become familiar with the paradigm.
The primary difference is that the transition-enter-timeout
and transition-leave-timeout
options have been renamed to transition-enter-duration
and transition-leave-duration
.
It also does not support CSSTransitionGroup
's concept of transitionAppear
.
Also, this plugin introduces the concept of transition-enter-delay
and transition-leave-delay
options that delay the start of the corresponding transitions. Their values are optional, and are used to delay the application of the -active
class for the animation.
<div
class="my-alert"
data-transition-name="example"
data-transition-enter-duration="2000"
data-transition-leave-delay="500"
data-transition-leave-duration="500"
>
...
</div>
.my-alert.example-enter {
transition: opacity 500ms ease-out;
opacity: 0;
}
.my-alert.example-enter-active {
opacity: 1;
}
.my-alert.example-leave {
opacity: 1;
transition: opacity 500ms ease-out;
}
.my-alert.example-leave-active {
opacity: 0;
}
You can disable just the entry or the leave animations as follows:
<div
class="my-alert"
data-transition-name="example"
data-transition-enter="false"
data-transition-leave="false"
>
...
</div>
Disable both by omitting the data-transition-name
attribute.
transitionElementMovement
enables you to transition an element that has moved from one position on the page to another, or changed height or width across calls to morphdom. Its implementation was heavily influenced by react-flip-move.
<div class="my-element"
data-transition-motion="true"
data-transition-motion-duration="200"
data-transition-motion-class="transition-motion"
> ... </div>
.my-element.transition-motion {
transition: transform 500ms ease;
}
That's it. The plugin will take care of the rest.
Note: data-transition-motion-class
is optional. If not provided, it defaults to transition-motion
. This class will be present on the node when its position should be transitioned. You should add a transition rule for the transform property when the data-transition-motion-class
class is present on your node.
morphdom-plugins
includes a sass mixin to help you easily generate compatible CSS rules for dom nodes relying on the transitionElementMovement
plugin.
@import 'node_modules/morphdom-plugins/sass-mixins/transition-element-movement/index'
.transition-movement {
@include transitionElementMovement(
"transition-motion",
500ms ease
);
}
.transition-movement.transition-motion {
transition: transform 500ms ease,
height 500ms ease,
width 500ms ease;
}
transitionHeight
enables you to transition the expanding or collapsing of an element. It takes care of setting the max-height
on the element to match the height of its content.
This plugin is only necessary because browsers can't transition from a max-height: 0
to a max-height: auto
, which is what you need to do if the content being revealed is of variable height.
<div
class="my-element{{#if isCollapsed}} collapsed{{/if}}"
data-transition-height="true"
data-transition-height-collapsed-class="collapsed"
>
<div data-transition-height-target="true">
...
</div>
</div>
.my-element {
transition: max-height 250ms ease-out;
overflow: hidden;
}
.my-element.collapsed {
max-height: 0;
}
Note: data-transition-height-collapsed-class
is optional. If omitted, its value will default to is-collapsed
. The presence or absence of this class will be used to determine when to update the height of the element.
Note: transitionHeight
will throw if an element with data-transition-height="true"
does not have a single child that has the data-transition-height-target="true"
attribute.
The approach for transitioning newly added nodes is a little different, so opting into this functionality is handled differently.
<div
class="my-element"
data-transition-height-on-added="true"
data-transition-height-on-added-delay="1500"
>
<div data-transition-height-target="true">
...
</div>
</div>
.my-element {
transition: max-height 250ms ease-out;
overflow: hidden;
max-height: 0;
}
This component also exports a resizeHandler
to update the heights of elements on your page when the page size changes.
npm install --save-dev lodash.throttle
var throttle = require('lodash.throttle');
var morphdomPlugins = require('@retailmenot/morphdom-plugins');
throttle(morphdomPlugins.morphdomTransitionHeight.resizeHandler, 150);
morphdom-plugins
includes a sass mixin to help you easily generate compatible CSS rules for dom nodes relying on the transitionHeight
plugin.
@import 'node_modules/morphdom-plugins/sass-mixins/transition-height/index'
.transition-on-added {
@include maxHeightOnAddedTransitions(400ms ease);
}
.transition-on-added.always-show {
// reset the node to not transition its height when added
// if it contains the always-show class
@include resetMaxHeightOnAddedTransitions();
}
.transition-height {
@include maxHeightTransitions(400ms ease, "is-collapsed");
}
.transition-on-added {
transition: max-height 400ms ease;
overflow: hidden;
max-height: 0;
}
.transition-on-added.always-show {
max-height: initial !important;
}
.transition-height {
transition: max-height 400ms ease;
overflow: hidden;
}
.transition-height.is-collapsed {
max-height: 0 !important;
}
Sometimes, there are attributes you'd like to persist across calls to morphdom that are not controlled by your templates. This comes up a lot when you are using JS to add css rules or classes as the user interacts with your page. Purely interaction attributes should not need to go through your render call. attrPersistence
lets you persist certain attributes on elements across calls to morphdom.
<div
class="my-element"
data-persist-class="is-sticky,is-collapsed"
data-persist-css="height"
>
...
</div>
Now, whenever my-element
is re-rendered and updated through morphdom, the state of the is-sticky
, is-collapsed
classes or any height-related css properties will be persisted across the morphdom call.
inputPersistence
will help you ensure that the value of input[type=text],input[type=number]
fields are not overwritten if the input currently has focus. You should take care not to overwrite the value of a field if it has focus, but if, for whatever reason a morphdom call causes the value of an input to update while it has focus, this plugin will prevent it from updating and disorienting the user.
Without this plugin, you run the risk of overwriting the value of input[type=text],input[type=number]
fields with stale values from your redux state.
The redux philosophy is that that data flows in a single direction. That is, from the state, through the templates, into html, and finally into the DOM. A user typing into a text field is counter to this philosophy because now data needs to flow from the DOM back into the redux state. Typically, the user input is reduced into the redux state by event listener on the input field. If you are throttling the event that updates your state with the new value (you should be!), there exists a small window of opportunity–after the user types into an input, but before your state is updated with the new value–where some other update to the state could trigger a re-render that incorrectly reverts the value of the text input before the new (user input) value is reflected in your state. If this happens, the user would see the text they typed vanish and revert to some former value. This is no good.
Don't update a text field if it has focus, because the user is interacting with it. This plugin does that for you, easy as that.
Note: if your state that drives the input changes due to something other than user-interaction, and the field is in focus, this plugin will prevent the field from updating, even if doing so is desired. It is likely that proper UX in such a scenario would be to prompt the user to update the stale page state by explicitly clicking a refresh button.
- remove jQuery dependency
- enable element to
transition-height-on-added
andtransition-height
at the same time–current behavior in this case is undefined - make
css-transition-group
work more reliably if animation is reversed mid-way through