Skip to content
This repository has been archived by the owner on Sep 16, 2023. It is now read-only.
/ style-producer Public archive

Produces and dynamically updates stylesheets

License

Notifications You must be signed in to change notification settings

frontmeans/style-producer

Repository files navigation

Style Producer

NPM Build Status Code Quality Coverage GitHub Project API Documentation

Produces and dynamically updates stylesheets right in the browser.

Usage example:

import { produceStyle, stypObjectFormat, stypRoot } from '@frontmeans/style-producer';

const root = stypRoot(); // Create root CSS rule
const h1 = root.rules.add(
  { e: 'h1' }, // Define CSS rule for `h1` element
  {
    fontSize: '24px',
  },
);

// ...add more CSS rules

const supply = produceStyle(root.rules, stypObjectFormat()); // Produce stylesheets

// Update CSS rule. Stylesheet will be updated automatically
h1.add({
  fontWeight: 'bold',
  fontSize: '22px',
});

supply.off(); // Remove produced stylesheets

Structured CSS Selectors

Style Producer utilizes structured CSS selectors implemented by Doqry.

CSS Rules

CSS rules are represented by StypRule class and organized in hierarchy.

The top of the hierarchy is root CSS rule. It may be constructed by stypRoot() function.

Rules are added to the hierarchy using StypRule.rules.add() method like this:

const nested = rule.rules.add(selector, properties);

where selector is structured CSS selector, and properties is CSS properties map.

CSS rule may be removed from hierarchy with StypRule.remove() method.

StypRule.rules property contains a dynamically updated CSS rule list including all rules in hierarchy starting from current one. To obtain only rules directly nested within current one use a StypRule.rules.nested property.

It is possible to grab a subset of matching rules using a StypRule.rules.grab() method:

rule.rules.grab({ e: 'button' }); // Grab all CSS rules for the `button` element.

CSS Properties

CSS properties are represented by object with camel-cased property names and their corresponding string, scalar, or structured values:

const cssProperties = {
  position: 'fixed',
  display: 'block',
  width: '100%',
  height: '120px',
  borderBottom: '1px solid black',
};

CSS rule may be constructed with initial properties. Properties may be appended using StypRule.add() method, replaced using StypRule.set() method, or removed all together using StypRule.clear() method.

CSS properties with names started with anything but ASCII letter are not rendered as CSS. Still, they can be referenced and used internally.

CSS property string values ending with !important suffix are recognized as having !important priority. Note that the order of properties is meaningful. But important property values are always take precedence over non-important ones.

CSS rule properties may be defined by EventKeeper instance that may update properties dynamically.

Type-Safe CSS Properties

Apart from being scalars and strings, CSS property values may be structured. I.e represented by objects implementing StypValueStruct interface.

There are several implementations of structured values available:

Any custom implementation can be added.

It is possible to declare CSS properties structure to work with them in type safe manner. For that declare properties interface and use StypMapper to map arbitrary CSS properties to that interface, or StypRuleRef to access CSS rule in type safe manner:

import { RefStypRule, StypColor, StypLengthPt, StypRGB, stypRoot } from '@frontmeans/style-producer';
import { mapAfter } from '@proc7ts/fun-events';

// Type-safe CSS properties representing custom settings
interface MySettings {
  $color: StypColor;
  $bgColor: StypColor;
  $gap: StypLengthPt;
}

// Construct a mapping function for custom settings
const MySettings = RefStypRule.by<MySettings>(
  { $: '.my-settings' }, // Selector of CSS rule containing settings
  {
    // Mappings for settings
    $color: new StypRGB({ r: 0, g: 0, b: 0 }), // Text is black by default
    $bgColor: new StypRGB({ r: 255, g: 255, b: 255 }), // Background is white by default
    $gap: StypLengthPt.of(4, 'px'), // Gaps are 4 pixels by default
  },
);

// CSS rules root
const root = stypRoot();

// Settings CSS rule reference
const mySettingsRef = MySettings(root);

// Define `<body>` style
root.add(
  mySettingsRef.read.do(
    mapAfter(({ $color, $bgColor, $gap }) => ({
      color: $color, // Apply default text color
      backgroundColor: $bgColor, // Apply default background color
      padding: $gap, // Padding is based on default gap
    })),
  ),
);

