- Overview
- Installation
- Documentation - Sass
- Documentation - JS
- Creating a Theme
- Development
- Changelog
A front-end framework for creating modular, configurable and scalable UI components.
Synergy is predominantly a Sass framework but also has an optional JavaScript add-on.
For three reasons:
Using BEM (example source):
panels-list__item panels-list__item--blog panels-list__item--featured panels-list__item--no-summary panels-list__item--image
Using Synergy:
panelsList_item-blog-featured-noSummary-image
Using Synergy with original BEM syntax:
panels-list__item--blog--featured--no-summary--image
Synergy takes advantage of CSS attribute wildcard selectors. By looking for classes which contain certain strings as opposed to looking for specific classes, your markup can be much more flexible, allowing you to chain modifiers and components, removing the need for any repetition (i.e. no more 'button button--large button--round').
Configure your modules without touching the source code. Call the Sass mixin and pass your options to it, leaving the module's source code untouched, allowing you to easily change options and styles.
@mixin buttons($custom: ()) {
// Default module configuration
$buttons: config((
'padding': 8px,
'radius' : 6px
), $custom);
@include module('button') {
display: inline-block;
padding: this('padding');
border-radius: this('radius');
}
}
@include buttons((
'padding': 0.75em,
'radius' : 3px
));
.button, [class*="button-"] {
display: inline-block;
padding: 0.75em;
border-radius: 3px;
}
Given a folder structure similar to the following:
|-- modules
| |-- grid
| | |-- _grid.scss
| | |-- grid.js
| | |-- grid.json
Inside grid.json
:
{
"grid": {
"breakpoints": {
"break-0" : "0px",
"break-1" : "460px",
"break-2" : "720px",
"break-3" : "940px",
"break-4" : "1200px",
"break-5" : "1400px"
}
}
}
Inside _grid.scss
:
@import 'grid.json'; // config is now accessible under the `$grid` variable
$breakpoint_tablet: map-get-deep($grid, 'breakpoints', 'break-3');
@media (min-width: $breakpoint_tablet) {
// do something when screen width is at least 940px
}
Inside grid.js
:
import config from './grid.json';
const breakpoint_tablet = config.grid.breakpoints['break-3'];
if (window.matchMedia(`(min-width: ${breakpoint_tablet})`).matches) {
// do something when screen width is at least 940px
}
- Sass 3.4+
- Sass JSON Vars[1]
- Node.js[1]
[1]This is only required if you intend on using JSON files to store configuration (i.e. you wish to share config between JS and Sass).
Ensure your paths are correct as they may differ from below
yarn add Synergy
@import '../node_modules/Synergy/dist/synergy';
npm install Synergy --save
@import '../node_modules/Synergy/dist/synergy';
bower install Synergy --save
@import '../bower_components/Synergy/dist/synergy';
git clone https://github.com/esr360/Synergy.git
@import '../Synergy/dist/synergy';
git submodule add https://github.com/esr360/Synergy.git vendor
@import '../vendor/Synergy/dist/synergy';
@import 'PATH/TO/synergy';
Install the Sass JSON Vars Ruby Gem:
gem install sass-json-vars
Require/import the Synergy module into your project's JS (the below assumes you have installed Synergy using either NPM or Yarn):
import Synergy from 'synergy';
Or:
var Synergy = require('synergy');
N.B you will need a module bundler like Webpack in order to require/import modules.
You will also need to configure your Sass compiler to use Sass JSON Vars as the importer:
sass /PATH/TO/app.scss -r sass-json-vars
N.B Synergy requires Ruby Sass, so if using Grunt you should use the grunt-contrib-sass compiler.
sass: {
dist: {
options: {
require: 'sass-json-vars'
},
files: {...}
}
}
N.B Synergy requires Ruby Sass, so if using Gulp you should use the Gulp Ruby Sass compiler.
gulp.task('sass', function () {
return gulp.src('/PATH/TO/app.scss ')
.pipe(sass({require: 'sass-json-vars'}).on('error', sass.logError))
.pipe(gulp.dest('./css'));
});
- Bool Options
- Non-Bool Options
- Hybrid Options
- Nested Options
- Including Your Module
- Passing Custom CSS to Your Module
- Global Configuration
Inside _header.scss
@mixin header($custom: ()) {
// Merge default config with custom values
$header: config((
'name' : 'header',
'fixed' : false,
'background' : #000000,
'wrapper-width' : 960px
), $custom);
@include module {
background: this('background');
@include modifier('noLogo') {
@include module('logo') {
display: none;
}
}
@include component('wrapper') {
width: this('wrapper-width');
}
@include option('fixed') {
position: fixed;
}
}
}
Wherever you want to output the CSS, in the same or another file...
@include header();
To modify the default options, pass them to the mixin with the new value:
@include header((
'background' : blue,
'wrapper-width' : 90%
));
.header,
[class*='header-'] {
background: blue;
}
[class*='header-'][class*='-noLogo'] .logo,
[class*='header-'][class*='-noLogo'] [class*='logo-'] {
display: none;
}
.header_wrapper,
[class*='header_wrapper-'] {
width: 90%;
}
[class*='header-'][class*='-fixed'] {
position: fixed;
}
Your markup for the above module may now look something like the following:
<div class="header">
<div class="header_wrapper">
<div class="logo">...</div>
...
</div>
</div>
If you want to hide the logo, you can add the noLogo
modifier to the header:
<div class="header-noLogo">
...
</div>
If you want to set the header's position to fixed
, there are two ways you can do this. Firstly, you can again add the appropriate modifier to the markup:
<div class="header-fixed">
...
</div>
N.B. Modifiers can be chained in any order:
<div class="header-fixed-noLogo">
...
</div>
Or you can set the header to be fixed (without the need of a modifier) by passing the option to the mixin when calling it:
@include header((
'fixed' : true
));
Then just:
<div class="header">
...
</div>
The module()
mixin is what generates the selectors for your module. The mixin accepts 2 parameters:
$modules
{string|list} - the name of your module(s) (optional)$type
{string} - this defines how the mixin generates the selectors for your component(s) ['flex'] (optional)
@include module('header') {
...
}
If $modules
is not defined, it will look for a name
value in your module's config. This is an alternative way of using the module()
mixin:
@mixin header($custom: ()) {
$header: config((
'name' : 'header'
), $custom);
@include module {
...
}
}
$modules
is usually a single value but can also be a list, eg. ('header', 'footer')
, should you wish to apply styles to more than one module. For such instances, an alias mixin of modules()
is available:
@include modules(('header', 'footer')) {
...
}
$type
can be one of three values: flex
(default), chain
or static
. By default, flex
is enabled for all componenets. To globally change the default type from flex
to something else, pass your specified value to the $selector-type
variable before importing Synergy.
// Re-define default selector type
$selector-type: 'chain';
// Import Synergy
@import 'path/to/synergy'
// Modules
...
@include module('header', 'flex') {
...
}
This is the default value for a module; it creates selectors for both .module
and [class*="module-"]
, allowing you to use both the naked module as well as modifiers. Whilst this is the most flexible option, it does mean the generated CSS is slightly greater, which is what the other 2 options are for.
.header, [class*="header-"] {
...
}
If using the global default
$type
value offlex
, you do not need to pass a second parmeter here to achieve theflex
option
@include module('header', 'chain') {
...
}
[class*="header-"] {
...
}
The chain option should be used if you are looking to optimise your CSS output, and you know your module will not exist as a naked selector without modifiers. Ie - this option outputs only [class*="module-"]
, thefore you cannot use .module
to achieve any styles.
@include module('header', 'static') {
...
}
.header {
...
}
The static option creates only the naked selector for your module; ie - .selector
, meaning no modifiers or components can be used. This option is only available for consistency; it could make more sense to just write .module
instead of using the mixin in this case - I'll let you think about that one.
It is possible to nest modules within one another:
@include module('header') {
@include module('button') {
...
}
}
.header .button, .header [class*='button-'],
[class*='header-'] .button, [class*='header-'] [class*='button-'] {
...
}
@include modules(('header', 'footer'), 'static') {
...
}
@include module('header', 'static') {
...
}
@include module('footer') {
...
}
.header, .footer {
...
}
.header {
...
}
.footer, [class*="footer-"] {
...
}
Because of how the selectors are generated, it is not possible to create relating modules which begin with the same namespace. For example, if you have a header
module with the default $type
of flex
, any classes which contain header-
will receive the core header styles, so if you were to create a header-button
element, this would inherit the header
styles, as you are telling Synergy you want a header module with a button modifier. There are several options to get around this, including:
- camelCase (headerButton)
- reversed wording (button-header)
- underscore (header_button)
To keep things as similar to BEM as possible, Synergy provies an easy way to create relating components using underscores, eg - header_wrapper
. The component
mixin accepts 2 parameters:
$components
{string|list} - the name of your component(s) [null] (optional)$glue
{string} - the glue to chain components to modules ['_'] (required)
@include module('header') {
...
@include component('wrapper') {
...
}
}
<div class="header_wrapper">...</div>
.header, [class*="header-"] {
...
}
.header_wrapper, [class*="header_wrapper-"] {
...
}
Components work like regular modules, in the sense that you can add modifiers:
@include module('header') {
...
@include component('wrapper') {
...
@include modifier('fullscreen') {
...
}
}
}
<div class="header_wrapper-fullscreen">...</div>
.header, [class*='header-'] {
...
}
.header_wrapper, [class*='header_wrapper-'] {
...
}
[class*='header_wrapper-'][class*='-fullscreen'] {
...
}
@include module('footer') {
...
@include components(('nav', 'copyright')) {
...
}
}
<div class="footer">
<div class="footer_nav">...</div>
<div class="footer_copyright">...</div>
</div>
.footer, [class*='footer-'] {
...
}
.footer_nav, [class*='footer_nav-'],
.footer_copyright, [class*='footer_copyright-'] {
...
}
By not passing a parameter to the component()
mixin, you can apply styles to all components of the parent module:
@include module('widget') {
@include component {
@include modifier('inline') {
...
}
}
@include component('icon') {
...
}
@include component('header') {
...
}
}
<div class="widget">
<div class="widget_icon-inline">...</div>
<div class="widget_header-inline">...</div>
</div>
[class*='widget_'][class*='-inline'] {
content: 'foo';
}
.widget_icon, [class*='widget_icon-'] {
content: 'foo';
}
.widget_header, [class*='widget_header-'] {
content: 'foo';
}
If you want to use a different string to chain components to modules, you can pass the $glue
parameter when including the module:
@include module('header') {
@include component('wrapper', $glue: '__') {
...
}
}
.header__wrapper, [class*='header__wrapper-'] {
...
}
To globally change the component glue, pass the $component-glue
variable with before importing Synergy.
// Set custom component glue
$component-glue: '__';
// Import Synergy
@import "path/to/synergy"
// Modules
...
It is possible to nest components within one another:
@include module('header') {
@include component('user') {
@include component('profile') {
...
}
}
}
.header_user_profile, [class*='header_user_profile-'] {
...
}
The modifier()
mixin generates the selector for any modifiers for your module, for example a small or large modifier. This mixin accepts the following paramters:
$modifiers
{string|list} - the name of the desired modifier(s) (required)$special
{string} - set a special operator (optional)$glue
{string} - the glue to chain components to modules ['_'] (required)
@include module('button') {
...
@include modifier('small') {
font-size: 0.75em;
}
@include modifier('large') {
font-size: 1.5em;
}
}
<div class="button">Button</div>
<div class="button-small">Button</div>
<div class="button-large">Button</div>
.button, [class*="button-"] {
...
}
[class*="button-"][class*="-small"] {
font-size: 0.75em;
}
[class*="button-"][class*="-large"] {
font-size: 1.5em;
}
The modifier()
mixin is infinitely nestable allowing you to require more than one modifier for styles to take effect:
@include module('header') {
...
@include modifier('side') {
position: absolute;
@include modifier('left') {
left: 0;
}
@include modifier('right') {
right: 0;
}
}
}
<div class="header-side-left">...</div>
.header, [class*="header-"] {
...
}
[class*="header-"][class*="-side"] {
position: absolute;
}
[class*="header-"][class*="-side"][class*="-left"] {
left: 0;
}
[class*="header-"][class*="-side"][class*="-right"] {
right: 0;
}
You can use any number of modifiers on a single element in the HTML, and in any order, for example:
<div class="button-large-round-primary">...</div>
<div class="button-primary-large-round">...</div>
@include module('button') {
...
@include modifiers(('buy-now', 'add-to-basket')) {
text-transform: uppercase;
}
@include modifier('buy-now') {
...
}
@include modifier('add-to-basket') {
...
}
}
.button, [class*="button-"] {
...
}
[class*="button-"][class*="-buy-now"],
[class*="button-"][class*="-add-to-basket"] {
text-transform: uppercase;
}
[class*="button-"][class*="-buy-now"] {
...
}
[class*="button-"][class*="-add-to-basket"] {
...
}
If you want to use a different string to chain modifiers to modules/components, you can pass the $glue
parameter when including the modifier:
@include module('button') {
@include modifier('large', $glue: '--') {
...
}
}
[class*="button--"][class*="--large"] {
...
}
To globally change the modifier glue, pass the $modifier-glue
variable with before importing Synergy.
// Set custom modifier glue
$modifier-glue: '--';
// Import Synergy
@import "path/to/synergy"
// Modules
...
This mixin allows you to extend multiple modifiers into a new, seperate modifer, essentially combining several modifiers into one.
The extend mixin takes the following parameters:
$modifiers
{string|list} $modifiers [null] - The modifiers which you wish to combine$parent
string} $parent [null] - The target parent module if not the current one$core
{bool} $core [false] - Extend the core '.module' styles?
@include module('button') {
...
@include modifier('round') {border-radius: 6px}
@include modifier('large') {font-size: 2em}
@include modifier('success') {color: green}
@include modifier('primary') {
@include extend(('round', 'large', 'success'))
}
}
<div class="button-primary">...</div>
.button, [class*="button-"] {
...
}
[class*="button-"][class*="-round"],
[class*="button-"][class*="-primary"] {
border-radius: 6px;
}
[class*="button-"][class*="-large"],
[class*="button-"][class*="-primary"] {
font-size: 2em;
}
[class*="button-"][class*="-success"],
[class*="button-"][class*="-primary"] {
color: green;
}
@include module('list') {
...
@include modifier('reset') {
list-style: none;
margin-left: 0;
}
}
@include module('tabs') {
...
@include component('nav') {
@include extend($parent: 'list', $modifiers: 'reset');
}
}
.list, [class*='list-'] {
...
}
[class*='list-'][class*='-reset'],
.tabs_nav, [class*='tabs_nav-'] {
list-style: none;
margin-left: 0;
}
.tabs, [class*='tabs-'] {
...
}
This only extends the list's modifier, in order to extend the core styles as well, the $core
paramater should be passed as true
:
@include module('tabs') {
@include component('nav') {
@include extend($parent: 'list', $modifiers: 'reset', $core: true);
}
}
.list, .tabs_nav,
[class*='tabs_nav-'], [class*='list-'] {
...
}
[class*='list-'][class*='-reset'],
.tabs_nav, [class*='tabs_nav-'] {
list-style: none;
margin-left: 0;
}
.tabs, [class*='tabs-'] {
...
}
For usages like the above, an alias mixin of _module()
is available. This is to provide a more semantic way of achieving the above task, allowing you to pass the $parents
parameter, which is normally the second parameter, as the first, and also has a default $core
value of true
:
@include module('tabs') {
@include component('nav') {
@include _module('list', 'reset');
}
}
The context()
mixin allows you to apply styles to your module when certain conditions are met. This mixin accepts 1 parameter:
$context
- the name of the predefined condition you wish to be met (required)
The following conditions can be passed to the mixin:
parent-hovered
- apply styles to a component when the parent module is hovered- more coming soon
@include module('widget') {
@include component('icon') {
color: blue;
@include context('parent-hovered') {
color: white;
}
}
}
.widget_icon, [class*='widget_icon-'] {
color: blue;
}
.widget:hover .widget_icon,
.widget:hover [class*='widget_icon-'],
[class*='widget-']:hover .widget_icon,
[class*='widget-']:hover [class*='widget_icon-'] {
color: white;
}
As outlined in the overview section, Synergy allows you to configure your modules with customizable options.
@mixin header($custom: ()) {
$header: config((
'bg-color' : black,
'top' : 50px
), $custom);
@include module('header') {
background-color: this('bg-color');
margin-top: this('top');
}
}
.header, [class*='header-'] {
background-color: black;
margin-top: 50px;
}
For all intents and purposes, there are 2 types of options; bools and non-bools. A bool option is one whose value determines whether or not some code should be applied. A non-bool option is one whose value is used as a value for a CSS property. In the below example there is one of each.
@mixin header($custom: ()) {
$header: config((
'dark' : false,
'top' : 50px
), $custom);
@include module('header') {
margin-top: this('top');
@include option('dark') {
background-color: black;
}
}
}
.header, [class*='header-'] {
margin-top: 50px;
}
[class*='header-'][class*='-dark'] {
background-color: black;
}
Your configuration can be infinitely nested, like so:
@mixin global($custom: ()) {
$global: config((
// Options
'typography': (
'sizes': (
'size-1' : 1em,
'size-2' : 1.2em,
'size-3' : 1.6em
),
'colors': (
'primary' : red,
'secondary' : blue,
'validation' : (
'valid' : #19d36d,
'invalid' : #d32828
)
)
)
), $custom) !global;
...
}
If your option is a bool, you can use the option()
mixin. The styles added within this mixin will automatically be applied to the module if the option is set to true.
@mixin header($custom: ()) {
$header: config((
'dark' : false,
'top' : 50px
), $custom);
// styles will be applied if 'dark' is set to 'true'
@include option('dark') {
...
}
}
You can alternatively pass the bool value to your option like so:
@mixin header($custom: ()) {
$header: config((
'dark':(
'enabled': false
),
'side':(
'enabled': left,
'background': black
),
'top': 50px
), $custom);
...
}
This allows you to pass other options to the setting.
Since by default adding a setting will also create a modifier for the setting, you can apply the styles by adding the modifier to your HTML tag, regardless of the setting's value:
<div class="header-dark">
...
</div>
If you are watching your CSS output, you may wish to remove these modifiers (and related selectors) from the generated styles and only use them conditionally. To do so, you can pass the extend-options
option to your module's config, and set it to false:
@mixin header($custom: ()) {
$header: config((
'extend-options': false,
'dark' : false,
'top' : 50px
), $custom);
...
}
To set this option globally for all modules, use the $extend-options
variable before importing Synergy:
// Disable creation of modifiers for module settings
$extend-options : false;
// Import Synergy
@import "path/to/synergy"
// Your Modules
...
If your option is a CSS property, to call the option in your module the this()
function is used, like so:
margin-top: this('top');
which will generate:
margin-top: 50px;
If your desired value is nested, such as:
'breakpoints': (
'break-1': 420px
...
)
It would be fetched it like this:
this('breakpoints', 'break-1');
In some cases, you may require a hybrid of the above 2 options. You may have a set of styles you wish to use conditionally, and you may wish for these styles to vary depending on the value passed. Let's look at the following example - imagine your website has a side header, and you want to easily change whether it appears on the left or right hand side:
@mixin header($custom: ()) {
$header: config((
'side' : false // left or right
), $custom);
@include module('header') {
@include option('side') {
// core side header styles
@include value('left') {
// left side styles
}
@include value('right') {
// right side styles
}
}
} // module('header')
} // @mixin header
@include header();
[class*='header-'][class*='-side'] {
...
}
[class*='header-'][class*='-side'][class*='-left'] {
...
}
[class*='header-'][class*='-side'][class*='-right'] {
...
}
And setting the value to left
:
@include header((
'side': left
));
.header, [class*='header-'] {
...
}
.header, [class*='header-'],
[class*='header-'][class*='-side'][class*='-left'] {
...
}
[class*='header-'][class*='-side'][class*='-right'] {
...
}
The above example inserts an optional set of styles if side
is set to anything other than false. Depending on the value of your option, we can choose to include additional styles by using the value()
mixin. Again, by default these options are extended as modifiers so you can use them regardless of the setting's value:
<div class="header-side-left">..</div>
<div class="header-side-right">..</div>
If you've completely followed this documentation so far you may have already picked up on the fact you can also use:
<div class="header-left-side">..</div>
<div class="header-right-side">..</div>
And just to reiterate, with the side
option set to either left or right in the above example, you don't need to pass any modifiers to the HTML, we just use:
<div class="header">...</div>
In some circumstances, we can achieve the same thing without having to use the option()
mixin. Consider the above example; "left" and "right" are both also CSS properties, so we can pass the setting's value as a CSS property:
@mixin header($custom: ()) {
$header: config((
'side' : left;
), $custom);
@include module('header') {
@include option('side') {
// Side-Header Styles
...
#{this('side')}: 0; // left: 0;
}
} // module('header')
} // @mixin header
The above example is assuming we have a setup where the header's position is controlled via:
left: 0;
for a left headerright: 0;
for a right header
Our module is now ready to be included; to include the module with the default settings you have created, all that's required is:
@include header;
To include your header with customised options, this is done like so:
@include header((
'dark' : true,
'side' : left,
'top' : 0
));
If you want to pass custom CSS properties to a module, component or modifier, but don't want to add these properties to the source file, you can do this by passing your styles to the CSS
option when including your module:
@include buttons((
...
'CSS': (
'letter-spacing': -1px,
'text-transform': uppercase
)
));
.button, [class*="button-"] {
...
letter-spacing: -1px;
text-transform: uppercase;
}
If you need to pass styles to a component of a module, preprend the key of your property with the component glue (default is '_'):
@include buttons((
...
'CSS': (
'_wrapper': (
'overflow': hidden,
'margin-bottom': 10px
)
)
));
.button, [class*="button-"] {
...
}
.button_wrapper, [class*="button_wrapper-"] {
overflow: hidden;
margin-bottom: 10px;
}
If you need to pass styles to a modifer of a module or component, preprend the key of your property with the modifier glue (default is '-'):
@include buttons((
...
'CSS': (
'-foo': (
'text-transform': uppercase
)
)
));
.button, [class*="button-"] {
...
}
[class*='button-'][class*='-foo'] {
text-transform: uppercase;
}
You can target modules and components to an infinite depth:
@include buttons((
...
'CSS': (
'_foo': (
'content': 'alpha' ,
'-bar': (
'content': 'beta',
'-baz': (
'content': 'gamma'
)
)
)
)
));
.button, [class*="button-"] {
...
}
.button_foo, [class*='button_foo-'] {
content: 'alpha';
}
[class*='button_foo-'][class*='-bar'] {
content: 'beta';
}
[class*='button_foo-'][class*='-bar'][class*='-baz'] {
content: 'gamma';
}
What if you want to create a module whose options can be accessed by other modules? For example, say you have a module for your grid system and have configured some breakpoint values - you then may wish to access these values from throughout your project:
@mixin grid($custom: ()) {
$grid: ((
'breakpoints': ((
'break-1': 420px,
'break-2': 740px,
'break-3': 960px,
'break-4': 1200px
));
), $custom);
...
} // @mixin grid
This is entirely possible, and requires the addition of the !global
flag:
@mixin grid($custom: ()) {
$grid: ((
'breakpoints': ((
'break-1': 420px,
'break-2': 740px,
'break-3': 960px,
'break-4': 1200px
));
), $custom) !global;
...
} // @mixin grid
// Mixin to easily access breakpoints map
@function breakpoint($breakpoint) {
@return option($grid, 'breakpoints', $breakpoint);
}
The option()
function is used to get values from another module's configuration, like so:
width: option($grid, 'breakpoints', 'break-3'); // will return 960px
As long as your other modules are included after this one, we can now access the breakpoint values using:
width: breakpoint('break-3');
Inside header.js
import synergy from './path/to/synergy';
export function header(els, custom) {
const defaults = {
fixed: false,
background: '#000000',
wrapper-width : '960px'
}
synergy(els, function(el, options) {
const wrapper = el.component('wrapper');
const fixed = options.fixed || el.modifier('fixed');
if (fixed) {
console.log('header is fixed');
}
if (el.modifier('noLogo')) {
console.log('header has the "noLogo" modifier');
}
wrapper.doSomething();
}, defaults, custom);
}
Call the function on the header element:
<div class="header" id="header"></div>
// Any of the following would work - continue reading to learn more
header(document.getElementByID('header'));
header(document.querySelectorAll('.header'));
header('header');
To modify the default options, pass them to the function with the new value:
header('header', {
fixed: true
});
The synergy()
function accepts 4 parameters:
els
{String|NodeList|HTMLElement} - The element(s) to call the function oncallback
{function} - The function to call on each element inels
config
{Object} - Default confiuration to use for the functioncustom
{Object} - Custom configuration to use when calling the function
synergy(els, callback, config, custom);
The els
parameter is either a single HTML Element or a NodeList. If a NodeList is passed, the callback function will iterate on each element in the NodeList.
<div class="foo" id="bar">...</div>
The below examples would all target the above HTML element:
synergy(document.getElementByID('bar'), function() {...});
synergy(document.querySelectorAll('.foo'), function() {...});
synergy('foo', function() {...});
The callback parameter is a callback function with 3 paramaters available: function(el, options, exports)
This will be a single HTML Element - if you passed a NodeList to the main function the callback will iterate through each element, accessed by this parameter.
This returns a merged object of the objects retreived by the original config
and custom
parameters.
This returns the available exports of the Synergy module (currently 'modifier' and 'component').
<div class="foo-buzz" id="bar">...</div>
const defaults = {
foo: 'fizz',
bar: 2
};
synergy('foo', function(el, options, exports) {
console.log(options.foo); // returns 'qux'
console.log(options.bar); // returns 2
console.log(el.id); // returns 'bar'
console.log(el.modifier('buzz')); // returns true
}, defaults, {foo: 'qux'});
This is a JavaScript object containing any default configuration to use for the callback, which will get merged with the custom
object.
This is a JavaScript object containing any default configuration to use for the callback, which will get merged with the config
object.
String - the modifier of interest
If this parameter is passed as a truthy value, the method will set a new modifier instead of returning the existance of one.
<div class="foo-fizz-buzz">...</div>
synergy('foo').modifier('fizz'); // returns true
synergy('foo').modifier('buzz'); // returns true
synergy('foo').modifier('qux'); // returns false
synergy('foo').modifier(); // returns ['fizz', 'buzz']
synergy('foo').modifier('baz', true); // sets new modifier of 'baz'
String - the component of interest
If this parameter is passed as a truthy value, the method will set a new component on the selected element instead of returning the existance of one.
<div id="foo">
<div class="foo_fizz" id="fooFizz"></div>
<div class="foo_buzz" id="fooBuzz">...</div>
</div>
const el_1 = document.getElementByID('foo');
const el_2 = document.getElementByID('fooFizz');
synergy(el_1).component('fizz'); // returns HTML Element
synergy(el_1).component('buzz'); // returns HTML Element
synergy(el_1).component('qux'); // returns false
synergy(el_2).component(); // returns ['fizz']
synergy(el_1).component('baz', true); // sets new component of 'baz'
All JavaScript for this example will be written in ES6 using imports and exports. The project will consit of 4 modules and 1 theme. The complete folder structure will be as follows:
|-- modules
| |-- buttons
| | |-- _buttons.scss
| |-- grid
| | |-- _grid.scss
| | |-- grid.js
| | |-- grid.json
| |-- header
| | |-- _header.scss
| | |-- header.js
| | |-- header.json
| |-- typography
| | |-- _typography.scss
|-- themes
| |-- Buzz
| | |-- buzz.scss
| | |-- buzz.js
| | |-- buzz.json
|-- app.scss
|-- app.js
The goal is to be able to configure all modules via themes/Buzz/buzz.json
. From the above structure it can be seen that not all modules have a .json
file, which is where a module's default configuration is stored. This is only required if access to the configuration is required in both the app's JavaScript and Sass realms. Otherwise, the default configuration can be contained within the module's .scss
file.
Firstly, Synergy is imported, followed by each module.
// Synergy
@import 'src/scss/synergy';
// Modules
@import 'modules/typography/typography';
@import 'modules/buttons/buttons';
@import 'modules/grid/grid';
@import 'modules/header/header';
import synergy from 'synergy';
import { grid } from './modules/grid/grid';
import { header } from './modules/header/header';
const config = {};
export { config, synergy, grid, header }
The first thing to do is import the app. Then the theme's config is imported (which, thanks to Sass Json Vars, will be accessible via the $app
variable) and each module is included to output the CSS based off the config from buzz.json
(the custom()
function seen below retreives the module's custom config from the $app
variable).
@import '../../app';
@import './buzz.json';
@include typography(custom('typography'));
@include buttons(custom('typography'));
@include grid(custom('typography'));
@include header(custom('typography'));
The theme's config is imported as well as the app.js
exports. Each module's main function is called, passing the values from buzz.json
as parameters.
import * as app from '../../app';
import config from './buzz.json';
app.grid('grid', config.grid);
app.header('header', config.header);
Each module must live under the parent app
object. This is where custom config will be passed to each module later on (when this file is imported into a Sass file, the values will be accessible from the $app
variable).
{
"app": {
"typography": {},
"buttons": {},
"grid": {},
"header": {},
}
}
Because we don't need to access these values in the JavaScript, the default configuration for this module will be stored in this file.
@mixin typography($custom: ()) {
$typography: config((
'colors':(
'primary' : blue,
'secondary' : green
),
'sizes':(
'small' : 0.8em,
'regular' : 1em,
'large' : 1.4em
)
), $custom) !global;
} // @mixin typography
@function color($color) {
@return option($typography, 'colors', $color);
}
@function size($size) {
@return option($typography, 'sizes', $size);
}
Because we don't need to access these values in the JavaScript, the default configuration for this module will be stored in this file.
@mixin buttons($custom: ()) {
$buttons: config((
'line-height' : 1.4,
'side-spacing' : 0.5em,
'background' : grey,
'color' : white,
'round-radius' : 0.4em
), $custom);
@include module('button') {
// Core Styles
//*********************************************************
display: inline-block;
line-height: this('line-height');
padding: 0 this('side-spacing');
background: this('background');
color: this('color');
// Modifiers
//*********************************************************
// Patterns
@include modifier('round') {
border-radius: this('round-radius');
}
@include modifier('block') {
display: block;
}
// Colors
@include modifier('primary') {
background: color('primary');
}
@include modifier('secondary') {
background: color('secondary');
}
// Sizes
@include modifier('small') {
font-size: size('small');
}
@include modifier('large') {
font-size: size('large');
}
// Semantic Styles
@include modifier('purchase') {
@include extend(('round', 'primary', 'large'));
}
} // module(button)
}
{
"grid": {
"name": "grid",
"breakpoints": {
"break-0" : "0px",
"break-1" : "460px",
"break-2" : "720px",
"break-3" : "940px",
"break-4" : "1200px",
"break-5" : "1400px"
}
}
}
Perhaps in a real project this file may serve more purpose, but for this example it's only use is to globally expose the breakpoint values to other modules.
@import '../../modules/grid/grid.json'; // path is relative to `themes/Buzz/`
// Default config is now accessible via the $grid variable
@mixin grid($custom: ()) {
$grid: config($grid, $custom) !global;
}
@function breakpoint($breakpoint) {
@return map-get-deep($grid, 'breakpoints', $breakpoint);
}
This now means other modules can access the theme's breakpoint values via the breakpoint()
function defined above:
@media (min-width: breakpoint('break-3')) {
...
}
And likewise for the corresponding JavaScript file, there is little more going on than merging the default config with custom values and exposng the new breakpoint values to be used by other modules.
import * as app from '../../app';
import defaults from './grid.json';
export function grid(els, custom) {
app.config.grid = Object.assign(defaults.grid, custom);
};
This now means other modules can access the theme's breakpoint values:
import * as app from '../../app';
const breakpoint_tablet = app.config.grid.breakpoints['break-3'];
Allowing you to do things like:
function breakpoint(media, value) {
return window.matchMedia(`(${media}: ${app.config.grid.breakpoints[value]})`).matches;
}
if (breakpoint('min-width', 'break-3')) {
...
}
This is where the default configuration for the header module will be stored.
Notice how we can use the earlier defined color() function from the typography module as a value
{
"header": {
"name": "header",
"background" : "color('primary')",
"top": "50px",
"disable-top": "break-5",
"dark": false,
"dark-color": "rgba(black, 0.8)",
"side": {
"enabled": false,
"width": "100%"
}
}
}
@import '../../modules/header/header.json'; // path is relative to `themes/Buzz/`
// Default config is now accessible via the $header variable
@mixin header($custom: ()) {
$header: config($header, $custom);
// Module
//*************************************************************
@include module() {
// Core Styles
//*********************************************************
background: this('background');
@media (max-width: this('disable-top')) {
margin-top: this('top');
}
// Settings
//*********************************************************
@include option('dark') {
background: this('dark-color');
}
@include option('side') {
@media (min-width: breakpoint('break-3')) {
// Core Side-Header Styles
position: fixed;
top: 0;
width: this('side', 'width');
z-index: 99;
@include value('left') {
left: 0;
}
@include value('right') {
right: 0;
}
}
}
} // module('header')
}
This example doesn't really provide any UI effects for the header, it's just to demonstrate how to set up the JS file for a module, and how to access and export the config.
import * as app from '../../app';
import defaults from './header.json';
export function header(els, custom) {
app.synergy(els, function(header, options) {
const offest = options.top
if (options.side.enabled) {
console.log('Side header is enabled');
}
if (options.dark) {
console.log('Header is dark');
}
if (app.synergy(header).modifier('dark')) {
console.log('header element has "dark" modifier');
}
}, defaults, custom);
app.config.accordions = Object.assign(
defaults.accordions, custom
);
};
With all the files setup, buzz.scss
and buzz.js
can be sent to their respective compilers/transpilers to be (pre)processed. Each module will have their default values. In order to pass custom configuration to the theme, the values are passed to buzz.json
:
{
"app": {
"typography": {
"colors": {
"primary": "purple",
"secondary": "blue"
}
},
"buttons": {
"line-height": "24px"
},
"grid": {
"breakpoints": {
"break-3": "980px",
"break-6": "1860px"
}
},
"header": {
"dark": true,
"top": "20px",
"disable-top": "break-6",
"side": {
"enabled": "left"
}
}
}
}
The values are merged recursively, meaning you only have to re-define the values you are changing.
Passing buzz.scss through our Sass compiler yields the following CSS:
.button, [class*="button-"] {
display: inline-block;
line-height: 24px;
padding: 0 0.5em;
background: grey;
color: white;
}
[class*="button-"][class*="-round"],
[class*="button-"][class*="-primary"],
[class*="button-"][class*="-purchase"] {
border-radius: 0.4em;
}
[class*="button-"][class*="-block"] {
display: block;
}
[class*="button-"][class*="-primary"],
[class*="button-"][class*="-purchase"] {
background: purple;
}
[class*="button-"][class*="-secondary"] {
background: blue;
}
[class*="button-"][class*="-small"] {
font-size: 0.8em;
}
[class*="button-"][class*="-large"],
[class*="button-"][class*="-purchase"] {
font-size: 1.4em;
}
.header, [class*="header-"] {
background: purple;
}
@media (max-width: 1860px) {
.header, [class*="header-"] {
margin-top: 0;
}
}
.header, [class*="header-"],
[class*="header-"][class*="-dark"] {
background: rgba(0, 0, 0, 0.8);
}
@media (min-width: 980px) {
.header, [class*="header-"],
[class*="header-"][class*="-side"] {
position: fixed;
top: 0;
width: 100%;
z-index: 99;
}
.header, [class*="header-"],
[class*="header-"][class*="-side"][class*="-left"] {
left: 0;
}
[class*="header-"][class*="-side"][class*="-right"] {
right: 0;
}
}
Every configurable aspect of your project can now quickly and easily be changed from just one file, whilst retaining a completely modular architecture.
- Ruby Sass 3.4+
- Nods.js 6+
- Grunt
- Babel
- Mocha
To develop Synergy for either contributing or personal purposes, follow these recommendations:
git clone https://github.com/esr360/Synergy.git
# cd Synergy/
npm install
npm install -g grunt-cli
gem install sass-json-vars
You should now be able to use Grunt to run the various development tasks:
# Lint JS & Scss files
grunt lint
# Run JS & Scss unit tests
grunt test
# Generate distribution file (Scss)
grunt concat
# Generate SassDoc and JSDoc pages
grunt docs
# Run all of the above
grunt compile
# Run `compile` and set up a watch task for JS and Scss files
grunt
Released: 11th June 2017
- Synergy JS module rewritten in ES6
- Removing Sass-JSON as dependency, replacing with Sass-JSON-Vars
- Updating Sass-Boost dependency
- Dependences now node modules instead of git submodules
- Improving options mixin
- Adding value-enabled() utility function
- Adding enabled() utility function
- Allow modules to have default modifiers
- Allow extending of modifiers when including module
- Allow combining of modifiers when including module
- Allow module output to be disabled
- Removing
overwrite()
mixin - Removing
overwrite-component()
mixin module()
mixin is now nestablecomponent()
mixin is now nestable- Adding JavaScript unit tests