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

proposal: explicit custom-pseudo-element definition #2713

Closed
idoros opened this issue Sep 13, 2022 · 4 comments
Closed

proposal: explicit custom-pseudo-element definition #2713

idoros opened this issue Sep 13, 2022 · 4 comments
Assignees
Labels
core Processing and transforming logic discussion Ongoing conversation feature New syntax feature or behavior

Comments

@idoros
Copy link
Collaborator

idoros commented Sep 13, 2022

We talked before (internally) about semantic primitives like component, part, theme, and variant to allow better DX. And this new proposal attempts to define the @st-part.

Current state

Stylable does not allow explicit "part" definition, custom-pseudo-element is the result of other definitions:

  • class is a "part" - for every css class there is a public custom-pseudo-element accessible from the stylesheet root.
  • @custom-selector can be used to override a the type and transformation of a custom-pseudo-element derived from a class.

Goal

simplify custom-pseudo-element definition by opting-in to a new "part" syntax:

  • disconnect the automatic "class -> part" relationship
  • path out of how @custom-selector is used by stylable today

Proposal

When using the new @st-part:

  • Prevent the automatic definition of custom-pseudo-element from class definition
  • Register a custom-pseudo-element definition to the root class
    • In the future we might want to allow nested parts definitions to provide a way to describe deep structure API.
  • Make @custom-selector redundant and deprecated by adding it's selector mapping abilities to @st-part (we can revive it to use a native implementation if it will be formal one day)
  • Parts are only build time constructs - not exported to JavaScript runtime
  • Parts define selector access through the custom-pseudo-element style API - not exported by name to other stylesheets

Syntax

@st-part ::<ident> <selector>?

Examples

Implied class

Providing no class defaults to a class selector of the same name

/* define a part that maps to the ".name" class */
@st-part ::name;

::name {}      /* .ns__name */
.root::name {} /* .ns__root .ns__name */

Map selector to to target recursive structures

/* expose a part that maps more strictly to the ".subNode" class */
@st-part ::directNode & > .subNode;
/* expose a part that maps deeply to the ".subNode" class */
@st-part ::subNode .subNode;

.subNode {
    /* every sub node part is a tree node*/
    -st-extends: root;
}

.root::directNode {} /* .treeNode__root > .treeNode__subNode */

Then customize the Tree with the style API

@st-import TreeNode, [subNode] from './tree-node.st.css';

/* style all nodes */
TreeNode {} /* .treeNode__root */

/* style all nodes except the top level*/
.subNode {} /* .treeNode__subNode */

/* style directly nested nodes of an hovered tree node*/
TreeNode:hover::directNode {} /* .treeNode__root:hover > .treeNode__subNode */

/* style all nested nodes of an hovered tree node*/
TreeNode:hover .subNode {}    /* .treeNode__root:hover .treeNode__subNode */
TreeNode:hover::subNode {}    /* .treeNode__root:hover .treeNode__subNode */
TreeNode:hover TreeNode {}    /* .treeNode__root:hover .treeNode__root */

Map to multiple

/* define specific semantic parts  */
@st-part ::cancelBtn;
@st-part ::okBtn;
/* define generic map to target both */
@st-part ::btn :is(.cancelBtn, .okBtn);

/* style specific parts*/
.root::cancelBtn {} /* .prompt__root .prompt__cancelBtn */
.root::okBtn {} /* .prompt__root .prompt__okBtn */

/* style generic parts*/
.root::btn {} /* .prompt__root :is(.prompt__cancelBtn, .prompt__okBtn) */

Open questions

  • feat: Provide a way to set private custom-pseudo-element to be used only within their definition stylesheet?
  • sugar: With explicit exports - automatically export any of the part's selector classes to Javascript runtime to wire the view?
  • sugar: allow atrule body for definitions and styling:

Only class sugar definition

@st-import Button from './button.st.css';

/* define part with mapped class+definition body  */
@st-part ::navBtn {
    /* ".navBtn" class is a Button */
    -st-extends: Button;
}

/* style nav btn */
.root::navBtn {}       /* .ns__root .ns__navBtn */

/* style the infernal button label part of a navBtn */
.root::navBtn::label {} /* .ns__root .ns__navBtn .button__label */

Report error for attempting to define a complex selector:

