Giter VIP home page Giter VIP logo

css-blocks's Introduction

CSS Blocks

Blazing fast CSS for your Design Systems and App Components

Build Status Greenkeeper badge


CSS Blocks is an ergonomic, component-oriented CSS authoring system that compiles to high-performance stylesheets.

By combining an opinionated authoring system, build-time analysis and rewriting of templates, and a new type of CSS optimizer, css-blocks breathes new power and ease of use into the technologies and best practices that stylesheet developers already know and love.

Interested in contributing, or just seeing CSS Blocks in action? Head over to CONTRIBUTING.md to learn how to spin up the project!

Table of Contents

Why CSS Blocks?

With css-blocks added to your project, you receive:

  • 💎 One CSS File Per Component
  • 📦 Scoped Styles
  • 🔎 Nearly Non-Existent Runtime (~500b)
  • 🔥 Blazing Fast Stylesheets
  • 🚀 Project-Wide Optimization
  • 🚨 Build Time CSS Errors
  • 🧟 Dead Code Elimination
  • ✨ Object Oriented Inheritance

But, most importantly, CSS Blocks is ⚡️Statically Analyzable.

The ⚡️ of Static Analysis

Static analysis means css-blocks can look at your project and know with certainty that any given CSS declaration will, will not, or might under certain conditions, be used on any given element in your templates.

Most stylesheet architectures have to walk a fine line between performance and maintainability. Tilt too far in either direction and either your users or the developers will end up paying the cost. With CSS Blocks, you can focus on making sure your stylesheets are easy to maintain as your application changes, and with the new CSS optimizer, OptiCSS, the small size of your app's production stylesheets after compression will amaze you.

Gone are the days where you spend several minutes debugging your app only to discover a subtle typo that caused a selector to not match – CSS Blocks will give you a build error and suggest possible fixes. With IDE integration, projects using CSS Blocks will be able to quickly navigate to selector definitions that match your current template element and find which template elements match your current selector, autocomplete class names. With CSS Blocks new resolution system, cascade conflicts will be caught for you before you even know they exist and you will never have to fight a specificity war ever again.

CSS Blocks Example

CSS Blocks is inspired by CSS Modules, BEM and Atomic CSS

For a full deep-dive of the project architecture, I heavily recommend you review the CSS Blocks Architecture README!

⚙️ Supported Integrations

CSS Blocks requires deep integration with your build system and templating language. To learn how to install css-blocks for in your application, please consult the specific docs for your templating system and build system.

CSS Blocks is available for use in the following templating languages:

And has integrations with the following build systems:

Don't see your preferred platform yet?

Learn how to make your own Template Integration or Build System Integration and contribute it back!

🎁 API Features

CSS Blocks is under active development and there are a number of features that have not yet been implemented! You can get a snapshot of the feature-set state here.

✅ = Implemented | ❌ = Not Implemented | 💀 = Deprecated | 🖌 = In Proposal |

Status Feature Description
Selectors
:scope Scope selector for component root.
.class Class selectors for component sub-elements.
.class[name] State that is applied to scope and class selectors on state existence.
.class[name="value"] Mutually exclusive sub-states for scope and class selectors to be applied when a sub-state value matches.
[name=value] Bare state (not associated with an Originating Element) and optional substate selectors for targeting all elements in the Block that possess the state and/or sub-state.
🖌 .class[name=value default] Default state value to be applied when there is no other match.
At Rules
@block local-name from "./file/path.css" Reference another Block using a local name.
@block-debug block-name to channel Debug call that will print a block interface to a "channel": comment, stderr, or stdout.
@block-global block.path Declare a Block class or state as public. It may be used as a context selector in other Blocks.
Properties
block-name: "custom-name"; Provide custom Block names in :scope for a nicer debugging experience.
implements: block-name; A Block can declare that it implements one or more other Block's interfaces in its :scope selector and the compiler will ensure that all of those states and classes are styled locally.
extends: block-name; A Block may specify it extends another Block in its :scope selector to inherit and extend all the class and state implementations therein.
composes: "block.path"; Mixin-Style class and state composition. Apply other Blocks' Styles to one of yours.
Functions
resolve("block.path"); Provide an explicit resolution for a given property against another Block.
constrain(val1, val2 ... valN); Constrain this property to a list of specific values that may be set when this Block is extended.
range(min, max); Constrain this property to a range of values that may be set when this Block is extended.

🎨 What is a Block?

A "Block" is an isolated stylesheet, written in its own file, that contains all rulesets for any elements, and their various modes and interaction states, for a discrete unit of styling – like a component or design pattern.

Typically, a single Block will contain styles for a particular component or concept, but it is entirely natural – and encouraged – for a template to consume multiple blocks and compose them together in the markup.

A Block file may contain:

The Scope Selector

The scope ruleset contains styles applied to the root of the scoped style subtree. All other elements assigned styles from a Block must be contained in the document subtree of an element assigned to the block's :scope. We use the special :scope pseudo-class to represent these styles.

The :scope selector may contain the special block-name property so you may provide your own Block name for easy debugging and BEM class generation. If no block-name is provided, we will infer the Block name from the file name.

💡 Feature Note: Block Names

If two Blocks in your project have the same name, CSS Blocks will automatically generate a unique, but still human-readable, name for BEM output mode.

:scope {
  block-name: custom-block-name;
  /* 👆 optional! */
  /* ... more styles ... */
}

Class Selectors

Blocks may can contain other classes that may be applied to elements inside the scoped style sub-tree. These are just class selectors, but they are local to that Block and isolated from all other similarly named classes in other Blocks.

.sub-element { /* ... */ }
.other-sub-element { /* ... */ }

Together, the :scope selector and all declared .class selectors define the full interface of stylable elements available to a Block's consumer.

State Selectors

States represent a mode or interaction state that the :scope or a class – called the state's originating element – may be in. States are written as attribute selectors with the special state namespace.

:scope { /* ... */ }
:scope[enabled] { /* ... */ }

.sub-element { /* ... */ }
.sub-element[is-active] { /* ... */ }

⁉️ What the pipe is going on here?

Once upon a time, developers fell in love with XML and thus was born xhtml, a flavor of HTML that allowed HTML elements to be mixed together with elements from other XML syntaxes like SVG and MathML. CSS went along for the ride and so, while many have never seen or used the feature, CSS has support for namespaced elements and attributes. In CSS, the | symbol is used to delimit between a namespace identifier (assigned by the @namespace at-rule) and the element or attribute name (also called a qualified name).

In markup, instead of a pipe symbol, the colon is used to delimit a namespace identifier and a qualified name. Yes, this is confusing -- but we don't make CSS syntax, we just use it.

Sub-State Selectors

States on the :scope selector or a class selector may contain sub-states for more granular styling. Sub-states of a State are mutually exclusive and an element may only be in one sub-state of that state at any given time.

:scope { /* ... */ }
:scope[theme="inverse"] { /* ... */ }

.sub-element { /* ... */ }

/* Applied for *any* value of `color`, including no value. */
.sub-element[color] { /* ... */ }

/* Applied for *specific* values of `color */
.sub-element[color="red"] { /* ... */ }
.sub-element[color="blue"] { /* ... */ }
.sub-element[color="yellow"] { /* ... */ }

