Neverending Package Mountain
Miss any of our Open RFC calls?Watch the recordings here! »

react-dynamic-content

0.0.14 • Public • Published

React Dynamic Content

Live Demo

Live demo: react-dynamic-content-31776.bitballoon.com

demo gif

React dynamic content is a component to organize custom html content using layouts and reposition by dragging.

Features of react-dynamic-content

  • Content organization using "cascading" or "google images" layouts that resize elements to fit columns and rows
  • Supports any kind of content, not just images (can be text, video, complex elements)
  • Customization of layout method (you can provide your own layout positioning method)
  • Changing order of elements by moving them with mouse/touch events, and receiving callback with new setting
  • Customization of drag initiation (for example if you want to start drag with swipe or long click)
  • Mobile friendly

NOTE #1: it is recomended that you provide responsive content, and if you plan using "google images" layout then please provide content that maintains aspect ratio within inner width/height (client width/height) by changing width or height (both not necesary), for example plain <img>, you can still provide any margins/borders you like regardless of aspect ratio.

NOTE #2: the component does not include scroller, only renders content, you can wrap it into scroller.

Getting Started

npm install --save react-dynamic-content

Style import

import "react-dynamic-content/styles/style.css";    //for css
import "react-dynamic-content/styles/style.scss";   //for scss
import "react-dynamic-content/styles/style.less";   //for less

Example

import DynamicContent from 'react-dynamic-content';
import "react-dynamic-content/styles/style.css"; 
 
class MyComponent extends React.Component {
  render() {
    const content = [
      <img src="http://lorempixel.com/1000/600" />,
      
      <img src="http://lorempixel.com/500/550" />,
      
      <div style={{background:'white', fontSize:'22px', padding:'10px', 
      border:'2px solid grey', borderRadius:'15px'}}>
        Lorem ipsum dolor sit amet, melius consequat mea te. His dicat suscipit sadipscing an.
        Probo saepe eu vix. Nam cu clita deserunt.
        Cum et choro solet quodsi. Unum temporibus sit id. Eam fierent conclusionemque cu,
        ei euismod moderatius interpretaris nec, te movet nullam tincidunt vis.
      </div>,
      
      <img src="http://lorempixel.com/1000/1200" />,
      
      <iframe src={"https://www.youtube.com/embed/vO2Su3erRIA"}></iframe>,
      
      <img src="http://lorempixel.com/600/500" />
    ]
    return (
      <DynamicContent
        elements={content}
        layout={'cascading'}
        numOfColumns={3}
        allowDraggingMobile={true}
        allowDraggingDesktop={true}/>
    );
  }
}

Options

Property Type Default mandatory Description
elements array null yes input of elements to display, must be array of react elements
layout string null yes name of layout method:
"cascading" for cascading,
"images" for google images,
"custom" to provide your own layout method with "customLayoutMethod"
see More on "cascading" and "images" layouts
customLayoutMethod bool null only if layout="custom" custom layout method when layout = "custom"
see Providing custom layout method
numOfColumns number null only if layout="cascading"
AND columnWidth absent
num of columns for "cascading" layout
columnWidth number null only if layout = "cascading"
AND numOfColumns absent
column width for "cascading" layout
maxHeight number null only if layout="images" max height of row for "images" layout
horizontalCellSpacing number 0 no horizontal spacing between elements
verticalCellSpacing number 0 no vertical spacing between elements
onChange function null no this method is called with new order setting of "elements" array once some element is reordered after being moved with drag.
see Reorder elements by dragging
confirmElementDrag function starts drags
after mousedown
/ touchstart
no method to provide confirmation for drag to customize drag start
see Providing custom drag initiator
allowDraggingMobile bool false no ability to drag elements in mobile
allowDraggingDesktop bool false no ability to drag elements in desktop

More on "cascading" and "images" layouts

Both layouts organize by ordering elements by their index in "elements" array,

Cascading layout: organizes elements like pinterest or tumblr do (columns with cascading images).
Element with higher index will be located lower than element with lower index, may be in any column.
It is recommended to provide responsive elements for this layout because the component resizes width to fit the columns.

