Skip to content
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

How to handle descendant selection of pseudo-states? #10

Closed
zgotsch opened this issue Oct 26, 2015 · 37 comments · Fixed by #95
Closed

How to handle descendant selection of pseudo-states? #10

zgotsch opened this issue Oct 26, 2015 · 37 comments · Fixed by #95

Comments

@zgotsch
Copy link
Contributor

zgotsch commented Oct 26, 2015

If I want to apply styles to a child based on its parent's :hover state, I think I have to track hover state in js. Is that accurate? Do you think handling this is in the scope of aphrodite?

Specifically I am thinking of something that emulates the CSS:

.parent:hover .child {
    background-color: blue;
}
@jlfwong
Copy link
Collaborator

jlfwong commented Oct 26, 2015

In general, having parents affect the styles of their children is a pattern we want to avoid, but I can see why this particular example might occur within a single component's markup.

I'm reluctant to see patterns like this become first class citizens, but we do need some escape hatch way of writing arbitrary CSS (whether it's globally scoped or scoped to the generated selector), akin to dangerouslySetInnerHTML, which you'd be able to use to do that.

@zgotsch
Copy link
Contributor Author

zgotsch commented Oct 28, 2015

One hack which we're cautiously using right now works only because of the way pseudo-selectors are handled.

const styles = StyleSheet.create({
  parent: {
    ':hover .child': {
      backgroundColor: blue
    }
  }
});

Which is totally not part of the API, and I fully expect it to break at some point.

@zgotsch
Copy link
Contributor Author

zgotsch commented Oct 28, 2015

Come to think of it, this also might break on merge

@jlfwong
Copy link
Collaborator

jlfwong commented Oct 28, 2015

Interesting! That definitely was not an intention of the API design, but perhaps is something we should support for the sake of API simplicity. I don't see any immediate reason for that to break on merging.

@kentcdodds
Copy link
Contributor

I also just ran into this case. I'll proceed with @zgotsch's workaround. Is there anything specific that we could do to make this a first-class citizen so I could rely on it and not worry about this breaking in future releases? I'm happy to contribute to the project with a little direction :-)

@kentcdodds
Copy link
Contributor

Actually, thinking about this more, I think that this is a scenario that would be better to avoid supporting and we should find another way to work around this issue. Here's the kind of API I'd like to see (and maybe help build):

const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      nonCssPropertyShouldCreateAnotherClass: {
        color: 'black',
      },
    },
  },
})

const { parentClassName, nonCssPropertyShouldCreateAnotherClassClassName } = cssWithDecendents(styles.parent)
// parentClassName === 'parent-blahblah'
// nonCssPropertyShouldCreateAnotherClassClassName === 'nonCssPropertyShouldCreateAnotherClass-blahblah'

Which would produce classes with the following CSS:

.parent-blahblah:hover {
  background-color: 'white';
}

.parent-blahblah:hover .nonCssPropertyShouldCreateAnotherClass {
  color: 'black';
}

Obviously, the names are up for debate. But I think an entirely new method should be used and we shouldn't overload the css method. Also, the css method should probably warn when given a property that is not a css property it understands (maybe? but that's a different issue).

Thoughts 💭?

@xymostech
Copy link
Contributor

@kentcdodds I really like that interface. Seems like a great balance between being flexible and still sorta following the "inline-styling way". Some random thoughts:

  • We don't have a good way to distinguish whether something is a CSS property or not. Maybe we could make the key value something like ">descendantClass", where the > indicates a descendant?
  • What would rules for recursion be? I think I would want to limit the depth that you could go to 1 level (so you would never be generating .a .b .c class names).

@kentcdodds
Copy link
Contributor

Maybe we could make the key value something like ">descendantClass", where the > indicates a descendant

Seems reasonable to me 👍

As for depth, I don't see why aphrodite should concern itself with enforcing a depth ¯_(ツ)_/¯ while I agree developers should probably not go too deep on this, I don't think we'd have to put in extra effort to combat it. It'd turn into a serious code smell before it caused aphrodite trouble I think.

@RGBboy
Copy link

RGBboy commented Apr 11, 2016

If there was an API for defining a ruleset you could easily determine which are properties vs a descendantClass:

const child = StyleSheet.createRuleset({
  color: 'black'
}
const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      child: child
    }
  }
})

const { parent, child } = css(styles)
// parent === 'parent-blahblah'
// child === 'child-xyz'

