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

Add CSS Map #1492

Closed
wants to merge 13 commits into from
Closed

Add CSS Map #1492

wants to merge 13 commits into from

Conversation

liamqma
Copy link
Collaborator

@liamqma liamqma commented Aug 10, 2023

Add the support for cssMap

import { css, cssMap } from '@compiled/react';

const styles = cssMap({
  danger: {
      color: 'red',
      backgroundColor: 'red'
  },
  success: {
    color: 'green',
    backgroundColor: 'green'
  }
});

<div css={css(styles[variant])} />;

@atlassian-cla-bot
Copy link

atlassian-cla-bot bot commented Aug 10, 2023

Thank you for your submission! Like many open source projects, we ask that you sign our CLA (Contributor License Agreement) before we can accept your contribution.
If your email is listed below, please ensure that you sign the CLA with the same email address.

The following users still need to sign our CLA:
❌AtlassianRubberDuck

Already signed the CLA? To re-check, try refreshing the page.

@changeset-bot
Copy link

changeset-bot bot commented Aug 10, 2023

⚠️ No Changeset found

Latest commit: 3b6f848

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@liamqma liamqma force-pushed the css-map branch 2 times, most recently from cb0f1c8 to 334d839 Compare August 10, 2023 03:51
packages/babel-plugin/src/utils/css-map.ts Outdated Show resolved Hide resolved
packages/babel-plugin/src/utils/css-map.ts Outdated Show resolved Hide resolved
packages/react/src/css-map/index.ts Show resolved Hide resolved
examples/webpack/src/app.jsx Show resolved Hide resolved
packages/babel-plugin/src/utils/css-map.ts Outdated Show resolved Hide resolved
packages/babel-plugin/src/utils/css-map.ts Outdated Show resolved Hide resolved
packages/babel-plugin/src/utils/css-map.ts Outdated Show resolved Hide resolved
'&&',
t.binaryExpression(
'===',
t.isStringLiteral(objectProperty)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having a bit of trouble understanding this nested ternary expression - could it be made more readable somehow?

e.g. use of parentheses

A ? (B ? C : D) : E

(A ? B : C) ? D : E

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or perhaps by extracting this ternary expression into a separate variable:

const someVariable = A ? B : (C ? D : E);
// ...
return ...
    t.binaryExpression(
        '===',
        someVariable,
        t.stringLiteral(property.key.name)
    )
    ...

packages/babel-plugin/src/utils/css-map.ts Show resolved Hide resolved
JakeLane
JakeLane previously approved these changes Aug 18, 2023
Copy link
Collaborator

@tomgasson tomgasson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few thoughts

@@ -0,0 +1,12 @@
import { css, cssMap } from '@compiled/react';

const styles = cssMap({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Let's simplify this to

import { css } from '@compiled/react'

const styles = css.create({
   ...
});

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having cssMap simplifies types. What's the reason that css.create is preferred?

`);

expect(actual).toInclude(
'<div className={ax(["success"==="danger"&&"_syaz5scu _bfhk5scu","success"==="success"&&"_syazbf54 _bfhkbf54"])}/>'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something weird is happening here. "success"==="danger" can be statically evaluated to false. Where does this comparison even come from though? I've got a feeling we're doing more work than needed with the css() call here, risks having spooky action.

If I do this,

<div css={css(styles.a)} />
<div css={css(styles.b)} />
<div css={css(styles.c)} />
<div css={css(styles.d)} />
<div css={css(styles.e)} />

I shouldn't end up with each case considering all of a,b,c,d and e. I was explicit in each

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, you are right.
But I think the selected variant is runtime variable at most of cases. Statically access to a variant is "by-product". Users probably should use

const a = {
   color: red
}
<div css={css(a)} />

instead of

const styles = {
   a: {
       color: red
   }
}
<div css={css(styles.a)} />

Also, bundler drops the dead code ('apple' === 'orange') && '.aaaabbbb'

'<div className={ax(["success"==="danger"&&"_syaz5scu _bfhk5scu","success"==="success"&&"_syazbf54 _bfhkbf54"])}/>'
);
expect(actual).toInclude(
'<div className={ax([\'danger\'==="danger"&&"_syaz5scu _bfhk5scu",\'danger\'==="success"&&"_syazbf54 _bfhkbf54"])}/>'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quoting inconsistency here

* const Component = ({ borderStyle }) => <div css={css([
* borderStyle === 'none' && { borderStyle: 'none' },
* borderStyle === 'solid' && { borderStyle: 'solid'}
* ])} />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it's outputting an intermediate form. It's preferable for us to consolidate things into a 1-pass compilation rather than having multiple passes, because this is lossy and prevents us from understanding the original context (which is useful for optimisations like dropping the "success"==="danger" case. What's inhibiting us from doing this in the same pass as the css-prop -> className transform?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

um...good point. Let me look into it more.

Copy link
Collaborator Author

@liamqma liamqma Aug 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consolidate things into a 1-pass compilation rather than having multiple passes

It's been an architectural thing.

  • Compiled firstly evaluates user input, and compiles it to expression. e.g. foo ? 'color: red': 'color: green'
  • At later stage, then compiles it to classNames and sheets

I think we can change this two-pass compilation into 1-pass by using local transform, as mentioned from another comment. I will make sure that I will spike this 1-pass compilation before PR is merged.

},
});

export default ({ variant, children }) => <div css={css(styles[variant])}>{children}</div>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just walking my way through this...

  1. Just using javascript to choose the style
const styles = css.create({
    danger: {
       color: 'red'
    },
    success: {
      color: 'green'
    }
});

const map = {
  danger: styles.danger,
  success: styles.success
}

export default ({variant, children}) => <div css={css(map[variant])} >{children}</div>

Can compile to

const styles = {
    danger: {
       color: '<hash_color_red>'
    },
    success: {
      color: '<hash_color_green>'
    }
});

const map = {
  danger: styles.danger, // == {color: <hash_color_red>}
  success: styles.success // == {color: <hash_color_green>}
}

export default ({variant, children}) => <div css={css(map[variant])} >{children}</div>
  1. Consumer passes in a style itself
export default ({ extendedStyle, children }) => <div css={css(extendedStyle)}>{children}</div>;

can compile to

export default ({ extendedStyle, children }) => <div css={css(extendedStyle)}>{children}</div>;

You're combining those two cases together... to form
3.

const styles = css.match({
    danger: {
       color: 'red'
    },
    success: {
      color: 'green'
    }
});
export default ({ variant, children }) => <div css={css(styles[variant])}>{children}</div>;

seems like a nice shortcut to me, but I'm unsure if we need it to be a different and special construct vs what we could make possible with 1

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compiled essentially transforms one CSS rule into three pieces.

  1. classNames
  2. sheets
  3. CSS variables (if there are variables cannot be resolved statically). For simplicity, let's ignore it.

We can conditionally apply classNames to DOM element, but sheets cannot. Hence a special construct help identifying variants (will be useful for lints too), and piece sheets together.

const styles = cssMap({
    danger: {
       color: 'red'
    },
    success: {
      color: 'green'
    }
});
export default ({ variant }) => <div css={css(styles[variant])} />;

gets compiled to

<CS>".a {color: red}", ".b {color: green}"</CS> // sheets need to include all possible CSS outcome
...

I think the approach you mentioned from 1 is more leaning on local tranforms (which is roadmapped 🙂). If we have local transforms:

const foo = css({ 
    color: 'red' 
})

const bar = cssMap({
    danger: {
       color: 'red'
    },
    success: {
      color: 'green'
    }
});

gets compiled to

const foo = {
  className: ".a",
  sheet: ".a{color:red}",
};

const bar = {
    danger: {
         className: ".a",
         sheet: ".a{color:red}",
    },
    success: {
         className: ".b",
         sheet: ".b{color:green}",
    }
}

We can easily use javascript to choose the style

<div css={css([foo, bar[variant]])} />

gets compiled to

<CS>{[foo, bar]}</CS>
<div className={ax(foo.className, bar[variant].className])} />

If I misunderstood the comment, let's have a quick zoom call to clarify it (maybe)?

packages/babel-plugin/src/utils/css-map.ts Outdated Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: moving css-map.test.ts to packages/babel-plugin/src/utils/__tests__/ perhaps

(unless there's a reason to have it in the css-prop directory instead)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to be in packages/babel-plugin/src/css-prop, because they aren't unit tests against the functions of css-map.ts. These tests rely on many things from css-prop.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah no worries 👍

packages/babel-plugin/src/utils/css-map.ts Outdated Show resolved Hide resolved
meta: Metadata,
evaluateExpression: EvaluateExpression
):
| {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: not sure what this extra pipe character does here

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why the eslint plugin doesn't catch this 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

believe it or not. The extra pipe is enforced by prettier. 🤷‍♂️

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL

packages/babel-plugin/src/utils/css-map.ts Outdated Show resolved Hide resolved
Copy link
Collaborator

@dddlr dddlr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just got some nitpicks

@liamqma
Copy link
Collaborator Author

liamqma commented Aug 31, 2023

An alternative implementation has been adopted.

@liamqma liamqma closed this Aug 31, 2023
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 this pull request may close these issues.

4 participants