Its Just CSS!™️ (mostly)

CSS Blocks implements a strict subset of CSS. This means we've intentionally restricted some of the features you're allowed to use in a Block file to ensure we can optimize your stylesheets as much as possible!

As Opticss improves, we may choose to loosen some of these restrictions – keep an eye out for syntax updates as we approach the v1.0.0 release!

🎉 That means you may freely use:

🚨 However:

  • !important is forbidden – you won't be needing it!
  • The tag, non-state [attribute], #id and * selectors are forbidden (for now!)
  • The Logical Combinators :matches(), :not(), :something() and :has() are forbidden (for now!)
  • Selectors must remain shallow.

In css-blocks, shallow selectors mean:

1) Only one combinator per selector.

/* ✅ Allowed! */
:scope:hover > .my-class { /* ... */ }

/* ❌ Illegal! */
:scope:hover > .my-class + .my-class { /* ... */ }

2) The Hierarchical Combinators' (" " and ">") context selector must be a :scope states, sub-states, or pseudo-classes.

/* ✅ Allowed! */
:scope:hover .my-class { /* ... */ }
:scope[active] > .my-class { /* ... */ }
:scope[color=red] .my-class { /* ... */ }

/* ❌ Illegal! */
.container:hover > .my-class { /* ... */ }
.container[active] .my-class { /* ... */ }
.container[color=red] .my-class { /* ... */ }

3) The Sibling Combinators' ("+", "~") context selector must target the same class or :scope used in the key selector.

/* ✅ Allowed! */
.my-class + .my-class { /* ... */ }
.my-class:hover ~ .my-class { /* ... */ }
.my-class[active] + .my-class { /* ... */ }

/* ❌ Illegal! */
:scope + .my-class { /* ... */ }
.another-class:hover ~ .my-class { /* ... */ }
.another-class[active] + .my-class { /* ... */ }

💡 Feature Note: Global States and Selectors

"Global States" have their own rules on how they can be used in Block selectors! Keep an eye out for them a little later in this doc.

Of course, because we statically analyze and compile all your code before it ever hits the browser, you will get a helpful error if any of these syntax restrictions are violated.

Blocks in Your Templates

Every specific template integration with css-blocks will have their own slightly unique syntax for how to interface with Block files. However, they all will allow you to apply classes and states in a way that is analyzable and rewritable at build time.

💡 Feature Note: Template Integrations

Each integration implements a slightly different API. Ex: JSX lets you import Block files, Ember looks for, through convention, a stylesheet.css alongside every component template. Check out the README for your template language for full details.

However, whatever the implementation is, it will feel as though you're interfacing with regular CSS on the platform. For example, in Glimmer you just write the classes and states exactly as you would expect when working with a normal stylesheet:

:scope { /* ... */ }
:scope[enabled] { /* ... */ }
.button { /* ... */ }
.icon { /* ... */ }
.icon[inverse] { /* ... */ }
{{!-- :scope selector is automagically applied to the template's root-level element. Thanks Glimmer! --}}
<section block:enabled={{isEnabled}}>
  <button block:class="button">
    <div block:class="icon" block:inverse={{isInverse}}></div>
    {{value}}
  </button>
</section>

There are only two (2) common-sense rules to follow when using Block styles in your template:

  1. You may not use a Block class outside of it's :scope's subtree.
  2. Two classes from the same Block may not be applied to the same HTML element.

🏗 Block Composition

Blocks styles are, by design, scoped to the file they are written in, but we all know that in a real app your styles can't live in a vacuum!

As you'll see below, there are many methods to compose blocks together in your application. However, most of these methods will begin with the humble @block.

Block References

A Block may declare a dependency on another Block by using a @block at the top of your file. A @block creates a locally scoped alias where you can access the public API (declared classes and states) of the referenced block.

Block references don't cause any styles to be included. Instead, they are like an ES6 import statement -- they make it possible to refer to the public interface of another Block from within the current Block.

Adding a @block is as simple as this:

/* block-1.block.css */
:scope { block-name: block-1; }
.my-class { /* ... */ }
.my-class[my-state] { /* ... */ }
/* block-2.block.css */
@block other-block from "./block-1.block.css";

:scope { block-name: block-2; }

🔮 Future Feature: Node Modules Block Resolution

Whether you're integrating with a 3rd party library, or pulling in dependencies internal to your company, at some point you'll want to integrate with styles delivered via NPM! The resolution logic for @blocks to node_modules hasn't yet been implemented yet, but you can track progress (or even help out!) over on Github.

With the above code, block-2 now has a local reference other-block which points to block-1. We can now freely use the other-block identifier inside of block-2 when we want to reference reference block-1. This comes in handy! Especially with features like:

Object Oriented Features of Blocks

Block Implementation

A Block's public interface is defined by the states and classes it styles. A block may declare that it implements one or more other referenced blocks' interfaces, and the compiler will ensure that all the states and classes it defines are also in the implementing block. In this way, the compiler can guarantee it is safe to use different blocks to style the same markup in a component.

You do this via the special implements property in a Block's :scope selector:

/* block-1.block.css */
:scope { block-name: block-1; }
.my-class { /* ... */ }
.my-class[my-state] { /* ... */ }
/* block-2.block.css */
@block other-block from "./block-1.block.css";

:scope {
  block-name: block-2;
  implements: other-block;
}

💡 Feature Note: Implements Property

The implements property is only available in the :scope selector. If you use it in any other selector, it will be ignored.

However, the above code will throw an error at build time!

$ Error: Missing implementations for .my-class, .my-class[my-state] from ./block-1.block.css

For the build to pass, we need to implement the full public interface of block-1 in block-2:

/* block-2.block.css */
@block other-block from "./block-1.block.css";

:scope {
  block-name: block-2;
  implements: other-block;
}
.my-class { /* ... */ }
.my-class[my-state] { /* ... */ }

Block Inheritance

A Block may also choose to extend another referenced Block. This exposes all declared styles from the extended Block on the extending Block.

Those inherited styles may then be used in a template by accessing them on the extending block, and can even be augmented by re-declaring the styles in the extending block!

You do this via the special extends property in a Block's :scope selector.

Lets say we have a component called <basic-form>. Basic forms have an input element, and a big green button. Simple enough:

/* basic-form.block.css */
.button {
  font-size: 1.4rem;
  color: white;
  background-color: green;
}
.button[disabled] {
  color: #333;
  background-color: lightgray;
}
.input { font-weight: bold }

But, as the project evolves we realize we need a new form for submitting information for a dangerous action, we're asked to create a new kind of form called <danger-form>. Danger forms look and function exactly the same as a basic form, except the button and labels are red. We could re-implement the entire stylesheet to create <danger-form>, but that would be a such a waste of all the hard work we already put in to <basic-form>!

Instead, we can simply extend the <basic-form> Block, and only apply the small style changes we need:

/* danger-form.block.css */
@block basic-form from "./basic-form.block.css";

:scope  { extends: basic-form; }
.button { background-color: darkred; }
.label  { color: darkred; }

During rewrite, references to an inherited style will translate into the class(es) for the directly referenced style as well as all the classes that it inherits from so developers do not need to bring the complexity of the inheritance relationship into their templates. For example, a reference to danger-form.button would result in adding both .basic-form__button, as well as .danger-form__button to the element's list of classes.

When the blocks are compiled, property overrides are detected and automatically resolved. The selectors generated serve two purposes:

  1. Concatenation order independence - Once compiled, a CSS block file can be concatenated in any order with other compiled block output.
  2. Optimization hints - Normally, if there are selectors with the same specificity that set same property to different values on the same element, the optimizer would take care not to merge those declarations such that it might cause a cascade resolution change. But the selectors in the output from CSS Blocks allows OptiCSS to merge declarations more aggressively, because it can prove that it knows the value of those selectors when combined.
.basic-form__button { font-size: 1.4rem; color: white; background-color: green; }
.basic-form__button--disabled { color: #333; background-color: lightgray; }
.basic-form__input { font-weight: bold; }
.danger-form__button { background-color: darkred; }
.basic-form__button.danger-form__button { background-color: darkred; }
.danger-form__button--disabled { background-color: #957d7d; }
.basic-form__button.danger-form__button--disabled { background-color: #957d7d; }
.basic-form__button--disabled.danger-form__button--disabled { background-color: #957d7d; }
.danger-form__label { color: darkred; }

While this output is highly repetitive and may seem excessive, it's exactly the kind of repetition that OptiCSS is designed to search for and remove. From an authoring experience and in production, it's a laser-focused override with no performance impact.

💡 Feature Note: Extends Property

The extends property is only available in the :scope selector. If you use it in any other selector, it will be ignored.

An extending block is able to re-define any property on any style it inherits from. CSS declarations defined in the extending Block will always take priority over the definitions inherited by the same named Style in the base Block.

🔮 Future Feature: Extension Constraints

Sometimes, properties inside of a component are so important, that authors may want to constrain the values that extenders and implementors are able to set. In the near future, css-blocks will enable this use case through the custom constrain() and range() CSS functions and possibly through other ideas like custom constraints and conflicts. You can come help out over on Github to make this happen faster!

Style Composition

Block Paths

As your Blocks begin interacting with each other in increasingly complex ways, you will find yourself needing to reference specific classes or states on another Block, as you'll see later in this document. You do this using a small query syntax called a Block Path.

Block Paths take the form:

block.class[name='value']

All sections of this selector – except the leading Block name – are optional. The leading Block name must refer to an imported @block at the top of the file. If css-blocks is unable to resolve a Block Path at build time, you will get a friendly error message in your console!

All the following syntaxes are legal to select any given stylable on a referenced Block:

Stylable Syntax
Scope block
Scope State block[name]
Scope Sub-State block[name=value]
Class block.class
Class State block.class[name]
Class Sub-State block.class[name=value]

🔮 Future Feature: Block Path Wildcards

In some situations, you may want to select multiple classes, states or sub-states on a referenced block. In the near future you will be able to do so with a wildcard syntax: block.*, block.class[*], block.class[name=*]. Feel free to track progress of this feature here

Composition in Templates

Every template integration will provide a way to use more than one Block inside of a template. The syntax for this may change depending on your templating system, so please check with your specific template integration's documentation.

For Glimmer, using multiple blocks in a single template will look something like this:

/* hoverable.css */
:scope {
  block-name: hoverable;
  box-shadow: 0 2px 3px rgba(0, 0, 0, 0.2);
  transition: box-shadow .28s;
}
:scope:hover {
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2)
}
.button {
  background-color: rgba(255, 255, 255, .5);
  color: black;
  transition: background-color .28s;
}
:scope:hover .button {
  background-color: rgba(255, 255, 255, 1);
}
/* stylesheet.css */
@block other from "./hoverable.css";

:scope { block-name: main; }
.form {
  border: 1px solid gray;
  border-radius: 2px;
  padding: 16px;
}
.button {
  background-color: green;
  color: white;
  height: 32px;
  width: 100%;
}
{{!-- :scope selector from `stylesheet.css` is automagically applied to the template's wrapper element. Thanks Glimmer! --}}
<section>
  <form class="form other">
    <button class="button other.button">Click Me!</button>
  </form>
</section>

Above we have a simple template that contains a form with a single button that says "Click Me!". We style it with styles from the default Block for the template, stylesheet.css, and with styles from the referenced Block hoverable.css, referenced in this context as other.

In this template, we have the <form> element assigned the scoped root for other, and we apply the button class from both blocks to the <button> element.

But wait! If you try and run the css-blocks build with this code, you'd find an error in your console!

The following property conflicts must be resolved for these co-located Styles: (template.hbs:4:19)

          color:
            main.button (stylesheet.css:12:2)
            hoverable.button (hoverable.css:12:2)

          background-color:
            main.button (stylesheet.css:11:2)
            hoverable.button (hoverable.css:11:2)

Woah, what does this mean?! Well, if we stop and think for a second about what we just asked css-blocks to do, we'll realize that this error makes perfect sense.

Because Blocks have their own entirely independently scoped cascades, and right now no Block file is aware of any other Block's styles, css-blocks doesn't inherently know which Block should take priority over another when used together.

So, when css-blocks was asked to put the .button class from both the default template stylesheet, and from hoverable onto the same element, it noticed that both classes are claiming to set the color and background-color properties – and with this we have encountered our first Indeterminate Cascade Resolution.

Which Block should win in this situation? Right now, the compiler has no idea. To answer that, css-blocks needs a little help from you. We are able to provide explicit cross-Block cascade resolutions to the compiler by using resolve(). Lets learn how we can fix our above error by moving on to the next section: Block Resolutions.

Block Resolutions

The special resolve() function provides explicit resolution guidance for properties that are in conflict across two or more Block files. They look like any other property declaration:

selector {
  property-name: resolve("<block-path>");
}

You will be asked by the css-blocks compiler to add resolutions if and when two styles are found on the same element in your templates that attempt to set the same CSS property.

Resolve declarations work just like progressive enhancement and graceful degradation! The last declaration defined in the ruleset will win. This means that declaration order matters. There are two ways to resolve any given property:

Override Resolution

Override resolutions tell css-blocks that when these two styles are used together, we want this Block to override the value of the other Style's property.

Here, we tell css-blocks to use the color value from my-class instead of other.selector when both styles are applied to the same element:

.my-class {
  color: resolve("other.selector");
  color: red;
}

Yield Resolution

Yield resolutions tell css-blocks that when these two styles are used together, we want this Block to yield to the value of the other Style's property.

Here, we tell css-blocks to use the color value from other.selector instead of my-selector when both styles are applied to the same element:

.my-class {
  color: red;
  color: resolve("other.selector");
}

🔮 Future Feature: Resolve All Shorthand

For straightforward resolutions where you just want to yield or assume full control of styling against another block, feel free to use the CSS all property to quickly override or yield to all property conflict with another block. The downside of doing this is that as new properties are added to another element, you don't get a chance to review them and decide:

.my-class {
  color: red;
  background: blue;

  /* Yields all conflicts to `other.selector` */
  all: resolve("other.selector");
}

💡 Feature Note: Advanced Property Conflicts

The css-blocks compiler is smart! If you have dynamic classes or states in your template, it will ask you to provide explicit resolutions between Blocks that even only have a chance of being used together on the same element. This way, we can guarantee that your styles will work regardless of the state your application may find itself it.

Css Blocks is also aware of CSS shorthands and will ask you to resolve the lowest common denominator on conflicting shorthand/longhand expressions as well.

So, continuing with the example from the previous section – Composition in Templates – we can satisfy the css-blocks compiler by adding in two explicit resolutions for color and background-color like so:

/* stylesheet.css */
/* ... */

.button {
  /* Override Resolution */
  background-color: resolve("hoverable.button");
  background-color: green;

  /* Override Resolution */
  color: resolve("hoverable.button");
  color: white;

  /* ... */
}

Here we have told css-blocks that when our component's .button class is used with hoverable's .button class, we want our component's style declarations to win! We have declared an override resolution for both properties.

If we were to switch around the order a bit so our background-color resolution comes after our component's declaration, it means that when these two classes are used together, hoverable's .button class will win, but only for that property. This is why you will never have to fight the cascade or use !important ever again!

/* stylesheet.css */
/* ... */

.button {
  /* Yield Resolution */
  background-color: green;
  background-color: resolve("hoverable.button");

  /* Override Resolution */
  color: resolve("hoverable.button");
  color: white;

  /* ... */
}

💡 Feature Note: States and Pseudo-Classes

States and Pseudo-Classes inherit all resolutions set on their containing Class or :scope.

This means that in the above example, where we yield for background-color, and override for color, the button element where both classes are used will still use hoverable.button:hover's background-color, but it's color will remain white, like our component styles define!

Resolving Pseudo Elements

It is important to note that Pseudo-Elements do not inherit any resolutions from their container class and must be explicitly resolved in the source stylesheets when found to be in conflict.

So, for the following two Blocks where my-class-1[enabled] and my-class-2 are used on the same element, one of the Blocks will need to resolve the conflicting border-width property:

/* other */

.my-class-1[enabled]::before {
  border: 1px solid red;
}
/* main.css */

@block other from "./other.css";

.my-class-2::before {
  border-width: 2px;
  border-width: resolve("other.my-class-2[enabled]");
}

External Selectors

Sometime a class, identifier, or tag name comes from an external source, and the only thing you can do is use them as is. In these situations the Block must declare all external simple selectors it intendeds to use. These simple selectors may then be used as key selectors inside this Block. You'll get an error for any declared external selectors that aren't used or if they are used in the context selector.

Styles targeting an external selector are not rewritten and their declarations cannot be optimized! Style collisions on an external selector are not detected or resolved. As a result, it is allowed to use !important on declarations targeting an external selector.

Warning: If external selectors and CSS block objects both target the same HTML element in their key selectors you will get unpredictable results. It's best to avoid this.

@external h2.some-rando-class;

.foo h2.some-rando-class {
  font-size: 32px !important;
}

Global States

In rare occasions, a Block may choose to declare declare that a certain State is global. These states are special in that they can be used in other Blocks like they are local to that block.

This is most useful for global application states – like during initial application boot, or when a modal is displayed.

⚙️ Performance Note: Global States

When you apply classes and other attributes to elements like <html> or <body> it invalidates a lot of internal caches in the browser. It is still often a performance win compared to querying the document in javascript and applying classes on many elements.

/* application.block.css */

@block-global [is-loading];
@block-global [is-saving];
/* navigation.block.css */

@block app from "application.block.css";

/* Gray out signout button when app is saving */
:scope[app|is-saving] .signout {
  color: gray;
  pointer-events: none;
}

/* Animate the logo when app is loading data */
:scope[app|is-loading] .logo {
  animation-name: bounce;
}

css-blocks's People

Contributors

alonski avatar amiller-gh avatar bitttttten avatar brianchung808 avatar chriseppstein avatar chrisrng avatar danielruf avatar elnee avatar fabiomcosta avatar flawyte avatar forsakenharmony avatar greenkeeper[bot] avatar jonathantneal avatar josemarluedke avatar kgrz avatar knownasilya avatar liamross avatar mike-north avatar nickiaconis avatar ramitha avatar sandiiarov avatar snyk-bot avatar styfle avatar timlindvall avatar tomdale avatar wodin avatar zephraph avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

css-blocks's Issues

Glimmer Template Analyzer & Validator

Template Analyzer and Validator:

  1. Analyze templates against their corresponding css blocks.
  2. Validate that the classes and states referenced are defined in the block and used legally.
  3. Produce a block-usage.json file:
{
  "template": "path/to/my-component/template.hbs",
  "blocks": {
    "": "path/to/my-component/styles.block.css",
    "ref": "path/to/shared/reference.block.css"
  },
  "stylesFound": [
     "ref.root",
     "ref[state:b]",
     ".root",
     "[state|a]",
     "[state|foo=bar]",
     "ref.link",
     ".foo",
     ".foo[state|asdf]",
     "ref.quux",
     ".class1",
     "ref.ref-class"
  ],
  "styleCorrelations": [
    ["ref.root", "ref[state:b]"],
    [".root", "[state|a]", "[state|foo=bar]", "ref.link"],
    [".foo", ".foo[state|asdf]", "ref.quux"]
  ]
}

Thoughts:

  1. we probably want to produce one block-usage.json per template instead of one big file so that we can only update the data when the templates are changed.
  2. This will use the block parser for validation, but no rewriting occurs at this phase.
  3. This should happen before block compilation, resolution conflicts will be errors from the block compiler and we can use the correlations to guide actual resolutions that are created so that the optimizer has less work to do (because naive resolutions generate more selectors than are strictly necessary for the markup used).

Improve Correlation Data Model Efficiency

Currently, we enumerate and store every possible correlation of BlockObjects. This is expensive in both time and space and can be avoided by instead recording calls to addStyle and addExclusiveStyle, putting off permutation until the latest possible time.

TypeScript type checking for css-blocks as imported modules.

Each CSS block file can be represented as a collection of TypeScript classes.

.root { block-name: a-css-block; }
[state|a-block-state] {}
.a-thing {}
.a-thing[state|active] {}
.a-thing[state|my-theme=red] {}
.a-thing[state|my-theme=blue] {}
// these would be defined statically for all blocks and imported
type OptimizedClassNames = string[];
export interface BlockClass {
}
export type BooleanBlockState = () => OptimizedClassNames;
export type ExclusiveBlockState = (value: string) => OptimizedClassNames;
export type BlockState = BooleanBlockState | ExclusiveBlockState;
// type for this block
export class ACssBlock implements BlockClass {
  root: this;
  active: () => OptimizedClassNames;
  "a-thing": ACssBlock.AThingClass;
  [name: string]: BlockClass | BlockState;
}
export namespace ACssBlock {
   export class AThingClass implements BlockClass {
    [name: string]: BlockState;
     "my-theme": (value: "red" | "blue") => OptimizedClassNames;
   }
}
// usage example:
function foo(a: ACssBlock): OptimizedClassNames {
  return a["a-thing"]["my-theme"]("red");
}

TODO:

  • Further examples of block interfaces and inheritance are needed.
  • Figure out how to create indexed types with only a limited set of indexed names.

Automatic CSS Variable Scoping

Overview

Currently, CSS variables are treated just like any other property in blocks. This means that conflicting variable names are expected to be explicitly resolved by the developer, but we can do better than that!

Problem

CSS variable can be given special treatment in blocks. Consider two blocks a and b:

/* a.block.css */
:scope {
  --my-var: red;
}
.class {
  color: var(--my-var);
}
/* b.block.css */
:scope {
  --my-var: blue;
}
.class {
  color: var(--my-var);
}

If both blocks are applied to the same element, their --my-var definitions will conflict, causing unexpected behavior and requiring explicit resolution.

Now consider two blocks base and extended:

/* base.block.css */
:scope {
  --theme-color: red;
}
.class {
  color: var(--theme-color);
}
/* extended.block.css */
@block-reference base from "base.block.css";
:scope {
  extends: base;
  --theme-color: blue;
  --local-var: red;
}
.bar {
  color: var(--local-var);
}

Here, the exception is elements with block extended applied will use the re-defined --my-var class for all inherited styles.

Proposal

Compiled blocks should rewrite variable names to unique identifiers. This is easily done by prefixing all vars with their uid:

/* a.block.css */
.a {
  --a-my-var: red;
}
.a__class {
  color: var(--a-my-var);
}
/* b.block.css */
.b {
  --b-my-var: blue;
}
.b__class {
  color: var(--b-my-var);
}

In blocks that extend a base block, conflicting css var names should inherit the name of their base block, while locally defined names should be prefixed with the local uid. So our extended block example becomes:

/* base.block.css */
.base {
  --base-theme-color: red;
}
.base__class {
  color: var(--base-theme-color);
}
/* extended.block.css */
.extended {
  --base-theme-color: blue;
  --extended-local-var: red;
}
.extended__bar {
  color: var(--extended-local-var);
}

Conflict Resolver API Changes

The current resolver resolves all selectors that match the target block object in the key selector. This means that states targeting that same object get overridden too. That seemed like a good idea, but it turns out to be sub-optimal for many use cases, especially inheritance (which is a resolution abstraction)

A new syntax for resolution is documented in the readme and needs to be implemented.

https://github.com/css-blocks/css-blocks/blob/master/README.md#resolving-state-conflicts

Styling Scope State Dependent Descendant DOM

Problem

Consider:
A nav design where a link, containing an icon and a label, are red and blue respectively at rest. When any part of the link is hovered, they both the link and the label should become green:

<nav class="root">
  <a href="#" class="link">
    <svg class="icon"/>
    <span class="label">Label</span>
  </a>
</nav>
.root {
  background-color: black;
}
.link {
  color: red;
}
.link:hover {
  color: green;
}
.icon {
  color: currentColor;
}
.label {
  color: blue;
}

/* This is not currently allowed by css-blocks */
.link:hover .label {
  color: currentColor;
}

Proposals

There are two (2) proposals for how to accommodate for this use case:


Proposal 1: Require developer to make a new block just for the nav link. .link would become that block's .root, and the :hover state on the root would become the new context selector for .label. Blocks allow root states as context selectors and the only change require to make this work is to treat pseudo-selectors on the root class as states.


Proposal 2: Loosen restrictions on Block syntax to allow ClassStates as context selectors, enabling developers to write selectors like .link:hover .label. This syntax relaxation will output the same exact code as creating a new block, but not require the end developer to jump through hoops for simple features like customizing hover effects. Using bare Classes as context selectors would still be disallowed.

By only allowing compound selectors that terminate in a state as context selectors, we still encourage simple, flat style construction, but acknowledge that internal state (read: not root state) may have an effect on other elements within the component.


Proposal 1 upholds the current mental model of css-blocks where root states are the only context selectors allowed by the framework; simplifying development, vastly limiting selector complexity, and preventing CSS specificity foot-guns. Developers are forced to engineer very flat style hierarchies, reducing style complexity and maximizing optimization benefits.

Proposal 2, in most cases, will result in the same exact CSS output as Proposal 1, without the developer overhead of creating a whole new block and wiring up a block-reference just for the sake of creating a very common hover style pattern.

Proposal 2 has the added benefit of enabling very common css patterns that are currently impossible with CSS Blocks, described in more detail below.

Drawbacks

Proposal 1

This issue raises concerns about some exceptionally common CSS patterns that are currently impossible with css-blocks as is. Proposal 1 does nothing to fix these concerns. Consider a custom checkbox component using the only method in CSS for creating styled check boxes:

import checkbox from "./styles.css";

return (
  <my-checkbox class={checkbox}>
    <input type="checkbox" id="my-checkbox" class={checkbox.input} />
    <label for="my-checkbox" class={checkbox.label}>Click me!</label>
  </my-checkbox>
);
// styles.css
.root {
  ...
}
.input {
  opacity: 0;
  pointer-events: none;
}
.label {
  background-color: red;
}
.input:checked + .label {
  background-color: green;
}

The above is not currently possible with css-blocks and no combination of new block files, global states, or inheritance make it work, even after treat pseudo-selectors on the root class as states. (@chriseppstein, please confirm?)

Proposal 2

Proposal 2 has the added benefit of enabling very common css patterns that are currently impossible with CSS Blocks, but with this comes the worry that developers can abuse this behavior, resulting in less-intuitive CSS and impacting optimization. For example, more freely using internal state to style components with sibling selectors instead of using root state:

.an-item[state|active] ~ .following-itmes {
  ...
}

These less-than-ideal selectors can have a non-trivial impact on optimization.

However, the argument can be made that when developers use sibling selectors they do so with intention and disallowing their use can drastically limit the styles we can compose with CSS, as shown above.

Shared State

I've been wondering if we should have a syntax for creating a state that is shared across all classes.

An example use case for this would be a the gutter and last states for a float-based grid block.

Using Sass, you can create mixins to simplify this process, but I'd like the block syntax to be usable without a preprocessor.

/* example.block.css */
.foo {
   margin: 10px;
}

.bar {
  margin: 18px;
}

*[state|last] {
   margin-right: 0;
}

Would then compile to:

.example__foo { margin: 10px; }
.example__bar { margin: 18px; }
.example__foo--last,
.example__bar--last { margin-right: 0; }

Maybe it's not common enough to warrant addressing with specialized syntax, but I thought I'd write it down for discussion at some point.

elision of state

Many states are static within a template and are used to select a display mode for a given component. In these situations we can statically resolve the selectors during optimization to a single unscoped class that can be merged with other declarations at the global level. Note this is only worth doing if all uses of the root-level state are static. if there's a dynamic use in any location, then there's no point in performing the optimization.

To do this, we have to record what classes and states are applied dynamically during template analysis.

it's possible this can be done by the css-blocks compiler by outputting a class that resolves against other classes/states that the specificity would normally resolve or perhaps this can be done purely by the optimizer.

Glimmer Template Rewriter

The glimmer template rewriter will use a block-mapping.json file to rewrite classes.

The blockMappings are populated by the block compiler. mappings are recorded by the optimizer.

{
  "blockMappings": [
    {
      "file": "path/to/my-block.block.css",
        "root": {
          "classNames": "my-block",
          "states": {
            "foo": { "classNames": "my-block--foo" },
            "bar": {
              "sub1": { "classNames": "my-block--bar-sub1" },
              "sub2": { "classNames": "my-block--bar-sub2" }
            },
          },
        },
        "classes": {
          "abc": {
            "classNames": "my-block",
            "states": {
            }
          }
        },
        "externals": {
          "classes": [ ],
        }
    },
  ],
  "mappings": {
    "c2": ["my-block--foo", ["asdf", "qwer", "Zxcv"], "my-block--bar-sub2"]
  }
}

blockMappings get from a block root/class/state to one or more css classes.

mappings structure is:

key is the class name that should be applied to the element if the values match.

values is a list of strings or arrays where any of the list entries matches. If the entry is a string then that class must be on the element once block mappings are resolved to classes. If the list entry is an array then all of those values must match.

create bloom filter of all values on right, do a full match if the bloom filter passes.
or maybe create a map of values to array of keys and then do a full match on those keys.

The classname optimizer can transform the classes produced by the block compiler
by replacing the block classNames in both the blockMappings and in the Mappings.

external classes cannot be transformed or mapped they are provided so that they aren't confused for block classes.

Proposal: Binary Encoding of Boolean Expression Shapes

Problem

  1. Currently, pushing styling logic to the templates bloats compiled template size and, in some cases, results in a net app size increase. We need a more efficient way to encode boolean logic back into the template to reduce template bloat.
  2. Template rewriters have the unnecessary responsibility of translating boolean expression objects emitted by opticss to the css-blocks runtime helper syntax. Is is possible to remove this overhead and push responsibility back to css-blocks core.

Proposal

We can greatly compress the css-blocks runtime by treating the runtime helper as a projection of arguments over unique boolean expression "shapes".

To maximize space efficiency we store these boolean expression instructions as a list of binary opcodes. All required opcodes for boolean expression evaulation may be represented using just three (3) bits:

const OP_CODES = {
  0: 'OPEN',     // 000
  1: 'VAL',      // 001
  2: 'NOT',      // 010
  3: 'OR',       // 011
  4: 'AND',      // 100
  5: 'EQUAL',    // 101
  6: '---',      // 110
  7: 'CLOSE'     // 111
};

Note: The VAL opcode is always followed by an integer representing the index of the dynamic value passed to our helper it represents. The number of bits to look at to fetch an index is determined by the number of dynamic expressions passed to ensure minimal size. All other opcodes have no special concerns or lookahead.

The following boolean expression:

!(exp1 || exp2)

May be compiled to the following opcodes:

let opcodes = "NOT OPEN VAL 0 OR VAL 1 CLOSE";

And then be represented by the following binary string:

let binaryOpcodes = "010 000 001 0 011 001 1 111";

Encoded shape expressions can be delivered to the runtime helper as an array of base 36 encoded Uint32 integers. The binary opcode order is reversed when inserted into base 36 integers to a) reduce encoded size when delivered to the browser and b) simplify the runtime function implementation.

Continuing the previous example, the above binary opcodes can be delivered as base 36 encoded Uint32 integers by undergoing the following transformations:

// Original binary opcode list:
// 010 000 001 0 011 001 1 111
let integer = parseInt("000000000000 111 1 001 011 0 001 000 010", 2); // 994370
let encoded = integer.toString(36); // "lb9e"

When a set of binary opcodes exceed the 32 bit limit, the opcodes overflow into the next encoded integer delivered in the expression shape's array.

To account for the logic required by many classes that must be applied in a single runtime call to an individual element, the boolean expressions for multiple classes may be encoded into a single expression shape by delineating standalone expressions with an extra CLOSE opcode, demonstrated below.

Note: Because expression shapes are encoded as strings we have the potential to reap gzip/brotli benefits if many identical expression shapes are reused across the app.

The runtime helper public interface now takes the following shape:

function runtime(shape: string[], classes: string[], expressions: boolean[]) => string;

An example with logic for multiple classes call may look like:

// Original: objstr({ class1: expr1 == expr2, class2: expr1 !== expr2 })
// VAL 0 EQUALS VAL 1 CLOSE VAL 0 NOT EQUALS VAL 1 
// 001 0 101 001 1 111 001 0 010 101 001 1 
// 162036945 
// "2oh0i9"
runtime(["2oh0i9"], ["class1", "class2"], [expr1, expr2]);

Here we encode the boolean expressions for two classes. class1 should be displayed when expr1 and expr2 are equal. class2 should be displayed when expr1 and expr2 are not equal.

In practice, the runtime implementation for this type of binary encoding system is very fast. An opcode parser may look something like this:

const INT_SIZE = 32;

function computeStyles(shape, classes, expressions) {

  // We dynamically determine the variable window size based on the number of
  // expressions it is possible to reference. It is the compiler's responsibility
  // to guarentee the expression shape matches at build time.
  const VAR_SIZE = ~~Math.log2(expressions.length - 1) + 1;

  let klass      = 0,     // Current class we are determining state of
      opcode     = null,  // Current opcode to evaluate.
      lookahead  = null,  // This is a single lookahead parser – lookahead opcode will be stored here.
      step       = 0,     // Character count used for opcode discovery
      invertNext = false, // Should we invert the next discovered value
      val,                // Working boolean value
      stack      = [];    // Stack for nested boolean expressions

  // For each 32 bit integer passed to us as a base 36 string
  for ( let segment of shape ) {

    // Convert to a base 10 integer
    let integer = parseInt(segment, 36);

    // Process each bit in this integer.
    // Note: `while` loop is faster than a `for` loop here.
    let iters = INT_SIZE;
    while (iters--) {

      // Construct our lookahead opcode and "pop" a bit off the end
      // of our integer's binary representation.
      lookahead += integer % 2 * (2 * step || 1);
      integer = integer >>> 1;

      // When we have discovered the next opcode, process.
      if (!(step = ++step % (opcode == 1 ? VAR_SIZE : 3))) {

        // Each opcode type requires implementation
        switch (opcode) {
          // START
          case 0: break;

          // VAL
          case 1: break;

          // NOT
          case 2: break;

          // OR
          case 3: break;

          // AND
          case 4: break;

          // EQUAL
          case 5: break;

          // ---
          case 6: break;

          // CLOSE
          case 7: break;
        }

        // Begin construction of new opcode.
        opcode = lookahead;
        lookahead = null;
      }
    }
  }
}

This example implementation runs at sub-millisecond times (~0.2ms) for even exceptionally long opcode sequences (~2000 bits). The addition of opcode functionality should not increase this significantly as boolean expression shape parsing should be the limiting operation.

Problems

This proposal, as written, does away with the current concept of Source Expressions. All source expressions – Boolean, Ternary and Switch – can be encoded for individual classes in their respective boolean expressions. This may not be the most efficient way to encode this logic and there is probably a way to encode these source expressions into the expression shape without increasing opcode size. Certainly something to explore more.

Another source of template bloat comes from having to write all substate values into the template to accommodate dynamic state logic. Substate strings may be best hard coded into a generated file and shared among all templates and instead reference them by UID. However, this opens a whole new can of worms, especially for code splitting. Also, with gzip, the benefits of substate abstraction may be compressed away and is possibly a worthless optimization.

We would be able to get even more template size savings by encoding the Uint32s in base 85, but this would bloat the runtime and slow down base conversion – may not be worth it.

[css-blocks] synthetic conflicts

There should be a way to create resolution constraints that span properties that don't naturally conflict in css. This can be used to implement things like accessibility constraints for contrast between foreground and background colors.

Such a capability should be extensible within an application or css library that is built on css-blocks.

Compression Ideas

This is a small set of ideas for the compression steps. I'm writing these free-form.

Leverage Brotli Static Dictionary for Key Values
We spoke about this in person, but I'd like to try an experiment with counter hashing leveraging the static dictionary brotli comes with (https://gist.github.com/anonymous/f66f6206afe40bea1f06).

Here's the idea:

  1. Pull all the values from the dictionary and filter out ones invalid for classnames. (eg !, https://gist.github.com/anonymous/f66f6206afe40bea1f06#file-gistfile1-txt-L777)
  2. Use these values for classname keys
  3. Only apply to Brotli compressed version of the assets (this requires separate build of all dependents on a brotli pathway)

CSS Nano Optimizations
Understand you're already likely looking at these optimizations, but wanted to insure it was noted somewhere.

http://cssnano.co/

Improve Style Conflict Error Message

See: #62 (comment)

tl;dr, move template usage data to the error message:

The following property conflicts must be resolved for these co-located Styles:

  template:
    div.class always uses block-a.root (templates/my-template.hbs:10:32)
      and conditionally uses block-b.root (templates/my-template.hbs:10:39)

  color:
    block-a.root (blocks/foo.block.css:3:36)
    block-b.root (blocks/b.block.css:1:30)

  background-color:
    block-a.root (blocks/foo.block.css:3:48)
    block-b.root (blocks/b.block.css:1:43)

CSS Blocks README Updates

Remove Unimplemented bits from README and into design issues. Review for accuracy. Update with any new concepts and additional details.

Setup TSLint to check for more style preferences

  • indentation rules for class declarations (https://github.com/css-blocks/css-blocks/pull/62/files#r164588816)
  • space inside curly brackets
  • space inside parens
  • space after commas
  • consistent indentation
  • space after function/method declaration
  • space between : and return type.
  • space around binary operators
  • require return type declaration for functions (tbd: only for functions with multiple return statements?)
  • prefer for...of to forEach

and review settings for other whitespace lint rules and make sure we like them and enable them.

Update `addStyle` and `addExclusiveStyle` Interface

Problem

Currently, style correlation methods on Analysis objects expect analyzers to pass the exact BlockObject they wish to add. This forces analyzers to look up block references and pushes a lot of standard error catching logic into the analyzers, complicating analyzer implementation.

Solution

By updating addStyle and addExclusiveStyle to accept the parent Block, and string representations of child BlockObjects to add, block lookup and error throwing can happen in css-blocks core, further simplifying analyzer implementations.

Example

addStyle(block, ".foo", true);
addExclusiveStyle(block, true, ".foo[state|bar]", ".foo[state|baz]");

Issues

Analyzers will have to translate discovered classes and attributes into well-formed selector strings. This string formatting may be unpleasant and result in a lot of boilerplate code. We may consider instead exposing a BlockObject lookup API on Blocks that will throw a helpful error if the block doesn't exist. This removed error checking from the Analyzers, but still provides them a programmatic interface for accessing and passing blocks around.

addStyle(block.getClass("foo", locInfo), true); // Will throw if "foo" is not a class on `block`
addStyle(block.getClass("foo", locInfo).getState("bar"), true); // Will throw if "foo[state|bar]" does not exist on `block`

Webpack CSS Hot Reloading

Because we take over the css compile / emit step for builds in the webpack-plugin, and future broccoli-plugin, no existing css hot reloading plugins easily integrate with css-blocks. We need to either provide integration documentation, or roll our own css hot reloading.

Add custom linter for return types

This builtin rule will check that there's a declared return type for functions, which I think is overkill. But whenever a function has multiple return statements (or multiple yield statements in a generator), return type should be required.

Possible option: only require a type declaration if the return statements have different types.

This will require a custom linter.

This issue is being filed because it was too complex to fit into #65

Runtime: deliver runtime from css-blocks core?

This issue was originally filed by @amiller-gh against the runtime repo which has been merged into the css-blocks monorepo:

It doesn't make much sense to deliver as an independently versioned peer dependency. The rewriters will use a specific version of the runtime and that runtime will most likely be coupled to the version of css-blocks. Instead, css-blocks can:

  1. Return to the rewriters the exact arguments that must be passed into the helper, instead of raw boolean expressions.
  2. Expose the runtime in a well-defined location for rewriters to proxy up to the app they are integrated into.

This will reduce project module complexity and help to unify implementations of css-blocks across the board.

First Reactions

Chris, I'm so so so excited to be working on this with you. So much time has been focused on the JavaScript side of reusable, fast components and CSS remains such a pain point for people (both ergonomically and from a performance perspective).

I wanted to write down my first impressions while I'm still more or less Curse of Knowledge-free. Hopefully documenting the delta between what I was expecting and the initial proposal will be helpful, even if we decide that the first spike is the correct one, because we can better anticipate the objections other people might have.

I think, fundamentally, I was expecting something more "component-oriented." That is probably largely driven by my own biases, having been thinking about React and Glimmer components so much recently. You've been thinking about BEM a lot, so it's no wonder this proposal is framed from that perspective.

I wonder if there's some way we can join these two worlds. The proposal as-is is fairly standalone, in the sense that it introduces its own terminology and concepts. That's a win if the current excitement around components ends up being a fad (two-way bindings, anyone?). But my gut feeling is that it is here to stay, and even folks who don't write a lot of JavaScript will be thinking more in terms of components in the future thanks to Web Components, shadow DOM, etc.

Looking at the form.block.scss example, there's more cognitive overhead around naming than I think is necessary if we can integrate deeply into e.g. the Glimmer component model.

Blocks & Components

If I understand correctly, it seems like there's a pretty natural correlation between a "block" and a "component". It would be nice if we could omit having to declare and name the block and have .scss files co-located with the component implicitly be scoped to the component block.

You still need some way to declare rules for the "component block," and there is some prior art we can consider borrowing. For example, ember-component-css uses the SCSS & parent selector. This could look like:

/* src/ui/components/my-form/style.scss */
& {
  margin: 2em 0;
  padding: 1em 0.5em;

  &:state(compact) {
    margin: 0.5em 0;
    padding: 0.5em 0.5em;
  }
}

We could also theoretically use :component pseudo-selector?

Behind the scenes, this could generate a block with the same name as the component, and we could apply that class to the component automatically.

Auto-Inferring Elements

Can we treat class names in a component template as implicit elements? Using more complex selectors would be a build error unless there was an explicit opt out.

(And can we talk about how confusing re-using the term Element is in BEM?)

Glimmer Example

Spitballing here, but here's an strawman that's integrated into a Glimmer component.

Style

/* src/ui/components/sailfish-form/style.scss */
:component {
  margin: 2em 0;
  padding: 1em 0.5em;
  &:state-set(theme) {
    &:state(red) {
      color: #c00;
    }
    &:state(blue) {
      color: #006;
    }
  }

  &:state(compact) {
    margin: 0.5em 0;
    padding: 0.5em 0.5em;
  }
}

.input-area {
  display: flex;
  margin: 1em 0;
  font-size: 1.5rem;
  :state(compact) & {
    margin: 0.25em 0;
  }
}

.label {
  flex: 1fr;
}

.input {
  flex: 3fr;
  :state(theme red) & {
    border-color: #c00;
  }
  :state(theme blue) & {
    border-color: #006;
  }
}

.submit {
  width: 200px;
  &:substate(disabled) {
    color: gray;
  }
}

Template

{{! src/ui/components/sailfish-form/template.hbs }}
<form class="{{state 'compact'}} {{state 'theme' theme}}">
  <div class="input-area">
    <label class="label">Username</label>
    <input class="input">
  </div>
  <submit class="submit {{state disabled}}">
</form>

Component

// src/ui/components/sailfish-form/component.ts
import Component from '@glimmer/component';

export default class extends Component {
  disabled = true;

  get theme() {
    return this.args.user.isAdmin ? 'red' : 'blue';
  }
}

Output

This is what would be rendered in the final CSS output/to the DOM (non-compressed). (Block name is inferred from component name.)

<form class="sailfish-form sailfish-form--compact sailfish-form--theme-red">
  <div class="sailfish-form__input-area">
    <label class="sailfish-form__label">Username</label>
    <input class="sailfish-form__input">
  </div> 
  <submit class="sailfish-form__submit sailfish-form__submit--disabled">
</form>
.sailfish-form { margin: 2em 0; padding: 1em 0.5em; }
.sailfish-form--theme-red { color: #c00; }
.sailfish-form--theme-blue { color: #006; }
.sailfish-form--compact { margin: 0.5em 0; padding: 0.5em 0.5em; }

.sailfish-form__input-area { display: flex; margin: 1em 0; font-size: 1.5rem; }
.sailfish-form--compact .form__input-area { margin: 0.25em 0; }

.sailfish-form__label { flex: 1fr; }

.sailfish-form__input { flex: 3fr; }
.sailfish-form--theme-red .form__input { border-color: #c00; }
.sailfish-form--theme-blue .form__input { border-color: #006; }

.sailfish-form__submit { width: 200px; }
.sailfish-form__submit--disabled { color: gray; }

Discussion

A few notes about this:

  1. Use :component to target root component block.
  2. It seems like states can either be boolean or string values. I'm not sure how that interacts with the state-set stuff.
  3. The class name for the root component block is auto-generated based on the component name, and added on the template's root element by default.
  4. Any class inside the component's SCSS file is interpreted as an element. That means any combination of class names will throw an error at build time (if I understand elements correctly).
  5. State goes through the {{state}} helper. In this example, it auto-infers the element/block to apply the state/substate to, although I don't know how reliable this would be in practice. Probably easier for the root component block than elements inside. The helper can take either a static value or dynamic data from the template.

WDYT?

Shorthand Conflict Resolution

Here's the project that we need to fork: https://github.com/ben-eb/css-values

This is the JSON data that it uses:
https://github.com/ben-eb/css-values/blob/master/data/data.json

Which is a copy and unification of this source data: https://github.com/mdn/data/blob/master/css/

but it has been cleaned up and had a number small issues fixed in the local copy.

According to Ben, this data really ought to be updated against the latest. In theory, there should be a repeatable process for taking updates and applying local patches (which we really need to get back into the MDN official copy eventually -- they accept pull requests).

The source for the code generator is here: https://github.com/ben-eb/css-values/tree/master/src

What we need for css-blocks is to be able to implement the following functions:

function extractLonghand(shorthandProperty: string, value: string, longhandProperty: string): string | null {};

function extractLonghandOrDefault(shorthandProperty: string, value: string, longhandProperty: string): string {};

The first function would extract the value for the longhand from the shorthand or return null if it's not set explicitly.

The second function would always return a value for the longhand, returning the default value for that longhand property if it's not set explicitly.

You can also imagine other useful api's:

  • return all values for the long hands in a given shorthand
  • validate shorthand

Those may be generally useful for other use cases. In general, any support for shorthands should reflect the existing patterns and support for the long hands already in the code.

Keep in mind that some shorthands expand to shorthands that expand to longhands. E.g. border -> border-width, border-style, border-color; border-width -> border-top-width, border-right-width, border-bottom-width, border-left-width.

So it should be possible to request the value for border-bottom-style from a border value.

I'd suggest that we first submit a pull request with the data update.

Then open an issue with proposed api changes and give ben an opportunity to weigh in on the api before we build it.

Rename `state` namespace to `ui`

We have found that the term state is easily confused with the React concept of component state and [ui|statename] is a more concise syntax.

Restrict ClassState Styles to Elements With Base Class

Problem

Currently, ClassStates may be applied to elements that do not have their corresponding class styles applied.

// .block__class
.class {
  color: red;
}
 // .block__secondary--filled
.class[state|filled] {
  background: blue;
}
import block from "filename.block.css";
let styles = objstr({
  [block.secondary]: false,
  [block.secondary.filled()]: true
});
<div class={styles}></div> // Receives blue background, no color.

Proposed Solution

In lieu of a runtime, or very complex static analysis, we can slightly modify ClassState output to restrict styles to elements with the corresponding parent class:

// .block__class
.class {
  color: red;
}
 // .block__class.block__class--filled
.class[state|filled] {
  background: blue;
}
import block from "filename.block.css";
let styles = objstr({
  [block.secondary]: false,
  [block.secondary.filled()]: true
});
<div class={styles}></div> // Receives no styling.

This has the additional benefit of maintaining the specificity of pre-transformed block code, avoiding potentially confusing specificity errors.

Potential Drawbacks

This will result in increased pre-optimized file size and may create new considerations for optimization. Discussion needed.

default substate

When a block class has a state with several substates, it is convenient to allow one of the substates to be declared as a default.

during rewriting the class(es) for the default substate would get added whenever no other substate for that state is specified.

Handle state attribute values in quotes

postcss-selector-parser includes the quotes -- we need to strip them.

> n = selectorParser().process('[state|foo="1"]')
Processor {
  func: [Function: noop],
  res:
   Root {
     spaces: { before: '', after: '' },
     nodes: [ [Object] ],
     type: 'root' } }
> n.res.nodes[0].nodes
[ Attribute {
    operator: '=',
    value: '"1"',
    source: { start: [Object], end: [Object] },
    sourceIndex: 0,
    attribute: 'foo',
    namespace: 'state',
    spaces: { before: '', after: '' },
    type: 'attribute',
    raws: { unquoted: '1' },
    quoted: true,
    parent:
     Selector {
       spaces: [Object],
       nodes: [Circular],
       type: 'selector',
       parent: [Object] } } ]

Support interoperableCSS properly

For existing webpack apps we should explore whether we can build an iCSS interface to css-blocks that works well. We had an early implementation of this, but I am no longer confident that it works sanely and I think re-implementing it from scratch on top of our rewriter code (or a common shared implementation) would be for the best.

Private/Protected Objects?

We should consider whether inheritance needs the notion of a private object. Marking an object as private would make it illegal to resolve with an override.

Protected would be the same but would allow overrides in sub-blocks.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.