@st-part ::a .y.z {
    /* error: cannot define "-st-extends" inside a complex selector */
    -st-extends: X;
}

Styling could also be inlined:

@st-part ::a {
    -st-extends: X;
}
@st-part ::b {
    color: green;
}

/* transforms to */
.ns__b {
    color: green;
}
@idoros idoros added feature New syntax feature or behavior discussion Ongoing conversation core Processing and transforming logic labels Sep 13, 2022
@idoros idoros self-assigned this Sep 13, 2022
@barak007
Copy link
Collaborator

barak007 commented Dec 8, 2022

We should also document nested parts and pseudo elements overriding possibilities when combining nested parts and -st-extends

@idoros
Copy link
Collaborator Author

idoros commented Dec 12, 2022

I wrote in the proposal about nested parts:

In the future we might want to allow nested parts definitions to provide a way to describe deep structure API

I think the first iteration of this will not include parts definitions with body that will allow extending, adding states or defining inline styles, but I can definitely see this extending in this direction.

It isn't detailed, but I assume it will follow the same rules as a root extending another definition, just inline. So for example we could have an Accordion component with a header that extends something and defines nested parts that would override definitions from the extended definition:

/* accordion.st.css */
@st-part ::header {
    -st-extends: Box;
    @st-part ::icon {}
    @st-part ::title {}
}

From the outside: in case the Box had an icon, it would be overridden by the Accordion icon, while the Box frame that is not overridden can still be accessed.

@st-import Accordion from './accordion.st.css';

/* style the accordion icon inside the accordion header */
Accordion::header::icon {}

/* style the Box frame of the accordion header */
Accordion::header::frame {}

@idoros
Copy link
Collaborator Author

idoros commented Feb 7, 2023

Looking a bit forwards at issues with the nesting mechanism, we encountered some aspects that needs more clarifications.

Implicit class

When implicitly mapping a part to a class the part inherits the class definition:

@st-part ::navBtn;
.navBtn {
    -st-extends: Button;
}

/* .local__navBtn .button__label */
::navBtn::label {}

/* .local__navBtn .button__label */
.navBtn::label {}

Inherit Sugar

Adding -st-extends under the part is syntactic sugar for setting the class to extend the extending reference.
For example the following code is sugar for ::navBtn pseudo-element mapping to .navBtn class and setting the .navBtn class to extend the Button component.

@st-part ::navBtn {
    -st-extends: Button;
}

/* .local__navBtn .button__label */
::navBtn::label {}

/* .local__navBtn .button__label */
.navBtn::label {}

Nesting Parts

Adding a nested part defines style API to the part under the specific pseudo-element, and overrides the access to a part with identical name if it exist in the inherited extend, but doesn't change the class definition(!):

@st-part ::navBtn {
    -st-extends: Button;
    @st-part ::label;
}

/* .local__navBtn .local__label */
::navBtn::label {}

/* .local__navBtn .button__label */
.navBtn::label {}

The code above overrides the ::label pseudo-element just under the ::navBtn without effecting the .navBtn class.

Notice that we focus on definition of parts, but definition of states would work in the same way.

Multiple Identical Parts and collisions

There are cases where we want to have a part with the same name/class available in multiple placed in the structure:

/* top level nav button that targets all navigation buttons */
@st-part ::navBtn;
@st-part ::header {
    /* match more specific navigation buttons under the header */
    @st-part ::navBtn;
}

/* .local__navBtn */
::navBtn {}

/* .local__header .local__navBtn */
::header::navBtn {}

In this case setting -st-extends in one of the ::navBtn definitions would prevent setting -st-extends in another, or directly inside the class definition, just as -st-extends cannot be set in the same class more then once:

@st-part ::navBtn {
    /* this extend will be used */
    -st-extends: Button;
};
@st-part ::header {
    /* match more specific navigation buttons under the header */
    @st-part ::navBtn {
        /* ERROR: already extended */
        -st-extends: Other;
    }
}
.navBtn {
    /* ERROR: already extended */
    -st-extends: Other;
}

@idoros
Copy link
Collaborator Author

idoros commented Apr 4, 2023

closing in favor of #2848

@idoros idoros closed this as completed Apr 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core Processing and transforming logic discussion Ongoing conversation feature New syntax feature or behavior
Projects
Status: ⏸️ Paused
Development

No branches or pull requests

2 participants