// Define `<input>` element style based on default settings.
root.rules.add(
  { e: 'input' },
  mySettingsRef.read.do(
    mapAfter(({ $color, $bgColor, $gap }) => ({
      color: $color,
      backgroundColor: $bgColor.hsl.set(hsl => ({ l: hsl.l * 0.85 })), // Convert to HSL and darken input background
      padding: `${$gap} ${$gap.mul(1.5)}`, // Padding is based on default gap
      border: `1px solid ${$color}`,
    })),
  ),
);

// Make text dark grey
mySettingsRef.set({
  $color: new StypRGB({ r: 192, g: 192, b: 192 }), // Override default value
});

Producing CSS

Stylesheets can be produced by produceStyle() function. It accepts a dynamically updated list of CSS rules (e.g. StypRules.rules) and production format.

The stypObjectFormat() format creates a <style> element per CSS rule inside a document head. This element's stylesheet is filled and updated with that rule contents. Once the rule is removed the corresponding <style> element is removed too.

When CSS is no longer needed an off() method of the EventSupply instance returned from produceStyle() function may be called. That would remove all <style> elements.

CSS Renderers

Style production is performed by CSS renderers that may be specified as renderer production format option.

By default, all renderers are enabled. This may be not what is needed. In that case a produceBasicStyle() function can be called instead. The latter enables CSS properties rendering only. The rest of the necessary renderers may be specified with the renderer production format option. This may reduce the final bundle size a bit.

Raw CSS Text

CSS rule properties may be specified as raw text. This is the same as specifying a special $$css property.

The stypRenderText renderer treats this text as plain CSS. This text is rendered before the rest of the properties, so the latter take precedence.

@media and other at-rules

There is no dedicated @media properties in structured CSS selector. However, a stypRenderAtRules renderer recognizes selector qualifiers as at-rules and renders corresponding rules. So, CSS rule with selector like this: { c: 'screen-only', $: '@media=screen' } would be rendered as

@media screen {
  .screen-only {
    /* CSS properties */
  }
}

stypRenderAtRules renderer treats all qualifiers starting with @ as at-rule qualifiers. So the qualifier name may be e.g. @keyframes. The value of qualifier (if present) is used as at-rule query.

It is also possible to specify at-rule query as CSS property. For that the property name should be the same as qualifier one. E.g. if CSS rule selector is { c: 'screen-only', $: '@media:sr' } and CSS rule properties contain { '@media:sr': 'screen' }, the rendered CSS would be the same as above. This technique makes it possible to dynamically update the at-rule queries.

These rules are rendered by stypRenderGlobals renderer. This renderer interprets properly named CSS properties and renders corresponding CSS rules.

@import:url property value is treated as media query and appended after stylesheet URL. I.e.

{
  "@import:path/to/included.css": "screen"
}

becomes

@import url(path/to/included.css) screen;

@namespace property value is treated as default namespace URL. I.e.

{
  "@namespace": "http://www.w3.org/1999/xhtml"
}

becomes

@namespace url(http://www.w3.org/1999/xhtml);

@namespace:prefix property value is treated as namespace URL with the given prefix. I.e

{
  "@namespace:svg": "http://www.w3.org/2000/svg"
}

becomes

@namespace svg url(http://www.w3.org/2000/svg);

Namespaces

It is possible to specify namespaces for CSS selector elements. I.e. not only XML element namespace, but also the one for HTML element name, element identifier, or element class.

Then the unique namespace alias will be applied to original name or identifier. This can be used to avoid naming conflicts.

Example:

import { NamespaceDef } from '@frontmeans/namespace-aliaser';
import { stypRoot } from '@frontmeans/style-producer';

// Declare custom namespace
const customNs = new NamespaceDef(
  'https://wesib.github.io/elements', // Unique namespace URL
  'b', // Preferred namespace aliases, from most wanted to less wanted
  'wesib',
);

const root = stypRoot(); // Root CSS rule
// Declare styles for custom element
const rule = root.rules.add(
  { e: ['button', customNs] },
  {
    background: 'gray',
  },
);

The code above would add a unique prefix to the button element. E.g. by making it b-button.

The i (element identifier) and c (element class name(s)) properties accept namespaced values too.

The ns (XML namespace) property accepts namespace (NamespaceDef) value instead of plain string. The corresponding @namespace rule would be rendered automatically by stypRenderXmlNs renderer.