@kentcdodds
Copy link
Contributor

Ok, I think that we've got two solid ideas, which should we go with?

Use >decendantClass

const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      '>child': {color: 'black'}
    }
  }
})

const { parent, child } = css(styles.parent)
// parent === 'parent-blahblah'
// child === 'child-xyz'

Have a special API for creating rule sets

const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      child: StyleSheet.createRuleset({color: 'black'}
    }
  }
})

const { parent, child } = css(styles.parent)
// parent === 'parent-blahblah'
// child === 'child-xyz'

I'm not sure which I like better. Would love to get feedback from others.

@montemishkin
Copy link
Contributor

I like the simplicity (for the developer using aphrodite) of >descendentClass. However, I'm wondering if I'm misinterpreting something:

The > symbol is reminiscent of .foo > .bar in true CSS, which indicates a direct descendent, whereas via the examples given above, it seems like you are proposing that this produce css which is selecting any (direct or indirect) descendent.

I think that this would be a misleading choice of api.

Am I misinterpreting?

Also, is the goal to be able to support both direct and arbitrary descendents? It seems like arbitrary descendents should cover all desired use cases since the class names are uniquely generated, whereas direct descendents would not.

@kentcdodds
Copy link
Contributor

Agreed. That would be misleading... I also think that I prefer the simplicity of the > but not at the expense of it being misleading. Other thoughts of how we could make the API more intuitive and simple?

@montemishkin
Copy link
Contributor

Also, I think I agree with this:

... I think an entirely new method should be used and we shouldn't overload the css method.

cssWithDescendents or css.descendents seems reasonable to me, though I'll have to think on it a bit more.

@xymostech
Copy link
Contributor

@montemishkin You are correct, the > symbol would be a bit misleading about what it does if we allowed any descendents. Can anyone think of a better symbol to use there, or another way to indicate children?

@jlfwong
Copy link
Collaborator

jlfwong commented Apr 13, 2016

Would we still want to have this syntax if we supported unsafeRawCss as discussed in #30?

@kentcdodds
Copy link
Contributor

Is there a reason we couldn't just do:

const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      child: {color: 'black'}
    }
  }
})

const { parent, child } = css.descendents(styles.parent)
// parent === 'parent-blahblah'
// child === 'child-xyz'

I'm pretty sure that we can just say that if the value of a css property is an object, consider it a descendant.

As for the unsafeRawCss, the difference with this feature is that I can still get a className back for child which I can apply to a specific element. The use case with unsafeRawCss that I see is that you can use any kind of selector you want and you don't get a className back for it, so it covers cases where you're not in control of the markup (like with dangerouslySetInnerHTML)

@xymostech
Copy link
Contributor

I'm pretty sure that we can just say that if the value of a css property is an object, consider it a descendant.

This wouldn't work, because we sometimes allow real objects as values for css properties, for instance with fontFamily you can use arrays/objects, or with animationName you can provide objects.

@csilvers
Copy link
Member

Just a drive-by kibbitz, but one possible syntax for "direct or indirect descendant" could be ">>descendentClass".

@xymostech
Copy link
Contributor

Okay, I played around with a couple different alternatives, and it looks like we haven't really talked about what gets done with the results of css.descendents. Would it make sense to plug those things into a call to css()? So after something like:

const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      '>>child': {color: 'black'},
    },
  },

  red: {
    color: 'red',
  },
})

const { parent, child } = css.descendents(styles.parent);

Would we be doing className={child + " " + css(styles.red)} or className={css(styles.red, child)}? It sounds like we were talking about the first one, but the second one looks nicer.

I also played around with something that looks like

const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      '>>child': {color: 'black'},
    },
  },

  red: {
    color: 'red',
  },
})

<div className={css(styles.parent)}>
  <div className={css(styles.red, styles.parent.child)}> // <- 

but I don't think that we'll actually be able to generate appropriate class names using this interface (even though it looks even nicer than the css.descendents stuff).

@zgotsch
Copy link
Contributor Author

zgotsch commented Apr 19, 2016

Just a drive-by, but I really like the second API, Emily (the one with styles.parent.child). I would also be happy with something like css(styles.red, styles.parent.descendent('child')) or similar.

@xymostech
Copy link
Contributor

@zgotsch I like that too. I'm a little scared of a situation like this though:

