Ladies and gentlemen…

Ladies and gentle­men, meet ELQ – The classy approach to Element Queries for modular and responsive components.

Demo
Docs

Why Element Queries?

Element queries are not for the faint hearted (read: not suitable for all projects). If your web app consists of modules that are (or should be) responsive – then this is most probably something for you! Otherwise, move on and come back another time :)


Why ELQ?

Short answer: because it's classy.

Since element queries have not been standardized (and not implemented in browsers), developers need to resort to JavaScript to provide element queries. There are numerous libraries that enable element queries in different ways, but here are the main selling points to why we believe ELQ is the best solution:

For a very detailed description of ELQ and element queries, read the Master's Thesis. The API and architecture is somewhat outdated, but the Thesis is still relevant for understanding ELQ.




Stop... It´s Demo time!

The most basic example

A simple component which changes color and border-radius depending on its size.




Nested modules

The modules are naturally nestable. Let's try nesting our demo-module.



Documentation

We haven't had time to write proper docs yet. What follows is really a minimal explanation of the API. ELQ has more power than meets the eye, you are more than welcome to look under the hood!

Install

Let's just assume the obvious, you want to install using npm. Here you go ;-)

npm install elq --save

Usage example

Once the dist/elq.js file is included into the HTML (it is built with UMD, so include it as you wish) it exposes a global function Elq. This is a constructor that creates ELQ instances. It is recommended to only use one instance per application.

JS

The main idea is to annotate elements with breakpoints of interest so that children can be conditionally styled in CSS by targeting the different breakpoint states. ELQ is bundled with three plugins as default, that let you annotate breakpoints as attributes of your elements like so:

When ELQ has processed the element, it will always have two classes, one for each breakpoint, that tells if the size of the element is greater or lesser than each breakpoint. For instance, if the element is 400 pixels wide, the element has the two classes elq-min-width-300px and elq-max-width-500px. Similarly, if the element is 200 pixels wide the element the classes are instead elq-max-width-300px and elq-max-width-500px. So for each breakpoint only the min/max part changes.

It may seem alien that the classes describe that the width of the element is both maximum 300 and 500 pixels. This is because we have taken a user-centric approach, so that when using the classes in CSS the API is similar to media queries. However, developers are free to change this API at will as Elq is plugin-based.

Now that we have defined the breakpoints of the element, we can conditionally style it by using the classes:

Nested modules

Regarding nested modules… things start's getting a little more complicated when you need to have control over the styling of a modules subelements with regards to the modules breakpoints. It is a bit tricky to scope the styling, but luckily not impossible. Consider the following example:

The selector .block.elq-min-width-210px > .block__element would work to select a subelement in modules that have a width of at least 210px. But this is not a nice selector, since it is dependentent on the structure of the DOM.

So, what alternatives are left? Well, you could of course activate ELQ on the subelements, which would give you total control… But in most cases, you just want the subelements to have the same breakpoint-classes as the whole module. That's why ELQ provides a plugin that lets you define elements to "mirror" the breakpoints classes of the nearest ancestor breakpoints element.

This is how you would use the mirror API.

And this is the result


Hello
World!

More to come…

Since we in the previous examples have annotated elements manually, the power and flexibility of the API have not been properly displayed. Only when combined with JavaScript, things get more interesting. To be continued...


Public API

An ELQ instance exposes the following public methods:

elq.getVersion()

Returns the version of instance.


elq.use(plugin)

Registers a plugin to be used by the instance. Parameter is required to be a plugin definition object.


elq.using(plugin)

Tells if the given plugin has been registered or not. Parameter can be a plugin name (of type string) or a plugin definition object.

Returns a Boolean..


elq.activate(elements)

Activates the given collection of elements. This triggers ELQ to perform its work so that all element queries are updated. Plugins are invoked, so that they can perform their logic. Resize listeners are also installed when propriate. Elements may be activated multiple times.

Parameter can be any collection of elements, and also accepts a single element. The are assumed to be ELQ-elements and it is not recommended to activate non-ELQ elements.


elq.listenTo([element], event, callback)

Registers a callback to be called for an event of an element. The element parameter is option, and if omitted the callback will be called when the event is emitted for any element.


Options

cycleDetection

When enabled, the cycle detection system tries to detect cyclic rules and breaks them if needed. When a cycle is detected, a console warning is printed. This may be helpful during development to catch cycles.


defaultUnit

Sets the default unit for all breakpoints that do not have a unit postfix. For instance, if defaultUnit is set to "em", the breakpoint 300 is interpreted as 300em while breakpoint 500px is still interpreted as 500px.


Architecture & Plugins

One our of contributions is to allow ELQ to be easily extended with plugins. For example, if annotating HTML is undesired it is possible to create a plugin that instead parses CSS. Likewise, if adding breakpoint classes to element is undesired it is possible to create a plugin that does something else when a breakpoint state of an element has changed. In order to enable such powerful behavior altering by plugins, extensibility has been the main focus when designing the ELQ architecture.

Plugin Definition Object

A plugin is defined by a plugin definition object and has the following structure:

All of the methods are invoked when registered to an ELQ instance. The getName and getVersion methods tells the name and version of the plugin. The isCompatible tells if the plugin is compatible with the ELQ instance that it is registered to. In the make method the plugin may initialize itself to the ELQ instance and return an object that defines the API accessible by ELQ and other plugins.

When necessary, ELQ invokes certain methods of the plugin API, if implemented, to let plugins decide the behavior of the system. Those methods are the following:

activate(element)

Called when an element is requested to be activated, in order for plugins to initialize listeners and element properties.


getElements(element)

Called in order to let plugins reveal extra elements to be activated in addition to the given element.


getBreakpoints(element)

Called to retrieve the current breakpoints of an element.


applyBreakpointStates(element, breakpointStates)

Called to apply the given breakpoint states of an element.




In addition, plugins may also listen to the following ELQ events:

resize(element)

Emitted when an ELQ element has changed size.


breakpointStatesChanged(element, breakpointStates)

Emitted when an element has changed breakpoint states (e.g., when the width of an element changed from being narrower than a breakpoint to being wider).


Flow

There are two main flows of the ELQ system; activating an element and updating an element. When ELQ is requested to activate an element, the following flow occurs:

  1. The element is initialized by installing properties and a system handling listeners.
  2. The getElements method of all plugins is called to retrieve any additional elements to activate. Additional elements will go through an own flow.
  3. The activate method of all plugins is called so that plugin specific initialized may occur.
  4. If any plugin has requested ELQ to detect resize events of the element, an resize detector is installed.
  5. The element is passed through the update flow.

The update flow is as follows:

  1. The getBreakpoints method of all plugins is called to retrieve all breakpoints of the element.
  2. Breakpoint states are calculated.
  3. If any state has changed since the previous update:
    1. Cycle detection is performed.
    2. The applyBreakpoints method of all plugins is called.
    3. The breakpointStatesChanged event is emitted.

Of course, there are options to disable some of the steps such as cycle detection and applying breakpoints. In addition to being triggered by the start flow and plugins, it is also triggered by element resize events.


Example Plugin Implementation

The API that enables developers to annotate breakpoints in HTML, as described in the usage section, is implemented as two plugins. One plugin parses the breakpoints of the element attributes and one plugin applies the breakpoint classes. The simplified code of the make method of the parsing plugin is as following:

The applying plugin simply implements the applyBreakpoints method to alter the className property of the element by the given breakpoint states.