Web Site of
Michael Michlin
Mimcss Guide: Defining Selectors
Previous Unit: Defining Rules Next Unit: Defining Styles

Mimcss Guide: Defining Selectors

CSS selectors

CSS selectors range from very simple to rather complex, and Mimcss provides several means to define them.

In its simplest form a selector can be a single tag name or CSS class or element identifier. The more complicated selectors (called compound selectors) can define attributes and pseudo classes and elements (we collectively call them “pseudo entities” here). Some pseudo entities are simple keywords (e.g. :hover), while others take form of a function with parameters (e.g. :nth-child(2n+1)). Moreover, all these items can be chained together or specified as a list or use other selector combinators to indicate item relationship to each other (e.g. descendant or immediate child or general sibling or adjacent sibling).

Simple selectors are handled by the StyleDefinition methods $tag(), $class() and $id(). The $tag methods accepts either a single name or an array of names, which it treats as a list. That is, the following code

class MyStyles extends css.StyleDefinition
{
    tags = this.$tag( ["p", "section", "article"], { padding: 2 })
}

will create the following CSS:

p, section, article { padding: 2px; }

The more complicated selectors could take two major forms:

  • Selectors which add items to a tag or class or ID, e.g. a:hover.
  • Selectors that combine any number of tags, classes, IDs, attributes and pseudo entities with complex relationships, e.g.
      section.major:not(#main) > section.minor .item:nth-of-type(odd)
    

Any type of selectors can be handled using the $style() method; however, the first type of selectors is more conveniently handled using the dependent styles, which are described in details in the next unit Defining Styles. As a short example, the a:hover selector would be described by the following code:

class MyStyles extends css.StyleDefinition
{
    a = this.$tag( "a", {
        ":hover": { textDecoration: "blue"}
    })
}

For the really complex selectors, the $style() method should be used, which accepts the CssSelector type as the first parameter. The CssSelector type can be created using the following methods:

  • A regular string, e.g.
      'input[type="text"]'
    
  • Invocation of the selector tag function, e.g.
      css.selector`input[type="text"]${this.search}:focus`
    
  • Invocation of the sel function with chained invocation of properties and methods, e.g.
      css.sel("input").attr("type", "text").and(this.search).focus
    
  • As an array of items - each of the CssSelector type - e.g.
      ['input[type="text"]', this.search, ':focus']
    

Using Strings

Using a regular string to define a selector is the simplest of all methods; however, in the context of Mimcss, it is very limited. If the selector doesn’t use any classes or IDs defined within the StyleDefinition class, then using a string is perfectly legitimate. For example, if you want any text input element within an <article> element to show a blue outline when focused, you can do it the following way:

class MyStyles extends css.StyleDefinition
{
    focusedInput = this.$style( 'article > input[type="text"]:focus', { outline: [2, "solid", "blue"] })
}

Problems arise, however, when classes or IDs should be part of the selector. How, for example, should we define a selector for an input element with a certain CSS class. Consider the following example:

class MyStyles extends css.StyleDefinition
{
    specialInput = this.$class({...})
    focusedInput = this.$style( 'article > input[type="text"]???:focus', { ... })
}

We would like the name of the class to appear where we put the three question marks, but we don’t know the name because it is generated by Mimcss. What if we try the following?

class MyStyles extends css.StyleDefinition
{
    specialInput = this.$class({...})
    focusedInput = this.$style( `article > input[type="text"].${this.specialInput.name}:focus`, { ... })
}

Although the compiler will be happy with the above code, it will not actually work. The problem is that by the time the $style() method is invoked, the name property of the specialInput rule object is not defined yet. The reason is that the $style method is invoked during the StyleDefinition object construction, and Mimcss didn’t have the chance yet to run its name generation process. And that makes defining CSS selectors using regular strings very limited.

Using selector() Function

The Mimcss selector() function is a tag function, that is, it accepts a template literal with embedded parameters denoted using the ${} notation. This allows embedding CSS classes and IDs into the otherwise static string. For example, the selector from the previous section can be easily defined like the following:

class MyStyles extends css.StyleDefinition
{
    specialInput = this.$class({...})
    focusedInput = this.$style( css.selector`article > input[type="text"]${this.specialInput}:focus`, { ... })
}

Note that we didn’t use the name property of the specialInput rule object - we passed the object itself. The selector() function is smart enough to defer getting the class name until it is generated. Also note that we don’t need to prefix the class name with the "." symbol because the selector() function will do it for us. Similarly, if we were to use an ID rule, the "#" symbol would be added.

The selector() function most closely resembles the “CSS” way of how the selectors are defined. Everything, which is static is specified as a regular string content and only the dynamic items such as classes and IDs are embedded using the ${} notation. The type of those embedded parameters is defined as CssSeletor, so anything of that type including another selector() function invocations as well as anything that is convertible to a string can be used as a parameter.

Using sel() Function

The Mimcss sel() function returns an object that has properties and methods for gradually building a compound selector. The following snippet demonstrates using the sel() function to build the selector from the previous section:

class MyStyles extends css.StyleDefinition
{
    specialInput = this.$class({...})
    focusedInput = this.$style( css.sel("article").child("input").attr("type", "text").and(this.specialInput).focus, { ... })
}

The sel() function itself returns the ISelectorBuilderinterface and every property and method of this interface also return it; therefore, any method or property invocation can be chained to the previous method or property invocation. The sel() function itself accepts zero or more selectors (of type CssSelector). If more than one selector is provided, they are simply concatenated with each other. The internal object that implements the ISelectorBuilder interface maintains a “current selector”. This current selector is initialized with the selectors provided to the sel() function.

// produces CSS: p.cls1.cls2
css.sel("p", this.cls1, this.cls2);

When the properties and methods are invoked, the corresponding items are added to the current selector. The properties and methods of the ISelectorBuilder interface can be divided into the following categories:

  • Method and() receives zero or more selectors. If more than one selector is provided, they are simply concatenated to the current selector and to each other. If zero selectors are provided the call has no effect.
      // produces CSS: p.cls1.cls2
      css.sel("p").and(this.cls1, this.cls2);
    
  • Methods or(), child(), desc(), sib() and adj() receive zero or more selectors. If more than one selector is provided, they are concatenated to the current selector and to each other using the corresponding combinator. If zero selectors are provided, then only the corresponding combinator is added to the current selector.
    • The or() function uses the list combinator ",".
    • The child() function uses the immediate child combinator ">".
    • The desc() function uses the descendant combinator " ".
    • The sib() function uses the general sibling combinator "~".
    • The adj() function uses the adjacent sibling combinator "+".
      // produces CSS: p, .cls1, .cls2
      css.sel("p").or(this.cls1, this.cls2);
    
      // produces CSS: p, .cls1 + .cls2
      css.sel("p").or(this.cls1).adj(this.cls2);
    
      // produces CSS: p .cls1 > .cls2 > .cls3
      css.sel("p").desc(this.cls1).child(this.cls2, this.cls3);
    
  • Method attr() is used to construct an attribute selector and concatenate it with the current selector. The method accepts the attribute name and, optionally, attribute value, comparison operation, case-sensitivity flag and attribute namespace.
      // produces CSS: input[checked]
      css.sel("input").attr("checked");
    
      // produces CSS: audio[preload="metadata"]
      css.sel("audio").attr("preload", "metadata");
    
      // produces CSS: audio[href^="https"]
      css.sel("a").attr("href", "^=", "https");
    
  • Properties for every property-like pseudo entity, such as :hover, :focus, :enabled, ::after and so on. The property names correspond to the pseudo entity names in Camel form without the : or :: prefix.
      // produces CSS: a:first-child:visited:hover
      css.sel("a").firstChild.visited.hover;
    
  • Methods for every function-like pseudo entity, such as ::dir(), :is(), :nth-child(), ::slotted() and so on. The method names correspond to the pseudo entity names in Camel form without the : or :: prefix. Since CSS defines both property-like and method-like pseudo classes using the same name :host, Mimcss defines the method name host$() to distinguish it from the property host. The methods accept different parameters according to their definitions.
      // produces CSS: :is("header", "footer") > :is(.cls1, .cls2)
      css.sel().is("header", "footer").child().is(this.cls1, this.cls2);
    
      // produces CSS: p.cls1:nth-of-type(2n+1)
      css.sel("p").and(this.cls1).nthOfType(2,1);
    

Using Arrays

A selector can be represented by an array where each item is a selector itself - that is, object of CssSelector type. This is a simple way of composing compound selectors by concatenating simpler selectors. If you want to insert any combinators between the selectors, you need to add the combinators as items in the array:

// produces CSS: nav.cls1 > a
["nav", this.cls1, ">", "a"];

Since an array of selectors is a selector, arrays can be embedded in arrays with arbitrary levels of nesting. For example the array in the following example creates selector identical to that from the previous example:

// produces CSS: nav.cls1 > a
["nav", [this.cls1, [">", "a"]]];

Arrays are sometimes helpful when creating selectors manually; however, they are most useful when creating selectors programmatically.


Previous Unit: Defining Rules Next Unit: Defining Styles