const styles = StyleSheet.create({
    child: {
        color: 'red',
    },

    base: {
        color: 'black',
        ':hover': {
            '>>child': {
                color: 'blue',
            }
        },
    },

    greenchild: {
        ':hover': {
            '>>child': {
                color: 'green',
            },
        },
    },
});

<div className={css(styles.base, styles.greenchild)}>
    <div className={css(styles.child, styles.base.child)}>

Presumably we would want to generate CSS like...

.child_XXX {
    color: red;
}
.base_XXX-o_O-greenchild_XXX {
    color: black;
}
.base_XXX-o_O-greenchild_XXX:hover .child_XXX {
    color: green;
}

but then you might want

<div className={css(styles.base, styles.greenchild)}>
    <div className={css(styles.child, styles.greenchild.child)}>

to do the same thing? But we can't make styles.base.child and styles.greenchild.child add the same class names, so maybe we have to duplicate the related CSS:

.base_XXX-o_O-greenchild_XXX:hover .child_XXX { // child from base
    color: green;
}
.base_XXX-o_O-greenchild_XXX:hover .child_XXX { // child from greenchild
    color: green;
}

Sorry for brain dumping. I'm gonna play around with this...

xymostech added a commit that referenced this issue Apr 20, 2016
Summary: It turns out descendant selectors are kinda hard. In particular, it's
difficult to let *descendants have unique classnames* for themselves, at the
same time as allowing *merging between styles which contain descendant
selectors*.

This pull request attempts to do both of these things. The code is a bit messy,
so I'll lay out what's going on in this pull request. Please ask questions, if
something is confusing! The Aphrodite code is nice because it's all fairly
easily understood, and I'm afraid that this adding too much complexity.

An example of the syntax (and an example for the explanation in this commit
message), consider:

```js
const styles = StyleSheet.create({
    parent: {
        '>>child': {
            color: "red"
        },
        ':hover': {
            '>>child': {
                color: "blue"
            },
            '>>otherchild': {
                color: "white"
            }
        },
    },

    altparent: {
        ':hover': {
            '>>child': {
                color: "green"
            }
        }
    }
});

The basic flow of this diff is:

1. In `StyleSheet.create`, we recurse through the passed-in styles, and find
each of the descendant selectors (which have keys that look like `>>blah`). In
the example above, it would find `parent['>>child']`,
`parent[':hover']['>>child']`, `parent[':hover']['>>otherchild']`, and
`altparent[':hover']['>>child']`. In each place, we:

  - generate a class name for that descendant selector. This is based on the
    class name of the parent class, as well as the key name.

    For example, if the class name for `styles.parent` was `parent_abcdef`, we
    might generate the class name `parent_abcdef__child` for `parent['>>child']`.

  - tag the style by adding a `_names` object, with the class name as a key of
    the object.

    For example, `parent['>>child']` would end up looking like `{ color: "red",
    _names: { parent_abcdef__child: true } }`.

  - collect a map of each of the keys (without the `>>` bit) to their class
    names.

    For example, for `styles.parent`, we would generate a map that looks like
    `{ child: "parent_abcdef__child", otherchild: "parent_abcdef__otherchild"
    }`.

We merge in the map from key to class name into the generated style, so that
the class names can be accessed using a syntax like `styles.parent.child`.

2. When *parent* styles are passed into `css()`, their styles are merged
together. If one style overrides another's descendant styling, the `_names`
object will be merged together and will contain all of the associated class
names.

For example, when evaluating `css(styles.parent, styles.altparent)`, we would
end up with merged styles looking like:
```
{
    '>>child': {
        color: "red",
        _names: { parent_abcdef__child: true },
    },
    ':hover': {
        '>>child': {
            color: "green",
            _names: {
                parent_abcdef__child: true,
                altparent_123456__child: true,
            },
        },
        '>>otherchild': {
            color: "white",
            _names: { parent_abcdef__otherchild: true },
        }
    }
}
```

We then generate a map from the descendent keys to all of the class names that
could be associated with a given key by recursing and looking at each of the
`_names` objects. For example, the map would look like:

```
{
    '>>child': ["parent_abcdef__child", "altparent_123456__child"],
    '>>otherchild': ["parent_abcdef__otherchild"]
}
```

When generating the styles, we look at this map and then generate styles for
each of the classnames listed. This is so that these styles will match up with
uses of both `css(styles.parent.child)` and `css(styles.altparent.child)`.

For example, when generating the `style[':hover']['>>child']` styles, we
generate:

```
.parent_abcdef-o_O-altparent_123456:hover .parent_abcdef__child { ... }
.parent_abcdef-o_O-altparent_123456:hover .altparent_123456__child { ... }
```

3. When *descendant* styles are passed into `css()`, like
`css(styles.parent.child)`, we simply return the associated class name (in this
case, `"parent_abcdef__child"`) in the output.

Fixes #10

Test Plan:
 - `npm run test`
 - `cd examples && npm run examples`, then visit http://localhost:4114/ and see
   that the last line starts green and when "Hover over me" is hovered, the
   other part turns blue.

@zgotsch @jlfwong @kentcdodds @montemishkin
xymostech added a commit that referenced this issue Apr 20, 2016
Summary: It turns out descendant selectors are kinda hard. In particular, it's
difficult to let *descendants have unique classnames* for themselves, at the
same time as allowing *merging between styles which contain descendant
selectors*.

This pull request attempts to do both of these things. The code is a bit messy,
so I'll lay out what's going on in this pull request. Please ask questions, if
something is confusing! The Aphrodite code is nice because it's all fairly
easily understood, and I'm afraid that this adding too much complexity.

An example of the syntax (and an example for the explanation in this commit
message), consider:

```js
const styles = StyleSheet.create({
    parent: {
        '>>child': {
            color: "red"
        },
        ':hover': {
            '>>child': {
                color: "blue"
            },
            '>>otherchild': {
                color: "white"
            }
        },
    },

    altparent: {
        ':hover': {
            '>>child': {
                color: "green"
            }
        }
    }
});

