-
Notifications
You must be signed in to change notification settings - Fork 1
Cell Query
Cell implements Cell Query (CQ), which is an abstraction of Cell's mixins' behaviour in the form of plain keys and values, allowing them to be authorable in most programming languages as objects (or "maps" in Sass).
There are two main ways to utilise CQ within Cell:
The first way is by passing a CQ compatible Sass map to the $content
parameter of either the Module or Component mixins. These maps will automatically be parsed as CQ.
CQ is ideal to store in your module's configuration, where Cell can read the configuration and subsequently parse any CQ as CSS. This is an opt-in feature, and can be used in conjunction with the previous $content
method. To opt-in to this feature, the $outputCSSFromConfig
Global Variable should be set to true
.
This feature gives you more flexibility and hence control over authoring the styles for your modules. The purpose isn't to simply allow you to author all of your styles as objects (like with the previous $content
method), but is rather to allow you to abstract configurable CSS properties from your source code and keep them separate as configutation. In short, if a CSS property is prone to changing for cosmetic purposes, it can be considered "configurable" and can be kept inside the module's configuration, instead of within the module's source code.
Consider the following example of a hypothetical header
module:
@include module('header') {
display: 'block';
position: 'absolute';
top: 0;
width: 100%;
background-color: #444444;
height: 100px;
}
If we wanted to make the background-color
and height
properties configurable, we might think to do something like this, like many other UI frameworks:
$header-background: #444444 !default;
$header-height: 100px !default;
@include module('header') {
display: 'block';
position: 'absolute';
top: 0;
width: 100%;
background-color: $header-background;
height: $header-height;
}
...i.e extract the values away from the source code and into variables. This is good as it allows you to keep all configurable values in one place. Using Cell's utilities, the same thing could be achieved in a slightly different flavour:
$config: (
'background-color': #444444,
'height': 100px
);
@include module('header') {
display: 'block';
position: 'absolute';
top: 0;
width: 100%;
background-color: this('background-color');
height: this('height');
}
Cell can take this concept one step further; if a configuration key (e.g. background-color
) just so happens to correspond to a valid CSS property, Cell will save you the trouble of having to hard-code that property into your souce code, leaving us with:
$config: (
'background-color': #444444,
'height': 100px
);
@include module('header') {
display: 'block';
position: 'absolute';
top: 0;
width: 100%;
}
This keeps the source code of your modules cleaner by housing only fundamental CSS properties that control the module's structure and functionality, abstracting any cosmetic CSS properties into configuration. The theory is that without a module's configuration you would still be left with a functioning interactive module that didn't "break" anything (i.e. no unwanted side effects).
One of the biggest risks when changing a CSS codebase is unknown or unwanted side effects, and when you're changing properties like display
and position
(i.e structural/layout properties), it's much easier to break things, which is why it makes sense to separate such properties.
Distinguising between CSS properties in these terms grants the power of changing the cosmetic look and feel of your project without writing any code, and with little to no risk of unwanted side effects.
Check out the CSS at Scale: Cosmetic vs Layout Properties article for more information on this concept
Combined with the CQ API, this allows you to style your modules like a superhero.
See the following sections to learn how each Cell mixin can be represented as CQ:
- Component Mixin
- Is/Modifier Mixin
- Context Mixin
- Sub-Component Mixin
- Pseudo-State/Pseudo-Element Mixin
- Complete Example
See the
Component()
page for more information on this mixin
The Component mixin is represented in CQ as:
'{COMPONENT}': {
...
}
'__{COMPONENT}': {
...
}
'component({COMPONENT})': {
...
}
@include component(#{COMPONENT}) {
...
}
$config: (
'name': 'myModule',
'myComponent': (
'color': red
)
);
@include module {
@include component('myComponent') {
display: block;
}
}
.myModule__myComponent, [class*="myModule__myComponent--"] {
color: red;
}
.myModule__myComponent, [class*="myModule__myComponent--"] {
display: block;
}
See the
Modifier()
page for more information on this mixin
The Modifier mixin is represented in CQ as:
'is-{MODIFIER}': {
...
}
'--{MODIFIER}': {
...
}
'modifier({MODIFIER})': {
...
}
@include is(#{MODIFIER}) {
...
}
$config: (
'name': 'myModule',
'scale-amount': 0.75,
'is-active': (
'background': red
)
);
@include module {
transform: scale(this('scale-amount'));
@include is('active') {
transform: scale(1);
}
}
.myModule, [class*="myModule--"] {
transform: scale(0.75);
}
[class*="myModule--"][class*="--active"] {
background: red;
}
[class*="myModule--"][class*="--active"] {
transform: scale(1);
}
See the
Context()
page for more information on this mixin
The Context mixin can be represented in CQ in several ways depending on the desired behaviour.
'in-{COMPONENT}': {
...
}
@include context(($this, #{COMPONENT})) {
...
}
$config: (
'name': 'post',
'heading': (
'in-tile': (
'margin-left': 1em
)
)
);
@include module {
display: inline-block;
}
.post, [class*="post--"] {
display: inline-block;
}
[class*="post__"][class*="__tile"] .post__heading,
[class*="post__"][class*="__tile"] [class*="post__heading--"] {
margin-left: 1em;
}
'in-${MODULE}': {
...
}
@include context(#{MODULE}) {
...
}
$config: (
'name': 'icon',
'in-$button': (
'margin-right': 1em
)
);
@include module {
display: inline-block;
}
.icon, [class*="icon--"] {
display: inline-block;
}
.button .icon,
.button [class*="icon--"],
[class*="button--"] .icon,
[class*="button--"] [class*="icon--"] {
margin-right: 1em;
}
'$-is-{MODIFIER}': {
...
}
'$:{PSEUDO-STATE}': {
...
}
@include context($this, #{CONTEXT}) {
...
}
$config: (
'name': 'myModule',
'myComponent': (
'$-is-highlight': (
'background': limegreen
),
'$:hover': (
'background': dodgerblue
)
)
);
@include module {
display: block;
}
.myModule,
[class*="myModule--"] {
display: block;
}
[class*="myModule--"][class*="--highlight"] .myModule__myComponent,
[class*="myModule--"][class*="--highlight"] [class*="myModule__myComponent--"] {
background: limegreen;
}
.myModule:hover .myModule__myComponent,
.myModule:hover [class*="myModule__myComponent--"],
[class*="myModule--"]:hover .myModule__myComponent,
[class*="myModule--"]:hover [class*="myModule__myComponent--"] {
background: dodgerblue;
}
'{COMPONENT}-is-{CONTEXT}': {
...
}
@include context(#{COMPONENT}, #{CONTEXT}) {
...
}
$config: (
'name': 'myModule',
'title': (
'panel-is-active': (
'background': limegreen
),
'panel-is-:hover': (
'background': dodgerblue
)
)
);
@include module {
display: block;
}
.myModule,
[class*="myModule--"] {
display: block;
}
[class*="myModule__panel--"][class*="--active"] .myModule__title,
[class*="myModule__panel--"][class*="--active"] [class*="myModule__title--"] {
background: limegreen;
}
.myModule__panel:hover .myModule__title,
.myModule__panel:hover [class*="myModule__title--"] {
background: dodgerblue;
}
[class*="myModule__panel--"]:hover .myModule__title,
[class*="myModule__panel--"]:hover [class*="myModule__title--"] {
background: dodgerblue;
}
'and-is-{MODIFIER}': {
...
}
'and:{PSEUDO-STATE}': {
...
}
See the
Component()
page for more information on this mixin
The Sub-Component mixin is represented in CQ as:
'{COMPONENT}': {
'{SUB-COMPONENT}': {
...
}
}
@include component(#{COMPONENT}) {
@include sub-component(#{SUB-COMPONENT}) {
...
}
}
$config: (
'name': 'header',
'navigation': (
'item': (
padding: 1em
)
)
);
@include module {
@include component('navigation') {
@include sub-component('item') {
display: inline-block;
}
}
}
.header__navigation__item,
[class*="header__navigation__item--"] {
padding: 1em;
}
.header__navigation__item,
[class*="header__navigation__item--"] {
display: inline-block;
}
See the
Pseudo-State()
page for more information on this mixin
The Pseudo-State mixin is represented in CQ as:
':{PSEUDO-STATE}': {
...
}
@include pseudo-state(#{PSEUDO-STATE}) {
...
}
&:#{PSEUDO-STATE} {
...
}
$config: (
'name': 'myModule',
':hover': (
'background': #0155a0
)
);
@include module {
display: block;
}
.myModule,
[class*="myModule--"] {
display: block;
}
.myModule:hover,
[class*="myModule--"]:hover {
background: #0155a0;
}
The below example shows how Cell can be used to style a UI accordion, keeping all configurable cosmetic properties separate from the functional/layout properties, allowing them to be easily overridden with different themes etc.
<div class="accordion">
<div class="accordion__panel">
<div class="accordion__title">Accordion title 1</div>
<div class="accordion__content">
...
</div>
</div>
<div class="accordion__panel--active">
<div class="accordion__title">Accordion title 2</div>
<div class="accordion__content">
...
</div>
</div>
</div>
$config: (
'name': 'accordion',
'title': (
'padding': 1em,
'color': #ffffff
'background': #0155a0,
':hover': (
'background': #1cb1db
),
'panel-is-active': (
'background': #f21550
)
)
);
@include module {
@include component('panel') {
display: block;
}
@include component('title') {
cursor: pointer;
}
@include component('content') {
display: none;
@include context(($this, 'panel'), 'active') {
display: block;
}
}
}
.accordion__title,
[class*="accordion__title--"] {
padding: 1em;
color: #ffffff;
background: #0155a0;
}
.accordion__title:hover,
[class*="accordion__title--"]:hover {
background: #1cb1db;
}
[class*="accordion__panel--"][class*="--active"] .accordion__title,
[class*="accordion__panel--"][class*="--active"] [class*="accordion__title--"] {
background: #f21550;
}
.accordion__content,
[class*="accordion__content--"] {
background: #ffffff;
}
.accordion__panel,
[class*="accordion__panel--"] {
display: block;
}
.accordion__title,
[class*="accordion__title--"] {
cursor: pointer;
}
.accordion__content,
[class*="accordion__content--"] {
display: none;
}
[class*="accordion__panel--"][class*="--active"] .accordion__content,
[class*="accordion__panel--"][class*="--active"] [class*="accordion__content--"] {
display: block;
}