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

Passing style to child component without using global styles #273

Closed
pruhstal opened this issue Aug 7, 2017 · 12 comments
Closed

Passing style to child component without using global styles #273

pruhstal opened this issue Aug 7, 2017 · 12 comments

Comments

@pruhstal
Copy link

pruhstal commented Aug 7, 2017

I've got a child component, let's call it Link.

This is the code for that:

import React from 'react';
import Link from 'next/link';

export default class CustomLink extends React.Component {
  render() {
    if (this.props.onClick) {
      return (
        <span className={this.props.className} onClick={this.props.onClick}>
          {this.props.children}
        </span>
      );
    }

    return (
      <Link
        href={this.props.href}
        params={this.props.params}
        as={this.props.as}>
        <a className={this.props.className}>{this.props.children}</a>
      </Link>
    );
  }
}

Now say that I import that component within another component, called Header

The code for that is:

import Link from './Link';

export default class Header extends React.Component {
  render() {
    return (
      <div className="root">
        <style jsx>{`
          .link {
            background-color: #fff;
            border-bottom: 1px solid #eee;
            color: #222;
          }
          `}
        </style>
       <Link className="link">Click me</Link>
    );
  }
}

However, unless I use <style jsx global>, then unfortunately the style doesn't get passed to the child component.

I think a related issue is #197, but I haven't seen any outcome or update on that issue since June.

Is there a solution to fixing this issue?

@pruhstal
Copy link
Author

pruhstal commented Aug 7, 2017

So it seems if I do this:

          .root .link {
            display: inline-block;
            border-bottom: 2px solid transparent;
            padding: 0 15px;
            text-decoration: none;
            text-transform: uppercase;
          }

          .root .link,
          .root .link:active,
          .root .link:visited {
            color: #222;
          }

          .root .link.active,
          .root .link:hover {
            border-bottom: 2px solid #222;
            color: #222;
          }

It seems to work.

Alternatively I can also do:

          :global(.link) {
            display: inline-block;
            border-bottom: 2px solid transparent;
            padding: 0 15px;
            text-decoration: none;
            text-transform: uppercase;
          }

          :global(.link),
          :global(.link:active),
          :global(.link:visited) {
            color: #222;
          }

          :global(.link.active),
          :global(.link:hover) {
            border-bottom: 2px solid #222;
            color: #222;
          }

from the parent component's styles.

What is the right thing to do here?

@colinmeinke
Copy link

I think the the modifier/variant styles should be applied to the Link component, not the component in which they are used.

import React from 'react';
import Link from 'next/link';

export default class CustomLink extends React.Component {
  render() {
    if (this.props.onClick) {
      return (
        <span className={this.props.className} onClick={this.props.onClick}>
          {this.props.children}
        </span>
      );
    }

    return (
      <Link
        href={this.props.href}
        params={this.props.params}
        as={this.props.as}>
        <a className={this.props.className}>{this.props.children}</a>
        <style jsx>{`
          .link {
            background-color: #fff;
            border-bottom: 1px solid #eee;
            color: #222;
          }
        `}</style>
      </Link>
    );
  }
}

use:

import Link from './Link';

export default class Header extends React.Component {
  render() {
    return (
      <div>
        <Link className="link">Click me</Link>
      </div>
    );
  }
}

@pruhstal
Copy link
Author

pruhstal commented Sep 5, 2017

@colinmeinke that doesn't really work well, because what if Link comes from some npm module (e.g. a UI library), and I don't have control over it?

That's the use case where this becomes a real problem.

@giuseppeg
Copy link
Collaborator

what if Link comes from some npm module (e.g. a UI library), and I don't have control over it?

That's the main point, we'd have to figure out a way to control styles from the parent because passing a prop (eg. className) is an half baked solution that won't work all the times.

As for now the best way to do this is to use .someSelectorInTheParentComponent > :global() from within the parent component eg.

<div className="root">
    <Link />
    <style jsx>{`
        .root > :global(a) { font-size: 60px }
     `}</style>
</div>