The basic flow of this diff is:

1. In `StyleSheet.create`, we recurse through the passed-in styles, and find
each of the descendant selectors (which have keys that look like `>>blah`). In
the example above, it would find `parent['>>child']`,
`parent[':hover']['>>child']`, `parent[':hover']['>>otherchild']`, and
`altparent[':hover']['>>child']`. In each place, we:

  - generate a class name for that descendant selector. This is based on the
    class name of the parent class, as well as the key name.

    For example, if the class name for `styles.parent` was `parent_abcdef`, we
    might generate the class name `parent_abcdef__child` for `parent['>>child']`.

  - tag the style by adding a `_names` object, with the class name as a key of
    the object.

    For example, `parent['>>child']` would end up looking like `{ color: "red",
    _names: { parent_abcdef__child: true } }`.

  - collect a map of each of the keys (without the `>>` bit) to their class
    names.

    For example, for `styles.parent`, we would generate a map that looks like
    `{ child: "parent_abcdef__child", otherchild: "parent_abcdef__otherchild"
    }`.

We merge in the map from key to class name into the generated style, so that
the class names can be accessed using a syntax like `styles.parent.child`.

2. When *parent* styles are passed into `css()`, their styles are merged
together. If one style overrides another's descendant styling, the `_names`
object will be merged together and will contain all of the associated class
names.

For example, when evaluating `css(styles.parent, styles.altparent)`, we would
end up with merged styles looking like:
```
{
    '>>child': {
        color: "red",
        _names: { parent_abcdef__child: true },
    },
    ':hover': {
        '>>child': {
            color: "green",
            _names: {
                parent_abcdef__child: true,
                altparent_123456__child: true,
            },
        },
        '>>otherchild': {
            color: "white",
            _names: { parent_abcdef__otherchild: true },
        }
    }
}
```

We then generate a map from the descendent keys to all of the class names that
could be associated with a given key by recursing and looking at each of the
`_names` objects. For example, the map would look like:

```
{
    '>>child': ["parent_abcdef__child", "altparent_123456__child"],
    '>>otherchild': ["parent_abcdef__otherchild"]
}
```

When generating the styles, we look at this map and then generate styles for
each of the classnames listed. This is so that these styles will match up with
uses of both `css(styles.parent.child)` and `css(styles.altparent.child)`.

For example, when generating the `style[':hover']['>>child']` styles, we
generate:

```
.parent_abcdef-o_O-altparent_123456:hover .parent_abcdef__child { ... }
.parent_abcdef-o_O-altparent_123456:hover .altparent_123456__child { ... }
```

