-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Overriding styles on child elements #5
Comments
I'll start off by noting that there's actually two different approaches to styling children based on parents. The former case is where the child owns the style override. Consider The latter case is where the parent owns the style override. Imagine a The hardest case IMO is if you have something like the latter case above, but where the children are of different types, so you want to only select a subset of those children. |
Porting my comment from @vjeux's gist: I've been struggling with this as well, working on Elemental UI. There are two concerns:
For the first, React context may be reasonable way to achieve this at a library level. For the second, maybe adding Here's an example of using both together, to modify a 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}),
...this.props.button,
}} />;
}
});
<ButtonGroup>
<Button style={{ fontWeight: 'bold' }}>Bold</Button>
<Button style={{ fontStyle: 'italic' }}>Italic</Button>
<Button style={{ textDecoration: 'underline' }}>Underline</Button>
</ButtonGroup> |
I'm a fan of the context approach. also, if your css lib has 'support' for pseudo selectors, and combining rules, you can get a tighter api. copied from my comment on vjeux's gist - @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>
}
} |
That case is actually something that's relatively straightforward to handle with CSS Modules. /* Button.css */
.group { /* whatever */ }
.btn { /* whatever */ }
.group .btn { /* whatever */ }
.group .btn:first-child { /* whatever */ }
/* ButtonGroup.css */
.group {
composes: group from "./Button.css";
/* actual styling */
} Where I feel this suffers a little is that it forces For relatively invasive changes to styling, that's fine, but I think there's a reasonably sized family of "simple" cases like tweaking margins where I feel like it makes more sense for the styling to belong to the parent, such that |
@threepointone That's really cool for cases where you want to write selectors that look like |
here ya go, don't need context - // button.js
export const buttonStyle = style({
marginLeft: 10,
display: 'inline',
border: '1px solid black'
})
class Button extends React.Component {
render() {
return <div {...buttonStyle}>{this.props.children}</div>
}
}
// buttongroup.js
import btnStyle from './button.js'
let btnId = idFor(btnStyle) // idFor exists in glamor, just needs to be exported
let groupStyle = merge(
select(` > ${btnId}`, { color: 'red' }),
select(` > ${btnId}:not(first-child)`, { marginLeft: 0 }) // etc
)
class ButtonGroup extends React.Component {
render() {
return <div {...groupStyle}>
{this.props.labels.map(label =>
<Button>{label}</Button>)}
</div>
}
} |
@taion I think if we could get css-modules implemented in pure JavaScript (see #3) this conversation would be over 😉 I should probably add to the requirements, for clarity, that the litmus test for whether we've achieved our goal is that the solution would work with ref facebook/create-react-app#78 facebook/create-react-app#90 facebook/create-react-app#111 |
@threepointone Very neat. You could also use a tagged template literal there instead of exporting |
@threepointone that's very cool. My concern is that it inverts control; For Elemental UI, it's a goal that we could ship something like For that to work, the inner component needs to control the styles, not the wrapping component. |
@JedWatson not sure I comprehend, you can do that too (comment on original gist for your usecase https://gist.github.com/taion/ac60a7e54fb02e000b3a39fd3bb1e944#gistcomment-1838307) gimme an idea for what api you'd like, and I'll try to fake it? |
@JedWatson what I'm trying to convey is you could go whichever way you want, glamor has the primitives to help you achieve that (I hope) |
@threepointone good point, both cases are possible - nothing with your approach prevents the use of context in my example 😄 |
@JedWatson and I are asking for the opposite thing I think in terms of which component (parent or child) owns the styling. Both cases have their uses. |
It's sorta like the expression problem :p Add a new component to an existing wrapper, v. adding a new wrapper that can style existing components. |
Spot on comparison, love it. |
I think we've just expressed a requirement 😀 I'll add it to the readme! |
👋 this is not exactly the right place for this but I did want to note that direct iteration of children for the sake styling is fraught. the simple cases of first/last child are easy enough however the opaque nature of react components and prevalence of HOC s makes doing stuff like |
That brings up 2 separate points:
|
Hi all, I think being more clunkier than CSS selectors is a feature. The issue with CSS selectors is they give you a lot of power, yet not always enough. Being able to drill down and target particular elements feels great because it is flexible. However, multiple competing selectors lead to fragile code where it can be hard for a reader or a writer to wrestle it to react in the intended way without breaking other use cases. I think the inner component has to control the styles. Its scope expands over time to handle more use cases — the flexibility lives in its props instead of living in the power of selectors. @taion Iterating with .map() is quite declarative IMO. Having a bunch of tools such as firstChild(), lastChild() might be well possible and even tasteful. It means all the active deciding factors happen in one place, rather than the potential of selectors to be interwoven into a complex pattern. I’m not sure if that’s what @jquense was mentioning about duck typing. I think the solution to the homogeneity issue are props that are commonly used across components, allowing them to react similarly to the same intentions, the same props. One interesting thing I have being playing around with is the idea of style objects being almost like little components. You create a function that gets passed certain props, and it renders out a CSS style and class name. You then splat this into the components you want. You might have tried it. This also allows that homogeneity to be more easily achieved — just reuse stylers between components. I have made two libraries for styling in React. The second is an opinionated approach to flexbox and the other parts of CSS. You can use it either by splatting with the |
Fundamentally, I think semantically, having in e.g. .form-inline > .btn {
margin: 0 0.5rem;
} is a good thing. The spacing of buttons in a form is a concern of the form, and should be expressed that way. I think @threepointone's https://gist.github.com/taion/ac60a7e54fb02e000b3a39fd3bb1e944#gistcomment-1838307 and #5 (comment) are the closest to what seem like approaches that blend the semantic expressiveness of CSS selectors with the explicitness and encapsulation that were the promise of CSS-in-JS. Most of the other solutions seem to give up too much on the former. |
I think the solutions with selectors are fine: they work, they are familiar. However, I think there is potential for more by breaking away from the constraints of CSS selectors. Before CSS were a variety of other more flexible syntaxes: https://eager.io/blog/the-languages-which-almost-were-css/ This sort of code is declarative in my opinion, or declarative ‘enough’:
The values being passed are dependent on the input, and the input only. There’s no external factors. Anything that has an effect is being explicitly passed down. Just like event handlers — it would have been understandable for React to have gone with a selector system to attach events, instead of explicitly setting handlers on every element. I think that’s what Cycle.js uses. But it is less explicit in my opinion — you are using tag and class names as a common bridge between the two worlds. An extra layer of indirection. Margin has always been a bit of an odd CSS property as it affects multiple elements — which element does it belong to? An alternate way of adding spacing within a form is to interleave spacer elements between children. This gives the parent total control, and removes the responsibility from the child component. This is how spacing is now recommended in Auto Layout: http://useyourloaf.com/blog/goodbye-spacer-views-hello-layout-guides/ |
We're not working with the same declaration of "declarative" here. By your definition, using The benefit of CSS selectors is that they're a DSL that is very well suited to expressing notions of element selection. In the same way that we use a DSL in JSX to express markup and a different DSL in GraphQL to express data requirements, there's nothing a priori wrong with using a DSL to express element selection. The point of a terse, expressive DSL isn't necessarily to satisfy all cases; a DSL that gives me a dramatically nicer grammar and still handles the vast majority of use cases is still a DSL that's worth using. |
Nicolas Gallagher has (had?) a very interesting take on how to style dependencies. The appearance of dependencies must be configured using the interface they provide. both of @vjeux's solutions seem to follow the same principle and I In my opinion they come with two pain points though
A CSS only solution might still possible and would involve attaching parent classes to the children and/or wrapping children. The latter has been mentioned already – map over the children and wrap them in a container with class In order to attach parent classes instead each component should duplicate and prefix with // pof
function ctxClassName(className, ctx) {
if (!ctx) { return className }
return `
${className}
${className.split(' ').map(c => ctx+'-'+c.toCamelCase()).join(' ')}
`.trim()
}
function Button({ children, ctx = ''}) {
return (
<button className={ctxClassName('Button', ctx)}>
{children}
</button>
)
} <div class="Toolbar">
<button class="Button Toolbar-button">...</button>
</div> I am pretty sure that this solution is not perfect and it definitely has some weak points (eg. specificity or deeply nested components). Finally it is worth keeping in mind that styling dependencies is still an unsolved problem in Pure CSS land so maybe the solution's to be found somewhere else 🎩 🐇 :) |
I think the
|
To take a step back, I'm not looking for CSS per se. What I want is instead something like expressiveness. Consider the following CSS selectors: .parent > * {}
.parent .child {}
.parent > :last-child {} The first one you'd do with something like: function Parent({ children }) {
return (
<div style={parentStyle}>
{React.Children.map(children, child => (
cloneElement(child, { style: childStyle })
))}
</div>
);
} For the second, you'd do something like: const childContextTypes = {
childStyle: React.PropTypes.object.isRequired,
};
class Parent extends React.Component {
getChildContext() {
return {
childStyle: whatever,
};
}
// ...
}
// And then the receiver in Child.js. This can and should be factored out though. For the last, you'd do something like: function Parent({ children }) {
return (
<div style={parentStyle}>
{React.Children.toArray(children).map((child, index, list) => (
cloneElement(child, {
style: index === list.length - 1 ? lastChildStyle : null,
})
))}
</div>
);
} You lose a lot in expressiveness and readability when you write things that way. The CSS selectors are obvious and in use cases like the above, are easy to grok. They capture similar concepts and look very similar, and communicate their intent very easily to other people reading the code. The React versions look dissimilar, and it's a lot less easy to tell at a glance what they're doing. In the last example, you have to go down to that conditional at the end and figure out the predicate before you know what it's doing. |
Yep precedence (specificity) is a problem – unless you double the specificity of an attached class e.g. Button.css would have to define .Excerpt-button, which I think is undesirable. This is not what I meant, Automatic class names generation could be done in a smart way e.g. React.Children.map(
children,
c => (
<div className={'Excerpt-'+(c.displayName || c.name || 'item').toCamelCase()}>
{c}
</div>
)
) By the way |
@jquense mentioned that above – |
Here is another approach, using ‘stylers’ which you splat into your component. Stylers focus only on presentation, and return This way the where (which components need styling) is separated from the how (what styles to use what situations).
I usually use this technique with components that render children based on props such as |
styled-components already supports this, so I'm going to close this... also for staleness. |
cc @jquense @threepointone @vjeux @JedWatson @ryanflorence
ref Medium article, @Vjeux Gist, @taion Gist (esp. @threepointone comment), css-modules/css-modules#147
It's somewhat difficult in the current componentized CSS solutions to handle cases like:
or
There may well be styling that you want to attach to buttons that render in specific contexts. With normal CSS, you'd write selectors that look like
.form-inline > .btn
or.form-inline .btn
.With componentized CSS, it can be more difficult. Depending on use case, sometimes this styling should go in
<Button>
, and sometimes perhaps it should go in<InlineForm>
.You can achieve similar behavior to these selectors with various combinations of React context and cloning children with additional props. However, for more complex selectors including e.g.
:first-child
or:last-child
, the equivalent code gets a bit more complicated.I think the question becomes:
Well, that's the best high-level summary I can synthesize. Let me know what I got wrong.
The text was updated successfully, but these errors were encountered: