0.0.7 • Published 3 years ago

responsive-style-attr v0.0.7

Weekly downloads
-
License
MIT
Repository
github
Last release
3 years ago

Responsive Style Attributes

Control the responsive style of html elements without creating one-off classes and media queries in your stylesheets. Here's a small demo.

Installation

npm i responsive-style-attr --save

The package contains three frontend builds:

  • dist/resp-style-attr.cjs.js - CommonJS bundle, suitable for use in Node.js
  • dist/resp-style-attr.esm.js - ES module bundle, suitable for use in other people's libraries and applications
  • dist/resp-style-attr.umd(.min).js - UMD build, suitable for use in any environment (including the browser, as a <script> tag)

The size of the minified script is ~9kb (~3.5kb gzipped)

There are two builds for headless operation:

  • dist/resp-style-attr-headless.cjs.js - CommonJS bundle, suitable for use in Node.js
  • dist/resp-style-attr-headless.esm.js - ES module bundle, suitable for use in other people's libraries and applications

Basic Usage

To enable responsive style on an element, just add a data-rsa-style-attribute containing a JSON object and call RespStyleAttr.init():

<h1 data-rsa-style='{"255px-to-512px" : "font-size: 1.5rem", "500px-up" : "font-size:2rem"}'>I change my font size according to screen width</h1>