3. When *descendant* styles are passed into `css()`, like
`css(styles.parent.child)`, we simply return the associated class name (in this
case, `"parent_abcdef__child"`) in the output.

Fixes #10

Test Plan:
 - `npm run test`
 - `cd examples && npm run examples`, then visit http://localhost:4114/ and see
   that the last line starts green and when "Hover over me" is hovered, the
   other part turns blue.

@zgotsch @jlfwong @kentcdodds @montemishkin
@ranyefet
Copy link

ranyefet commented May 8, 2016

Is there any update regarding this issue?
I'm using .mySelector:nth-child(3) .childElement which currently I can only set up as a global CSS.

xymostech added a commit that referenced this issue May 10, 2016
Summary: It turns out descendant selectors are kinda hard. In particular, it's
difficult to let *descendants have unique classnames* for themselves, at the
same time as allowing *merging between styles which contain descendant
selectors*.

This pull request attempts to do both of these things. The code is a bit messy,
so I'll lay out what's going on in this pull request. Please ask questions, if
something is confusing! The Aphrodite code is nice because it's all fairly
easily understood, and I'm afraid that this adding too much complexity.

An example of the syntax (and an example for the explanation in this commit
message), consider:

```js
const styles = StyleSheet.create({
    parent: {
        '>>child': {
            color: "red"
        },
        ':hover': {
            '>>child': {
                color: "blue"
            },
            '>>otherchild': {
                color: "white"
            }
        },
    },

    altparent: {
        ':hover': {
            '>>child': {
                color: "green"
            }
        }
    }
});

The basic flow of this diff is:

1. In `StyleSheet.create`, we recurse through the passed-in styles, and find
each of the descendant selectors (which have keys that look like `>>blah`). In
the example above, it would find `parent['>>child']`,
`parent[':hover']['>>child']`, `parent[':hover']['>>otherchild']`, and
`altparent[':hover']['>>child']`. In each place, we:

  - generate a class name for that descendant selector. This is based on the
    class name of the parent class, as well as the key name.

    For example, if the class name for `styles.parent` was `parent_abcdef`, we
    might generate the class name `parent_abcdef__child` for `parent['>>child']`.

  - tag the style by adding a `_names` object, with the class name as a key of
    the object.

    For example, `parent['>>child']` would end up looking like `{ color: "red",
    _names: { parent_abcdef__child: true } }`.

  - collect a map of each of the keys (without the `>>` bit) to their class
    names.

    For example, for `styles.parent`, we would generate a map that looks like
    `{ child: "parent_abcdef__child", otherchild: "parent_abcdef__otherchild"
    }`.

We merge in the map from key to class name into the generated style, so that
the class names can be accessed using a syntax like `styles.parent.child`.

2. When *parent* styles are passed into `css()`, their styles are merged
together. If one style overrides another's descendant styling, the `_names`
object will be merged together and will contain all of the associated class
names.

For example, when evaluating `css(styles.parent, styles.altparent)`, we would
end up with merged styles looking like:
```
{
    '>>child': {
        color: "red",
        _names: { parent_abcdef__child: true },
    },
    ':hover': {
        '>>child': {
            color: "green",
            _names: {
                parent_abcdef__child: true,
                altparent_123456__child: true,
            },
        },
        '>>otherchild': {
            color: "white",
            _names: { parent_abcdef__otherchild: true },
        }
    }
}
```

We then generate a map from the descendent keys to all of the class names that
could be associated with a given key by recursing and looking at each of the
`_names` objects. For example, the map would look like:

```
{
    '>>child': ["parent_abcdef__child", "altparent_123456__child"],
    '>>otherchild': ["parent_abcdef__otherchild"]
}
```

When generating the styles, we look at this map and then generate styles for
each of the classnames listed. This is so that these styles will match up with
uses of both `css(styles.parent.child)` and `css(styles.altparent.child)`.

For example, when generating the `style[':hover']['>>child']` styles, we
generate:

```
.parent_abcdef-o_O-altparent_123456:hover .parent_abcdef__child { ... }
.parent_abcdef-o_O-altparent_123456:hover .altparent_123456__child { ... }
```

3. When *descendant* styles are passed into `css()`, like
`css(styles.parent.child)`, we simply return the associated class name (in this
case, `"parent_abcdef__child"`) in the output.

Fixes #10

Test Plan:
 - `npm run test`
 - `cd examples && npm run examples`, then visit http://localhost:4114/ and see
   that the last line starts green and when "Hover over me" is hovered, the
   other part turns blue.

@zgotsch @jlfwong @kentcdodds @montemishkin
xymostech added a commit that referenced this issue May 10, 2016
Summary: It turns out descendant selectors are kinda hard. In particular, it's
difficult to let *descendants have unique classnames* for themselves, at the
same time as allowing *merging between styles which contain descendant
selectors*.

This pull request attempts to do both of these things. The code is a bit messy,
so I'll lay out what's going on in this pull request. Please ask questions, if
something is confusing! The Aphrodite code is nice because it's all fairly
easily understood, and I'm afraid that this adding too much complexity.

An example of the syntax (and an example for the explanation in this commit
message), consider:

```js
const styles = StyleSheet.create({
    parent: {
        '>>child': {
            color: "red"
        },
        ':hover': {
            '>>child': {
                color: "blue"
            },
            '>>otherchild': {
                color: "white"
            }
        },
    },

    altparent: {
        ':hover': {
            '>>child': {
                color: "green"
            }
        }
    }
});