Images layout: organizes like google images, (rows with elements with same height).
For elements in the same row, element with higher index will be on the right side from elements with lower, for elements in different rows, the element with higher index will be in lower rows.
It is recommended to provide responsive elements that maintain aspect ratio between clientWidth/clientHeight (regardless of border/margin) because the library uses the ratio to fit elements into rows without leaving trailing edges, meaning that width change should automatically change height and maintain ratio, or opposite, the component will try resizing both ways (by width or height) and force initial ratio if both ways fail.
see here how to make html elements that maintain raio (besides using <img/>'s).

Reorder elements by dragging

Component supports reposition element by dragging (mousedown/touchstart and then move as default), make sure to enable by setting allowDraggingDesktop={'true'} or allowDraggingMobile={'true'}.
To move element to a new place after you started dragging, you must move the cursor over some other element while dragging, and release mouse/tap (trigger mouseup/touchend), then your dragged element will be pushed before the other elment in "elements" array, and "onChange" will be called with new array setting.
After this, component will be rerendered anew with current layout method.

Providing custom layout method

If you want to provide your own layout to organizate, you should provide implementation of confirmElementDrag:

  /**
    arguments:
      elements: object of keys and values where: 
        key = index of element from 'elements' array,
        value = mounted element (that is rendered) for element at this index
      props: relevant props passed to component to assist you with organizing:
        maxHeight, numOfColumns, columnWidth, verticalCellSpacing, horizontalCellSpacing
  */
  customLayoutMethod (elements, props){
    //you should provide css styles top, left, width or height or both
    //for each each element of each value of elements obj
    //you can see example implementations for `cascading` and `images` layouts in `/src/utils.js`
    ...
  }

your job here is to set css styles left, top, width, height (can specify only width or height if element is responsive), to each element from values of "elements" obj, remember that component sets position: absolute; to all of its elements.

Providing custom drag initiator

If you want your own way to start dragging elements (long click, swipe, double click, drag by handle element ...)
you should provide implementation of confirmElementDrag:

  /**
    arguments:
      event: event fired on element, can be mousedown, mousemove, mouseup, and same with touchevents
      index: index of clicked element from 'elements' array
    output:
      true or NOT true (false/null/undefined), or deferred promise that yields the same.
  */
  confirmElementDrag (event, index){
    ...
    return result;
  }

to understand how to implement it to serve your needs, consider stream (pipe) of events (like RxJS stream) fired on your elements, then consider a new stream of results of confirmElementDrag(event,i) for each event from previous stream, such that if confirmElementDrag() returned a promise, then the result will also be deferred, meaning if you have 2 events: A and event B that fires immediately after A, if confirmElementDrag(A, i) returns a promise that yields after 100ms and B returns immediate result, then confirmElementDrag(A, i) will be yielded 100ms after B in second stream.
element dragging will start in either of these 2 cases:

  • confirmElementDrag(E, i) returns true immediately (does not return promise)
  • confirmElementDrag(E, i) returns deferred promise that returns true, and there was no event E2 that was fired after E whose confirmElementDrag(E2) yielded false, and it yielded before confirmElementDrag() of E.
    meaning false result for next events will cancel out the result of for previous events when their confirmElementDrag() results are yielded before the results of previous event

NOTE: once dragging has started, its can be cancelled only with mouseup/touchend, regardless of confirmElementDrag implementation

lets see some examples:

to start dragging after mousedown/touchstart (which is also the default setting):

  //returns true if mosuedown or touchstart and starts dragging immediately
  const mousedown = (e, index) => e.type === "mousedown" || e.type === "touchstart";
   <DynamicContent
      ...
      confirmElementDrag={mousedown}
      ...
      ></DynamicContent>

to start dragging after long press (600ms press):

  const longPress = (e, index) => {
    if(e.type === "mousedown" || e.type === "touchstart"){
      return new Promise((resolve, reject) => {
        setTimeout(()=>resolve(true), 600)
      });
    }
    //same as returning false here, will cancel out the previous promise and prevent drag
    //if press was not long enough (600ms)
  };
   <DynamicContent
      ...
      confirmElementDrag={longPress}
      ...
      ></DynamicContent>

to start dragging after swipe (400ms continuous mousemove after mousedown):

  let isDown = false; //is pressing (whether moving or not)
  const swipe = (e, index) =>{
    if(e.type === "mousedown" || e.type === "touchstart"){
      isDown=true;
    }
    else if(isDown && (e.type === "mousemove" || e.type === "touchmove")){
      return new Promise((resolve, reject) => {
        setTimeout(()=>resolve(true), 600)
      });
    }
    //if mousedown/touchend then disable promise generated from mousemove (if was)
    else{
      isDown = false;
    }
  };
   <DynamicContent
      ...
      confirmElementDrag={swipe}
      ...
      ></DynamicContent>

to start dragging after click and then mousedown, with less than 400ms interval:

  let lastClickTime=0;
  const clickAndMousedown = (e, index) => {
    //first mosuedown/touchstart time will be remembered
    //second will trigger drag if interval less than 400ms
    if(e.type === "mousedown" || e.type === "touchstart"){
      if(new Date() - lastClickTime < 400){ return true; }
      lastClickTime = new Date();
    }
  };
   <DynamicContent
      ...
      confirmElementDrag={clickAndMousedown}
      ...
      ></DynamicContent>

you can use e.target to detect sub-element in case you want to drag by handle inside your elements

Install

npm i react-dynamic-content

DownloadsWeekly Downloads

5

Version

0.0.14

License

none

Last publish

Collaborators

  • avatar