@a-ignatov-parc
Copy link
Contributor

I've found a better workaround for this issue.

function HighlightedLink(props) {
  const {
    theme,
    children,
    ...otherProps,
  } = props;

  /**
   * `scope` element is needed to properly parse `style` element
   * and it could be any DOM element you want.
   */
  const scope = resolveScopedStyles((
    <scope>
      <style jsx>{`
        .link {
          background: ${theme.background};
        }
      `}</style>
    </scope>
  ));

  return (
    <Link
      {...otherProps}
      className={scope.wrapClassNames('link')}
    >
      {children}
      <scope.styles />
    </Link>
  );
}

function resolveScopedStyles(scope) {
  return {
    className: scope.props.className,
    styles: () => scope.props.children,
    wrapClassNames: (...classNames) => [scope.props.className, ...classNames].filter(Boolean).join(' '),
  };
}

@giuseppeg
Copy link
Collaborator

@a-ignatov-parc it might add some runtime overhead but it is genius! :D

@a-ignatov-parc
Copy link
Contributor

a-ignatov-parc commented Dec 13, 2017

might add some runtime overhead

Yeah. But usually you shouldn't use child combinator selectors. Child component's markup is its private area and may change at any time.

@giuseppeg
Copy link
Collaborator

But usually you shouldn't use child combinator selectors

Agreed, unfortunately that's the only way to style 3rd parties components that don't accept a className.

By the way styles could be just scope.props.children and used like this {scope.styles}. You could expose Styles: () => scope.props.children and styles: scope.props.children :)

I've been thinking to implement automatic className propagation for components and support this syntax:

<Link className="link">
  {children}
  <style jsx>{`
     .link { background: ${theme.background} }
  `}</style>
</Link>

Although I like the explicitness of your solution since on can do anything with the className and styles.
Today I am giving a talk about styled-jsx if it is ok with you I would like to use your example (with credits) in my presentation.

@a-ignatov-parc
Copy link
Contributor

You could expose Styles: () => scope.props.children and styles: scope.props.children

Sounds good 👍

I would like to use your example (with credits) in my presentation.

Sure 😉

@a-ignatov-parc
Copy link
Contributor

Agreed, unfortunately that's the only way to style 3rd parties components that don't accept a className.

True. But it's better to choose another component/lib than apply such implicit styling. No one likes magic that can break your UI.

@giuseppeg
Copy link
Collaborator

Added @a-ignatov-parc workaround to the docs:

https://github.com/zeit/styled-jsx#styling-third-parties--child-components-from-the-parent

@addlistener
Copy link

My typed version

declare global {
  // eslint-disable-next-line @typescript-eslint/no-namespace
  namespace JSX {
    interface IntrinsicElements {
      scope: React.DetailedHTMLProps<
        React.StyleHTMLAttributes<HTMLElement>,
        HTMLElement
      >;
    }
  }
}

export function resolveScopedStyles(scope: ReactElement) {
  const className = scope.props.className as string;
  return {
    className,
    styles: () => scope.props.children,
    wrapClassNames: (...classNames: string[]) =>
      [className, ...classNames].filter(Boolean).join(" "),
  };
}

I've found a better workaround for this issue.

function HighlightedLink(props) {
  const {
    theme,
    children,
    ...otherProps,
  } = props;

  /**
   * `scope` element is needed to properly parse `style` element
   * and it could be any DOM element you want.
   */
  const scope = resolveScopedStyles((
    <scope>
      <style jsx>{`
        .link {
          background: ${theme.background};
        }
      `}</style>
    </scope>
  ));

  return (
    <Link
      {...otherProps}
      className={scope.wrapClassNames('link')}
    >
      {children}
      <scope.styles />
    </Link>
  );
}

function resolveScopedStyles(scope) {
  return {
    className: scope.props.className,
    styles: () => scope.props.children,
    wrapClassNames: (...classNames) => [scope.props.className, ...classNames].filter(Boolean).join(' '),
  };
}

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

No branches or pull requests

5 participants