<script>
    window.addEventListener('DOMContentLoaded', function () {
        RespStyleAttr.init();
    };
</script>

The data-attribute object's keys are expanded to media queries, the rules are put inside selectors that get wrapped by the media queries. The above example would expand to

@media all and (min-width: 255px) and (max-width: 511.98px) {
    .rsa-7063658802351566 {
        font-size: 1.5rem
    }
}

@media all and (min-width: 500px) {
    .rsa-8982493736072943 {
        font-size: 2rem
    }
}

The generated classes are applied to the <h1>-node. Note that all media queries and style rules are sorted and equalized to avoid duplicate selectors.

Media Query Shorthand Syntax

The media query shorthand syntax lets you combine multiple media query features, separated by an @-symbol. Examples:

/* "1000px" expands to: */
@media all and (min-width: 1000px) {
}

/* "255px-to-500px@portrait" expands to: */
@media all and (min-width: 255px) and (max-width: 499.98px) and (orientation: portrait) {
}

/* ... see the expansion spec file for more examples */

Why subtract .02px? Browsers don’t currently support range context queries, so we work around the limitations of min- and max- prefixes and viewports with fractional widths (which can occur under certain conditions on high-dpi devices, for instance) by using values with higher precision.

from the bootstrap 5.1 docs

Out of the box, this are supported query shortcuts in the object keys:

namesyntaxdescription
media typescreenmatches one or more given media types (screen,all,print,speech), is expected as first feature in shorthand!
orientationportraitmatches given orientation (portrait or landscape)
literal up800px-up or gt-800pxmatches viewports wider than the given value and unit
literal down500px-down or lt-500pxmatches viewports narrower than the given value and unit
literal between500px-to-1000pxmatches viewports between the two given values
ltelte-500pxmatches viewports narrower than or equal to given value
gtegte-500pxmatches viewports wider than or equal to given value

OR for different feature sets

You can use @,@ to split a shorthand key into multiple media queries. This is useful when you want to address vendor-specific features for the same style, for example -webkit-min-device-pixel-ratio and min-resolution.

Literal Features

To use a media query feature as is, just write it wrapped in parentheses, like this:

/* "lt-1000px@(prefers-color-scheme: dark)" would expand to: */
@media all and (max-width: 999.98px) and (prefers-color-scheme: dark) {
  /* .... */
}

Negations and MQL4 Boolean Operators

Neither is currently implemented but may be at a later time.

Using Breakpoint Sets in Shortcuts

In addition to the literal viewport size shortcuts, you can define breakpoint sets in your stylesheet as a CSS variable and use them in shortcuts. For example, a bootstrap 5 breakpoint set CSS variable would look like this:

html {
    --breakpoints-default: [["xs","0"], ["sm","576px"], ["md","768px"], ["lg","992px"], ["xl","1200px"], ["xxl","1400px"]];
}

This list is picked up by the breakpoint parser and enables the following shortcuts:

namesyntaxdescription
breakpoint onlymdmatches viewports between the given breakpoint and the next larger one (if a larger exists)
breakpoint upxs-up or gt-xsmatches viewports wider than the value of the given breakpoint
breakpoint downlg-down or lt-xsmatches viewports narrower than the value of the given breakpoint
breakpoint betweenmd-to-xlmatches viewports between the two given breakpoints
mixed betweenmd-to-1000px, 400px-to-xlmatches viewports between the given breakpoint and the literal value
ltelte-500pxmatches viewports narrower than or equal to given breakpoint
gtegte-500pxmatches viewports wider than or equal to given breakpoint

Controlling Breakpoint Sets

When using breakpoint sets, two additional data-attributes control which selector contains breakpoint set CSS variable and the name of the CSS variable:

data-rsa-selector[="html"]

The breakpoint set variable for the element will be picked off this selector.

data-rsa-key[="default"]

Controls the name of the CSS variable from which the breakpoint set is parsed:

html {
/* ^ the selector */
    --breakpoints-default: "json...";
    /* the key    ^^^^^^^ */
}

Example implementations

The src/scss folder contains example code for bulma, bootstrap and foundation to render each framework's breakpoint-map into your stylesheet.

Custom Shortcut Features

If you need to go deeper, you can create custom shortcut features that modify every feature of the media query. The custom features must be passed in the options-object of the init-function.

let options = {
    features: {
        androidOnly: function (mediaQuery) {
            // this will set the media type to "none" on devices that are not android
            if (!/android/i.test(navigator.userAgent)) {
                mediaQuery.media = 'none'
            }
        },
        uaMustMatch: function (mediaQuery, input) {
            //this will disable the the mediaquery if the useragent does not match input ...
            const re = new RegExp(input, 'i');
            if (!re.test(navigator.userAgent)) {
                mediaQuery.media = 'none';
            }
        }
    }
}

The custom features would be used like this:

<p data-rsa-style='{"androidOnly" : "border: 1px solid #000;"}'>I have a border on android devices</p>
<p data-rsa-style='{"usMustMatch(ios)" : "border: 1px solid #000;"}'>I have a border on iOs devices</p>

If you set a feature to true it will be written without a value. This is useful for setting features like prefers-reduced-motion where only the feature key is used in the query. Setting a feature to false will remove it from the final media query. A feature function can modify, set or remove more than one media query feature.

If you prefix the feature key with : the key will be omitted from the final media query and only the value will be used. This can be handy for things like level 4 range context where the expression is not in key: value format.

The full signature of a custom feature function is

/**
 * @param mediaQuery    object   media query map (object) that's currently constructed
 * @param inputArgs     string|undefined  string of the arguments 
 * @param currentKey    string   the key that's currently expanded
 * @param currentNode   HTMLElement|null  the node thats currently operated on, if instance runs in DOM context
 */
someFeature(mediaQuery, inputArgs, currentKey, currentNode) {
    //...
}

See test/expansion.spec.js for a few more examples.

Options

Pass options to the RespStyleAttr.init-function or to the RespStyleAttr.Css-constructor when creating instances manually. You can also set options for all instances by modifying the default options via RespStyleAttr.defaultOptions.

nametypedefaultdescription
debugboolfalsecontrols if verbose information is written to console
breakpointSelectorstring'html'the default breakpoint selector (compare data-rsa-selector)
breakpointKeystring'default'the default breakpoint key (compare data-rsa-key)
selectorTemplatefunctions => `.rsa-${s}`a small function that generates the selector used inside the generated stylesheet. Class is used by default but you could also create a data-attribute. Don't create ids because the same selector may be used for multiple elements.
selectorPropertyAttacherfunction(node, hash) => node.classList.add(`rsa-${hash}`)a function that actually attaches the property to the node.
attachStyleNodeTostring|HtmlElement'head'Selector or node to which the generated style node is attached
scopedStyleNodebooltruecontrols whether the style node has a scoped attribute
breakpointsArray|nullnullAlternative way of passing a breakpoint set to an instances (see "Breakpoint sets" for more information)
ignoreDOMboolfalseinstructs the instance to ignore the dom, only used for testing
alwaysPrependMediatypebooltruecontrols if the media type is always set on generated media queries
minMaxSubtractfloat0.02value that is subtracted from values in certain situations (see notice above)
useMQL4RangeContextboolfalseif enabled, screen width query features will be generated in new syntax

API

The RespStyleAttr Object provides the main class Css, and the helper functions init, refresh and get.

init

RespStyleAttr.init() will pick up all elements in your document that have a data-rsa-style-attribute and deploy the media queries and styles rules extracted from those attributes. Instances are created for each combination of key and selector attributes that are found on the nodes (or implied by default values). The default instance's key would be default_html.

If you pass options, they will be passed on to every instance created.

refresh

If you add more elements that use responsive style attributes to the document, you can call the RespStyleAttr.init() -method to process all new and unprocessed elements and deploy their styles.

get

RespStyleAttr.get() will yield a map of all instances. Pass an instance key to get only that instance.

Manually Creating Instances

Just call let myRSAInstance = new RespStyleAttr.Css() to create a new instance. You should pass an options-object containing at least the breakpointSelector and breakpointKey properties.

If the constructor detects that an instance with the same instance key (consisting of given breakpoint key and breakpoint selector) already exists in the internal instance map, that instance will be refreshed and returned. You also can call refresh on an existing instance.

Events

There is currently only one event supported: rsa:cssdeployed will be dispatched on the <style>-node that belongs to the instance. You can use it like this:

window.addEventListener('rsa:cssdeployed', e => {
    console.log(e.detail);
    //is a reference to `Css`-instance that dispatched the event
})

Preventing FOUC

If you want to prevent FOUC, add the class rsa-pending to your elements. When the stylesheet is deployed, the class is removed from each elements' class list. Since the nodes usually aren't measured etc during style creation, just use:

.rsa-pending{ display: none }
/* or */
.rsa-pending{ visibility: hidden }

Headless

The "headless" variant lets you generate stylesheets off of document fragments. It does not rely on the DOM so you can run it in a node environment. Since classList is not available, data-attributes and data-attribute selectors are generated by default. The headless variant also supports an extra option

nametypedefaultdescription
removeDataAttributeboolfalsewhen true, occurences of data-rsa-style="..." are removed from the given fragment

Usage

Since Headless extends Css, it takes the same options. Parse a fragment like this:

const {Headless} = require('...path-to/resp-style-attr-headless.cjs'),
        instance = new Headless(),
        someHTML = `<div data-rsa-style='{"lt-400px":"border: 1px solid #000"}'></div>`;

instance.parse(someHTML);
// -> <div data-rsa-style='{"lt-400px":"border: 1px solid #000"}' data-rsa-3523518655946362></div>

// passing true as the second arg will remove the original data-attribute
instance.parse(someHTML, true);
// -> <div data-rsa-3523518655946362></div>

You can call parse on the same instance repeatedly or pass an entire document. parse's output will be the input fragment but with selector attributes added.

Adding styles directly is also supported, simply pass an object or json string to the push method and receive a list of hashes, that you can convert into attributes and attach them yourself:

push is also supported by the browser variant!

//...
instance.push('{"gt-800px":"background:#f00;"}');
//-> ['data-rsa-6088263273057222']
instance.push({"gt-1000px":"background:#00f;","portrait" : "padding:20px" });
//-> ['data-rsa-366898066636896', 'data-rsa-2976456585877488']

Finally, you can fetch the css that has been generated from all styles by calling getCss or get a style node by calling getStyleSheet.

//...
instance.getCss();

// -> @media all and (max-width: 399.98px){
//      [data-rsa-3523518655946362]{ border:1px solid #000 }
//    }
//    @media all and (min-width: 800px){
//      [data-rsa-6088263273057222]{ background:#f00 }
//    }
//    @media all and (min-width: 1000px){
//      [data-rsa-366898066636896]{ background:#00f }
//    }
//    @media all and (orientation: portrait){
//      [data-rsa-2976456585877488]{ padding:20px }
//    }
0.0.7

3 years ago

0.0.6

3 years ago

0.0.5

3 years ago

0.0.4

3 years ago

0.0.3

3 years ago

0.0.2

3 years ago

0.0.1

3 years ago