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

Descendant combinators proposal #229

Closed
rtsao opened this issue Apr 24, 2018 · 6 comments

Comments

Projects
None yet
5 participants
@rtsao
Copy link
Member

commented Apr 24, 2018

I've been thinking how to make a robust implementation of this because it's a fairly common use case.

The proposed idea has a few nice properties:

  • Still no true CSS selectors/class names
  • Clear precedence / style resolution (this is needed because the normal JS method of resolving styles doesn't translate into CSS land)
  • No brittle assumptions about component hierarchy. The correct ancestor/descendant relationship is enforced via the render prop API. (excluding portals, I suppose)

Example

I'm not totally happy with the names of things (still working on it), but hopefully the overall idea makes sense:

import {createAncestor, Descendant} from "styletron-react";

const Foo = styled("div", {color: "green"});

const HoverSource1 = createAncestor(":hover", {
  color: "red"
});

const HoverSource2 = createAncestor(":hover", {
  color: "purple"
});

<HoverSource1 $as="div">
  {target1 => (
    <HoverSource2>
      {target2 => (
        <div>
          <Descendant $as={Foo} $combinators={[target1, target2]} />
        </div>
      )}
    </HoverSource2>
  )}
</HoverSource1>;

Concepts

  • The order of the array passed to Descendant determines the precedence of styling. As an implementation detail, we can iteratively increase the specificity of generated rules to make this happen.
  • Descendent combinator styles always take precedence over "normal styles".

There's still a lot that needs to be ironed out, especially in terms of figuring out all the implementation details, but this is probably the first API that I've liked for this feature.

@aronallen

This comment has been minimized.

Copy link

commented Apr 24, 2018

How would this work for non react users?

@schnerd

This comment has been minimized.

Copy link
Contributor

commented May 23, 2018

One of the use cases in which I've wanted this API is making a set of action buttons appear at the top right of a card when you hover over it, using this API I guess I'd have the following:

const ViewSourceLink = styled("a", {opacity: 0});
const HoverSource = createAncestor(":hover", {
  opacity: 1
});

<HoverSource $as={Card}>
  {target1 => (
    <>
      <CardHeader>
        <Descendant $as={ViewSourceLink} $combinators={[target1]} href="https://github.com..." target="_blank"/>
      </CardHeader>
      <CardBody>
        ...
      </CardBody>
    </>
  )}
</HoverSource>

^ Is that how I would pass props to the ViewSourceLink? Or would I need to wrap ViewSourceLink in some sort of withProps hoc? Can't say I love it either way.

It's admittedly a very clever solution, but I do worry that it's a bit verbose. My personal preference might be something like the following:

const Card = styled('div', {
  `:hover ${ViewSourceLink}`: {
    opacity: 1
  }
});

While this could be a footgun for developers who choose to abuse this feature, I think it's a far simpler API to grok for the few edge cases where descendant combinators are actually needed. That said, I'm not sure how well it would play with styletron's atomic style engine.

@femesq

This comment has been minimized.

Copy link

commented Aug 31, 2018

I was thinking on a way of doing this, but couldn't set-up my prototype project (CRA) to work with my modified version (yarn add /path/to/cloned-styletron/packages/styletron-xxx - any tip on this I appreciate).
My test would try to implement this codepen in this CRA app with the concept below:

const Parent = styled("div", {
  display: 'flex',
  flexDirection: 'row',
  justifyContent: 'space-around',
  height: '50px',
  background: 'grey'
});

const Deep = styled("div", {
  padding: '20px',
  background: 'lightgrey'
});

const Child = styled("div", {
  alignSelf: 'center',
  height: '40px',
  width: '40px'
});

// suppose this generates ".aa" class
const DivA = withStyle(Child, {
  background: 'red'
});

// suppose this generates ".ab" class
const DivB = withStyle(Child, {
  background: 'green'
});

Then, we would have a static method on components created with styled or withStyle, so that we could make this:

/// method parameters:  Parent<Component>, cssSelector<string>, styles<object>
DivA.createDescendant(Parent, ':hover', { background: 'cyan' });

This call would append and h_ba class to Parent's classNames list, and theac class to DivA classNames list, so that we can add the composite style like this:

.h_ba:hover .ac {
  background: cyan;
}

Since it's a simple string concatenation, It would also work for > and + as selectors.

There could be more than one descendant. The browser would take care o rendering the more specific/strict css rule.

Maybe there is something I'm missing (sorry about that), but I hope I can still contribute to the discussion.

Thanks,

@femesq

This comment has been minimized.

Copy link

commented Sep 1, 2018

Have spent some time on this...

Although it was interesting to get deeper and know the code better, I got stuck so that I can't say this approach is possible..

If you guys could take a look and comment it would be great...

Not sure if it's possible to 'reach' the engine outside the <Consume> component... If not, this approach may be inviable or the magic would must happen here, possibly previously creating random class-name to Parent (or, to avoid clashes, create a hash of Parent+Child displayName).

@rtsao

This comment has been minimized.

Copy link
Member Author

commented Dec 18, 2018

After more contemplation, I think unless it is hard requirement that hierarchical hover-based styling works without JS (i.e. pure server-rendering only), a generic JS solution is probably going to be simpler than alternatives.

For example, given a Button that should highlight when the parent Card is hovered:

Hooks

import {useHoverState} from "somewhere-else";

function SomeView() {
  const [targetProps, hoverState] = useHoverState();

  return (
    <Card {...targetProps} size="large">
      <h1>Hover me</h1>
      <Button highlighted={hoverState}>Click me</Button>
    </Card>
  );
}

Render prop equivalent

import {Hover} from "somewhere-else";

function SomeView() {
  return (
    <Hover>
      {({ targetProps, isHovered }) => (
        <Card {...targetProps} size="large">
          <h1>Hover me</h1>
          <Button highlighted={isHovered}>Click me</Button>
        </Card>
      )}
    </Hover>
  );
}

Hoverable component

import {Hoverable} from "somewhere-else";

function SomeView() {
  return (
    <Hoverable as={Card} size="large">
      {isHovered => (
        <>
          <h1>Hover me</h1>
          <Button highlighted={isHovered}>Click me</Button>
        </>
      )}
    </Hoverable>
  );
}

I think these generalized solutions pretty elegantly express the parent/child dependencies and is probably better than something specialized in Styletron, especially when it comes to customization of this sort of logic. General JS/React patterns are more flexible/composable and better suited to the task.

@tajo

This comment has been minimized.

@tajo tajo closed this Mar 20, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.