Skip to content

Instantly share code, notes, and snippets.

@stubbornella
Created December 18, 2014 15:29
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save stubbornella/e97662e4a197eb9a534a to your computer and use it in GitHub Desktop.
Save stubbornella/e97662e4a197eb9a534a to your computer and use it in GitHub Desktop.
Media Object from OOCSS and now Bootstrap written in React/jsx
'use strict';
var React = require('react/addons');
var _ = require('lodash');
var setClass = React.addons.classSet;
var MediaObject = React.createClass({
render: function () {
var classes = setClass({
'media-left': this.props.horizontalAlignment === 'left',
'media-right': this.props.horizontalAlignment === 'right',
'media-middle': this.props.verticalAlignment === 'middle',
'media-bottom': this.props.verticalAlignment === 'bottom',
'media-object': !this.props.imageHref
});
var paddingClasses = setClass({
'prs': this.props.leftMediaSpacing === 'small',
'prm': this.props.leftMediaSpacing === 'medium',
'prl': this.props.leftMediaSpacing === 'large',
'prxl': this.props.leftMediaSpacing === 'xlarge',
'pls': this.props.rightMediaSpacing === 'small',
'plm': this.props.rightMediaSpacing === 'medium',
'pll': this.props.rightMediaSpacing === 'large',
'plxl': this.props.rightMediaSpacing === 'xlarge'
});
var mediaClasses = [classes, paddingClasses].join(' ');
if (this.props.imageHref) {
return (
<a className={mediaClasses} href={this.props.imageHref} >
<img alt={this.props.alt} className="media-object" src={this.props.imageSource} height={this.props.height} width={this.props.width} />
</a>
);
} else {
return (
<img alt={this.props.alt} className={mediaClasses} src={this.props.imageSource} height={this.props.height} width={this.props.width} />
);
}
}
});
var Media = React.createClass({
render: function () {
var leftMedia,
rightMedia = '';
var classes = setClass({
'media': true,
'media-stackable-xs': this.props.stackSize === 'xsmall',
'media-stackable-sm': this.props.stackSize === 'small',
'media-stackable-md': this.props.stackSize === 'medium',
'media-stackable-lg': this.props.stackSize === 'large'
});
var bodyClasses = setClass({
'media-body': true,
'media-middle': this.props.bodyAlignment === 'middle',
'media-bottom': this.props.bodyAlignment === 'bottom'
});
if (this.props.leftImageSource) {
leftMedia = (
<MediaObject
horizontalAlignment='left'
verticalAlignment={this.props.leftImageAlignment}
imageHref={this.props.leftImageHref}
imageSource={this.props.leftImageSource}
leftMediaSpacing={this.props.leftMediaSpacing}
alt={this.props.leftAlt}
height={this.props.leftImageHeight}
width={this.props.leftImageWidth}>
</MediaObject>
);
}
if (this.props.rightImageSource) {
rightMedia = (
<MediaObject
horizontalAlignment='right'
verticalAlignment={this.props.rightImageAlignment}
imageHref={this.props.rightImageHref}
imageSource={this.props.rightImageSource}
rightMediaSpacing={this.props.rightMediaSpacing}
alt={this.props.rightAlt}
height={this.props.rightImageHeight}
width={this.props.rightImageWidth}>
</MediaObject>
);
}
return (
<div {...this.props} className={classes}>
{leftMedia}
<div className={bodyClasses}>
{this.props.children}
</div>
{rightMedia}
</div>
);
}
});
module.exports = {
Media: Media
};
@stubbornella
Copy link
Author

What is the media block?

