Skip to content

Instantly share code, notes, and snippets.

@vjeux
Created July 31, 2016 02:46
Show Gist options
  • Save vjeux/8e27dd06c64dc566bfee705e1b19cb18 to your computer and use it in GitHub Desktop.
Save vjeux/8e27dd06c64dc566bfee705e1b19cb18 to your computer and use it in GitHub Desktop.
// Solution #1: Generic Override (like CSS)
var ButtonGroup = React.createClass({
render() {
return <div>{this.props.buttons.map((btn, i) =>
<Button style={{
...(i !== 0 && {marginLeft: 0})
...(i !== this.props.buttons.length - 1 && {marginRight: 0})
}} />
)}</div>;
}
});
var Button = React.createClass({
render() {
return <div style={{
...styles.button,
...this.props.style,
}} />;
}
});
// Solution #2: Encapsulated behavior
var ButtonGroup = React.createClass({
render() {
return <div>{this.props.buttons.map((btn, i) =>
<Button
hasLeftSibling={i !== 0}
hasRightSibling={i !== this.props.buttons.length - 1}
/>
)}</div>;
}
});
var Button = React.createClass({
render() {
return <div style={{
...styles.button,
...(hasLeftSibling && {marginLeft: 0}),
...(hasRightSibling && {marginRight: 0}),
}} />;
}
});
@taion
Copy link

taion commented Jul 31, 2016

I think these both work fairly well assuming that the children of the "group" component are homogenous.

A lot of the compile-to-CSS approaches actually give you something similar by letting you "escape hatch" with something like a > * selector, or various :nth-child selectors. CSS modules can't avoid giving you this, and @threepointone's glamor has a similar concept as well.

I think where this does less well is if the children of the group are not homogeneous, but you still want to apply some styling there. Imagine you had:

<InlineForm>
  <EmailInput />
  <PasswordInput />
  <LoginButton />
</InlineForm>

where this was just one example of a broader concept of "button in inline form".

I've written up an intentionally horrible example at https://gist.github.com/taion/ac60a7e54fb02e000b3a39fd3bb1e944 in a bit more detail.

@JedWatson
Copy link

JedWatson commented Jul 31, 2016

I've been struggling with this as well, working on Elemental UI.

@taion's example is exactly where we're falling down right now. The user (React library consumer) should control the children in the <InlineForm> component, which may even be nested:

<InlineForm>
  <EmailInput />
  <PasswordInput />
  <SomeComponent>
    <LoginButton />
  </SomeComponent>
</InlineForm>

You want the <LoginButton> component to be in control of its own styles and understand how to change them when it's wrapped inside an <InlineForm> component, rather than being on its own. This is an inversion of control to the first example, where you'd have to encapsulate understanding of how <LoginButton> (and any other component)'s styles should change inside the Form component.

I think that React context is a reasonable way to achieve this at a library level. It doesn't answer how we detect things like firstChild and lastChild. Maybe adding those props in the render loop is the right answer. This solution covers most of the requirements I've got:

var ButtonGroup = React.createClass({
  childContextTypes: {
    isInsideButtonGroup: React.PropTypes.boolean,
  },
  getChildContext() {
    return { isInsideButtonGroup: true };
  },
  render() {
    const childCount = React.Children.count(this.props.children);
    return <div>{React.Children.map(this.props.children, (c, i) => 
      React.cloneElement(c, {
        isFirstChild: i === 0,
        isLastChild: i === childCount - 1,
      })
    )}</div>;
  }
});

var Button = React.createClass({
  contextTypes: {
    isInsideButtonGroup: React.PropTypes.boolean,
  },
  render() {
    return <div style={{
      ...styles.button,
      ...(this.context.isInsideButtonGroup && styles.buttonInsideGroup),
      ...(!this.props.isFirstChild && {marginLeft: 0}),
      ...(!this.props.isLastChild && {marginRight: 0}),
    }} />;
  }
});

@threepointone
Copy link

here's a take on the above with glamor -

let addToButtStyle = merge(
  not(':first-child', { margin:0 }), 
  lastChild({ marginRight: 10 })
)

class ButtonGroup extends React.Component {
  render() {
    return <div>
    {this.props.labels.map(label => 
      <Button style={addToButtStyle}>{label}</Button>)}
    </div>
  }
}


let defaultStyle = {
  marginLeft: 10,
  display: 'inline',
  border: '1px solid black'
}
class Button extends React.Component {
  render() {
    return <div {...merge(defaultStyle, this.props.style)}>{this.props.children}</div>  
  }  
}


// alternately, with context 
@btnmerge(merge(  // assume this decorator magically comes from button.js 
  not(':first-child', { margin:0 }), 
  lastChild({ marginRight: 10 })
))
class ButtonGroup extends React.Component {
  render() {
    return <div>
    {this.props.labels.map(label => 
      <Button>{label}</Button>) // no props!
  }
    </div>
  }
}


// button.js 
let defaultStyle = {
  marginLeft: 10,
  display: 'inline',
  border: '1px solid black'
}
class Button extends React.Component {
  static contextTypes = {
    // yada yada 
  }
  render() {
    return <div {...merge(defaultStyle, this.context.buttStyle)}>{this.props.children}</div>  
  }  
}

@taion
Copy link

taion commented Jul 31, 2016

Trying to move discussion to styled-components/spec#5.

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