The basic flow of this diff is:

1. In `StyleSheet.create`, we recurse through the passed-in styles, and find
each of the descendant selectors (which have keys that look like `>>blah`). In
the example above, it would find `parent['>>child']`,
`parent[':hover']['>>child']`, `parent[':hover']['>>otherchild']`, and
`altparent[':hover']['>>child']`. In each place, we:

  - generate a class name for that descendant selector. This is based on the
    class name of the parent class, as well as the key name.

    For example, if the class name for `styles.parent` was `parent_abcdef`, we
    might generate the class name `parent_abcdef__child` for `parent['>>child']`.

  - tag the style by adding a `_names` object, with the class name as a key of
    the object.

    For example, `parent['>>child']` would end up looking like `{ color: "red",
    _names: { parent_abcdef__child: true } }`.

  - collect a map of each of the keys (without the `>>` bit) to their class
    names.

    For example, for `styles.parent`, we would generate a map that looks like
    `{ child: "parent_abcdef__child", otherchild: "parent_abcdef__otherchild"
    }`.

We merge in the map from key to class name into the generated style, so that
the class names can be accessed using a syntax like `styles.parent.child`.

2. When *parent* styles are passed into `css()`, their styles are merged
together. If one style overrides another's descendant styling, the `_names`
object will be merged together and will contain all of the associated class
names.

For example, when evaluating `css(styles.parent, styles.altparent)`, we would
end up with merged styles looking like:
```
{
    '>>child': {
        color: "red",
        _names: { parent_abcdef__child: true },
    },
    ':hover': {
        '>>child': {
            color: "green",
            _names: {
                parent_abcdef__child: true,
                altparent_123456__child: true,
            },
        },
        '>>otherchild': {
            color: "white",
            _names: { parent_abcdef__otherchild: true },
        }
    }
}
```

We then generate a map from the descendent keys to all of the class names that
could be associated with a given key by recursing and looking at each of the
`_names` objects. For example, the map would look like:

```
{
    '>>child': ["parent_abcdef__child", "altparent_123456__child"],
    '>>otherchild': ["parent_abcdef__otherchild"]
}
```

When generating the styles, we look at this map and then generate styles for
each of the classnames listed. This is so that these styles will match up with
uses of both `css(styles.parent.child)` and `css(styles.altparent.child)`.

For example, when generating the `style[':hover']['>>child']` styles, we
generate:

```
.parent_abcdef-o_O-altparent_123456:hover .parent_abcdef__child { ... }
.parent_abcdef-o_O-altparent_123456:hover .altparent_123456__child { ... }
```

3. When *descendant* styles are passed into `css()`, like
`css(styles.parent.child)`, we simply return the associated class name (in this
case, `"parent_abcdef__child"`) in the output.

Fixes #10

Test Plan:
 - `npm run test`
 - `cd examples && npm run examples`, then visit http://localhost:4114/ and see
   that the last line starts green and when "Hover over me" is hovered, the
   other part turns blue.

@zgotsch @jlfwong @kentcdodds @montemishkin
xymostech added a commit that referenced this issue May 10, 2016
Summary: It turns out descendant selectors are kinda hard. In particular, it's
difficult to let *descendants have unique classnames* for themselves, at the
same time as allowing *merging between styles which contain descendant
selectors*.

This pull request attempts to do both of these things. The code is a bit messy,
so I'll lay out what's going on in this pull request. Please ask questions, if
something is confusing! The Aphrodite code is nice because it's all fairly
easily understood, and I'm afraid that this adding too much complexity.

An example of the syntax (and an example for the explanation in this commit
message), consider:

```js
const styles = StyleSheet.create({
    parent: {
        '>>child': {
            color: "red"
        },
        ':hover': {
            '>>child': {
                color: "blue"
            },
            '>>otherchild': {
                color: "white"
            }
        },
    },

    altparent: {
        ':hover': {
            '>>child': {
                color: "green"
            }
        }
    }
});

The basic flow of this diff is:

1. In `StyleSheet.create`, we recurse through the passed-in styles, and find
each of the descendant selectors (which have keys that look like `>>blah`). In
the example above, it would find `parent['>>child']`,
`parent[':hover']['>>child']`, `parent[':hover']['>>otherchild']`, and
`altparent[':hover']['>>child']`. In each place, we:

  - generate a class name for that descendant selector. This is based on the
    class name of the parent class, as well as the key name.

    For example, if the class name for `styles.parent` was `parent_abcdef`, we
    might generate the class name `parent_abcdef__child` for `parent['>>child']`.

  - tag the style by adding a `_names` object, with the class name as a key of
    the object.

    For example, `parent['>>child']` would end up looking like `{ color: "red",
    _names: { parent_abcdef__child: true } }`.

  - collect a map of each of the keys (without the `>>` bit) to their class
    names.

    For example, for `styles.parent`, we would generate a map that looks like
    `{ child: "parent_abcdef__child", otherchild: "parent_abcdef__otherchild"
    }`.

We merge in the map from key to class name into the generated style, so that
the class names can be accessed using a syntax like `styles.parent.child`.

2. When *parent* styles are passed into `css()`, their styles are merged
together. If one style overrides another's descendant styling, the `_names`
object will be merged together and will contain all of the associated class
names.

For example, when evaluating `css(styles.parent, styles.altparent)`, we would
end up with merged styles looking like:
```
{
    '>>child': {
        color: "red",
        _names: { parent_abcdef__child: true },
    },
    ':hover': {
        '>>child': {
            color: "green",
            _names: {
                parent_abcdef__child: true,
                altparent_123456__child: true,
            },
        },
        '>>otherchild': {
            color: "white",
            _names: { parent_abcdef__otherchild: true },
        }
    }
}
```

We then generate a map from the descendent keys to all of the class names that
could be associated with a given key by recursing and looking at each of the
`_names` objects. For example, the map would look like:

```
{
    '>>child': ["parent_abcdef__child", "altparent_123456__child"],
    '>>otherchild': ["parent_abcdef__otherchild"]
}
```

When generating the styles, we look at this map and then generate styles for
each of the classnames listed. This is so that these styles will match up with
uses of both `css(styles.parent.child)` and `css(styles.altparent.child)`.

For example, when generating the `style[':hover']['>>child']` styles, we
generate:

```
.parent_abcdef-o_O-altparent_123456:hover .parent_abcdef__child { ... }
.parent_abcdef-o_O-altparent_123456:hover .altparent_123456__child { ... }
```

3. When *descendant* styles are passed into `css()`, like
`css(styles.parent.child)`, we simply return the associated class name (in this
case, `"parent_abcdef__child"`) in the output.

Fixes #10

Test Plan:
 - `npm run test`
 - `cd examples && npm run examples`, then visit http://localhost:4114/ and see
   that the last line starts green and when "Hover over me" is hovered, the
   other part turns blue.

@zgotsch @jlfwong @kentcdodds @montemishkin
@ide
Copy link
Contributor

ide commented May 28, 2016

Another motivation for supporting child selectors (outside of pseudo-classes even) is to style React components that we don't control. For example, React Bootstrap has a NavItem component that renders <li><a /></li>, and supports setting only the <li> element's className.

In this case I want to write:

StyleSheet.create({
  navItem: {
    'selector(a)': { color: linkColor }
  }
});

but sometimes would want more nested selectors like selector(a img) too.

@jlfwong
Copy link
Collaborator

jlfwong commented May 29, 2016

@ide The syntax I'm proposing would have you do 'selector(a)': { 'selector(img)': { color: linkColor } } for that instead. The generated CSS would do what you want, but having them separated would make the merging semantics more obvious.

@itajaja
Copy link

itajaja commented Jun 3, 2016

@ide I am facing the same problem. I came up with a pretty hacky function to select children:

function children(selector) {
  return `:active, ${selector}`;
}

/// use it like this

StyleSheet.create({
  header: {
    color: 'red',
    [children('li a')]: {
      color: 'blue',
    }
  },
})

Clearly, this works as expected only if the parent component cannot be active, otherwise you'd have to use another selector. I am using it until an official API is designed

@jlfwong
Copy link
Collaborator

jlfwong commented Jun 7, 2016

Thinking more about the selector(a) syntax, I'm worried about the non-determinism demonstrated in the following situation:

const Component = () => <div className={css(styles.foo)}>
    <div className={css(styles.bar)}>
        <span>Hello</span>
    </div>
</div>

const styles = StyleSheet.create({
    foo: { 'selector(span)': { color: 'red' }},
    bar: { 'selector(span)': { color: 'green' }}
});

The color of the resulting span will depend on the injection order of foo and bar, which is a kind of non-determinism we specifically try to avoid in aphrodite.

To see why this happens, consider the following:

<div><span><em>Hello</em></span></div>

with CSS

span em { color: green; }
div em { color: red; }

In this case, "Hello" will be red. The selectors have equivalent precedence, so the latter is used. If the CSS is switched to:

div em { color: green; }
span em { color: red; }

Then "Hello" becomes green. This reversal of the declaration order mimics what would happen in aphrodite depending on whether css is first called with styles.foo as an argument, or first called with styles.bar as an argument.

I don't think there's a clear implementation of the proposed selector(a) syntax that doesn't have this problem.

@xymostech
Copy link
Contributor

@jlfwong :( Oof. I guess that probably happens with my pull request implementation, as well?

@ide
Copy link
Contributor

ide commented Jun 7, 2016

Is this kind of non-determinism already an issue with multiple styles on the same component? For example:

<div className={css(styles.s1, styles.s2)} />
// or
<div className={classNames(css(styles.s1), css(styles.s2))} />

const styles = StyleSheet.create({
  s1: { color: 'red' },
  s2: { color: 'green' },
});

My understanding is that Aphrodite will emit two class selectors of the same specificity, so the last one emitted wins. I guess technically it's deterministic but hard to reason about since the order of the css() calls matter. But if this is OK, then there could be an argument that it's OK to have similar behavior for the selector(a) syntax too.

@xymostech
Copy link
Contributor

@ide That is true, but you should always do css(styles.s1, styles.s2). Actually, I think we have that written up somewhere internal but I don't know if we ever say that in the repo. We might want to put that in the README as a caveat.

The point is, adding two classnames is non-determinism that you can avoid (by combining the calls to css()). The non-determinism that @jlfwong is describing cannot be solved by anything that aphrodite does.

@misterfresh
Copy link

const styles = StyleSheet.create({ parent: { ':hover .child': { backgroundColor: blue } } });

This is actually rather intuitive. I'm using it in a project.

@xymostech
Copy link
Contributor

@misterfresh That's fine, but do recognize that it is a hack that's not specifically supported by aphrodite, and might break in future versions.

@ryan-hamblin
Copy link

@xymostech if hey way that @misterfresh is handling child elements is a hack, did the main contributors to this project decide on what will be the recommended way to implement styles on nested children in the future? I can use this now as we only have a beta application, but my boss just gave me the nod on using Aphrodite (AWESOME library btw) and I'll need to know the recommended way to handle this in the future. Thanks.

@patrickml
Copy link

has there been any progress for this? I am trying to select td's in a table table tr td:first-of-type

@sontek
Copy link

sontek commented Apr 3, 2017

Yeah, I want to selected all the anchors in a div (need to style component I don't control), whats the status of this? I see its closed but as far as I can tell there was no resolution?

@jlfwong
Copy link
Collaborator

jlfwong commented Apr 3, 2017

@sontek This was closed due to the solution in #95, which is documented here: https://github.com/Khan/aphrodite#advanced-extensions

@mohsen1
Copy link

mohsen1 commented Jun 4, 2021

This hacky solution might help if you end up in this thread...

.parent {
  --child-background-color: red
}
.parent:hover {
  --child-background-color: blue
}
.child {
  background-color: var(--child-background-color)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.