We wrote a media block component (like you'll find in oocss or Bootstrap) in Reactjs. It's goal is to align an image with content that goes with it, like so:

http://philipwalton.github.io/solved-by-flexbox/images/media-object.jpg

And the details of what it can do css-wise:

http://getbootstrap.com/components/#media

Tons of params

Buuuuut, we hate the interface we made. You end up passing so many params to the media object because it sets all the properties of it's inner images. It will only get worse when it supports icons, svg, or video content. This is how you might invoke it if you had both left and right images:

<Media
      leftImageHref='http://www.google.com'
      leftImageSource='http://placehold.it/50x50'
      leftImageAlignment='middle'
      rightImageHref='http://www.google.com'
      rightImageSource='http://placehold.it/50x50'
      rightImageAlignment='middle'
      bodyAlignment='middle'
      stackSize='medium'>
        Media block content
</Media>

Yuck.

GIven the number of modifiers we have already though, I can only imagine composability would be better:

Image modifiers

Media Modifiers Options Description
<left or right>ImageHref url Url the image should link to
<left or right>ImageSource url Image source
<left or right>ImageAlignment top (default), "middle", "bottom" Vertical alignment
<left or right>MediaSpacing "small", "medium", "large", "xlarge" Spacing adjustments between the image and the content next to it
<left or right>ImageHeight pixel or percentage Height of the image
<left or right>ImageWidth pixel or percentage Width of the image
<left or right>ImageAlt string Text description of the image (for disabled users)

General media object modifiers

Media Modifiers Options Description
bodyAlignment top (default), "middle", "bottom" Vertical alignment of the body (used for large images with small content next to it, usually centered)
stackSize "xsmall", "small", "medium", "large" At what breakpoint should the media object stack

Would composability be better?

I suspect this is a case where we should be using the composability of react components, but I'm not sure, and I'd love some advice. Would it be better to do something like this?

<Media bodyAlignment='middle' stackSize='medium'>
  <img src="http://placehold.it/50x50" verticalAlignment="middle" href="http://google.com" horizontalAlignment="left" />
   Media block content
  <img src="http://placehold.it/50x50" verticalAlignment="middle" href="http://google.com" horizontalAlignment="right" />
</Media>

What feels weird about this, is that we need the children to be in a particular order (left, content, right). The object won't layout correctly if they aren't. Can you loop over children and process each one differently? We also need to wrap images that are links differently than those that aren't and manage class names differently between the two.

Passing in image data in json?

I guess alternatively we could pass in data:

<Media bodyAlignment='middle' stackSize='medium' images={images}>
   Media block content
</Media>

That feels kind of weird too. Is there a good pattern here? Any examples that seem relevant?

Simplifying

Finally, there are only two kinds of media blocks that really get used a lot:

  1. Images and text are all top aligned (the default)
  2. Images and text are all center aligned

Maybe we could simplify some of the params by making those two objects separately...

Thoughts? Suggestions? We'd love feedback.

Thanks!
Nicole, @bebepeng, @gpleiss, @pmeskers, @pivotal-plech

@svnlto
Copy link

svnlto commented Dec 18, 2014

I could give a pretty detailed answer of what I think you should do but to keep it short.

| Maybe we could simplify some of the params by making those two objects separately...

Would be a good start. Also, the 'json' way might look odd but I'd find it an appropriate use case.

@matthewwithanm
Copy link

👍 on using composition. You can definitely loop over children and process each differently (though children isn't always an array so you either need to convert it to one or use one of React's iteration tools). But in similar situations, I've had luck using props as "slots". For example, left, content, and right. Any positioning of these slots I try to do within the component itself, instead of relying on adding classes to children. In other words, the Media component would add <div>s for each slot and just plop the provided React elements into those. This ends up with more DOM elements, but it makes composition SUPER simple and the component much more flexible.

@iamdustan
Copy link

React.Children[iteratorFn] covers for this.props.children being an Array or a single object.

You could use the iterator to also do some validation that the children are passed in the correct order, or that you have the correct number. E.g. if only one child is passed in throw an error. If you get two MediaImages in a row, through an error. If you don’t have any non-MediaImages throw an error.

👍 to composition. I would aim for a consumer API that looks like the following:

var MediaConsumer = React.createClass({
  render() {
     return (
        <Media bodyAlignment='middle' stackSize='medium'>
          <MediaImage {...imageProps} href="optionalhref" />
          {content}
          <MediaImage {...imageProps} href="optionalhref" />
      </Media>
     );
  }
});

@iamdustan
Copy link

Which looking at this further, that’s basically what you’re doing. The composition is being hidden inside of the Media component. Exposing that I think would clear up both your external API and make the path to those common cases much easier.

@stubbornella
Copy link
Author

If it's helpful, we have our docs for this react spike here:

http://styleguide.cfapps.io/beta.html#media_react

@stubbornella
Copy link
Author

@IamDustin - yes, we didn't expose the composition, but I like where you went with it.

@iamdustan
Copy link

@stubbornella
Copy link
Author

@svnlto - do you know of a way to validate that child nodes are a particular type of component? e.g. Media should only contain MediaBody or a `MediaImage

We found a property called type.displayName... but we're not sure if we should use it this way. What do you think?

@svnlto
Copy link

svnlto commented Dec 19, 2014

That's a good question, I've only ever had to check for props etc. using propTypes. Checking the displayName property on a given component seems sensible tho.

@yungsters
Copy link

At Facebook, we have a component similar to the media block called "image block" (this may be familiar to you) that requires an image child and a content child. It is used like this:

<ImageBlock>{image}{content}</ImageBlock>

You can imagine easily extending this to:

<ImageBlock>{imageLeft}{content}{imageRight}</ImageBlock>

This gets slightly more complicated if either image is optional. For example, if someone wanted to leave off the left image, the code would be pretty gross:

<ImageBlock>{null}{content}{imageRight}</ImageBlock>

If optional left/right images were desired, I would recommend passing each image in as props instead. For example:

<ImageBlock
  imageLeft={imageLeft}
  imageRight={imageLeft}>
  {content}
</ImageBlock>

Although this last suggestion might feel weird, something to keep in mind is that in React, children are simply props. The only thing that really differentiates children is how it is represented in JSX and how the primitive DOM components treat them.

Hope this helps.

@stubbornella
Copy link
Author

Thanks @yungsters, of course I remember the ImageBlock, though it was written in xhp back when I was at facebook. ;)

@stubbornella
Copy link
Author

Here is where we've ended up...

  • We created a flag object to handle middle vertical alignment, it just passes through to media and sets the right property. Media handles top alignment by default.
  • We pass in the image as a param, which feels much cleaner
  • We were able to add some validations
'use strict';

var React = require('react/addons');
var _ = require('lodash');
var setClass = React.addons.classSet;

var MediaObject = React.createClass({
  render: function () {
    var classes = setClass({
      'media-left': this.props.horizontalAlignment === 'left',
      'media-right': this.props.horizontalAlignment === 'right',
      'media-middle': this.props.vAlign === 'middle',
      'media-bottom': this.props.vAlign === 'bottom'
    });

    var paddingClasses = setClass({
      'prs': this.props.leftMediaSpacing === 'small',
      'prm': this.props.leftMediaSpacing === 'medium',
      'prl': this.props.leftMediaSpacing === 'large',
      'prxl': this.props.leftMediaSpacing === 'xlarge',
      'pls': this.props.rightMediaSpacing === 'small',
      'plm': this.props.rightMediaSpacing === 'medium',
      'pll': this.props.rightMediaSpacing === 'large',
      'plxl': this.props.rightMediaSpacing === 'xlarge'
    });

    var mediaClasses = [classes, paddingClasses].join(' ');


    return (
      <div className={mediaClasses}>
        {this.props.children}
      </div>
    );
  }
});

var Media = React.createClass({
  propTypes: {
    stackSize: React.PropTypes.oneOf(["xsmall", "small", "medium", "large"]),
    vAlign: React.PropTypes.oneOf(["middle","bottom"]),
    leftImage: function(props, propName, componentName) {
      if(props[propName] && (!props[propName].type || props[propName].type !== "img") ) {
        return new Error("Left image must be an image")
      }
    },
    rightImage: function(props, propName, componentName) {
      if(props[propName] && (!props[propName].type || props[propName].type !== "img") ) {
        return new Error("Right image must be an image")
      }
    },
    hasImages: function(props, propName, componentName) {
      if(!props["leftImage"] && !props["rightImage"]) {
        return new Error("The media component must have at least one image")
      }
    }
  },
  render: function () {
    var leftMedia,
        rightMedia = '';

    var classes = setClass({
      'media': true,
      'media-stackable-xs': this.props.stackSize === 'xsmall',
      'media-stackable-sm': this.props.stackSize === 'small',
      'media-stackable-md': this.props.stackSize === 'medium',
      'media-stackable-lg': this.props.stackSize === 'large'
    });

    var bodyClasses = setClass({
      'media-body': true,
      'media-middle': this.props.vAlign === 'middle',
      'media-bottom': this.props.vAlign === 'bottom'
    });

    if (this.props.leftImage) {
      leftMedia = (
        <MediaObject
          horizontalAlignment='left'
          vAlign={this.props.vAlign}
          leftMediaSpacing={this.props.leftMediaSpacing}>
            {this.props.leftImage}
        </MediaObject>
      );
    }

    if (this.props.rightImage) {
      rightMedia = (
        <MediaObject
          horizontalAlignment='right'
          vAlign={this.props.vAlign}
          rightMediaSpacing={this.props.rightMediaSpacing}>
            {this.props.rightImage}
        </MediaObject>
      );
    }

    return (
      <div {...this.props} className={classes}>
        {leftMedia}
        <div className={bodyClasses}>
          {this.props.children}
        </div>
        {rightMedia}
      </div>
    );
  }
});

var Flag = React.createClass({
  getDefaultProps: function () {
    return {
      vAlign: 'middle'
    }
  },
  render: function () {
    return (
      <Media {...this.props}>{this.props.children}</Media>
    );
  }
});

module.exports = {
  Media: Media,
  Flag: Flag
};

We still want to set it up to use a <Image /> component rather than the default html img (because it is weird to have an href attribute on a base html element, but that will have to be done after the holidays.

@stubbornella
Copy link
Author

We think the validations will be simpler with an <Image /> component, something like:

  optionalUnion: React.PropTypes.oneOfType([
      React.PropTypes.instanceOf(Image),
      React.PropTypes.instanceOf(Svg),
      React.PropTypes.instanceOf(Icon)
    ]),

But yeah, that will have to wait...

@sophiebits
Copy link

To check if a ReactElement has a particular type, check its .type property:

var img = <Image />;
img.type === Image  // true

There's no built-in PropTypes validator to check for an element with a particular type.

@vjeux
Copy link

vjeux commented Dec 28, 2014

You need to be really careful when checking against the type field of a ReactElement. When done the naive way, it prevents you from writing abstractions.

For example, if you only accept a prop img that satisfies img.type === Image, then it only accepts a literal <Image ...> component. But it won't accept a higher level component like <ProfilePicture> that eventually renders an <Image> component.

@pmeskers
Copy link

pmeskers commented Jan 2, 2015

@spicyj @vjeux I'm having difficulty getting a ReactElement to return its type the way you specified... my reproduction produced the following:

var img = <Image />
img.type === Image // false
console.log(img.type)

outputs...

function (props) {
      // This constructor is overridden by mocks. The argument is used
      // by mocks to assert on what gets mounted. This will later be used
      // by the stand-alone class implementation.
}

which comes from ReactClass.js. Would love any feedback to clarify my understanding of how to use the type field of ReactElements!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment