Skip to content

Instantly share code, notes, and snippets.

@JulienCabanes
Created January 26, 2014 13:41
Show Gist options
  • Save JulienCabanes/8632893 to your computer and use it in GitHub Desktop.
Save JulienCabanes/8632893 to your computer and use it in GitHub Desktop.
Generated by SassMeister.com.
<h1>Responsive logic with <a href="https://github.com/HugoGiraudel/SassyJSON">SassyJSON</a></h1>
<p><a href="#demo">This is a demonstration</a> of how SassyJSON can be useful for Responsive Design.</p>
<h2>The <em>real world</em> problem</h2>
<p>Sometimes you need to trigger some JS actions, like initializing a lib, changing some plugin options, but only when the layout is in a specific configuration. For instance, changing the zoom of a map depending on how many columns it fills, or switching from a simple lightweight list of images to a full featured gallery or just <a href="http://24ways.org/2011/conditional-loading-for-responsive-designs/">loading content</a>...</p>
<p>There is some micro-libraries for this kind of thing, or you can do it yourself. One solution is to hard-code in your JS the same media-queries you used in your CSS. Another solution is to check if an element is visible or not, and bind this visibility with the same media-queries that drives the layout. The same hack can be implemented in many ways, with the <code>content</code> property for instance but none of those is clean :</p>
<ul>
<li>Media-queries in JS &amp; CSS is not DRY, you could forget one of them</li>
<li>Adding visibility rules is a hack, checking this in JS is not good for perf</li>
</ul>
<h2>The solution</h2>
<p>That being said, this demo is neither efficient nor clean. It is just a proof-of-concept about how we can solve this kind of problematics with SassyJSON.</p>
<p>The idea here is to use Sass maps (the variable, not the geographical) for defining grids.</p>
<pre><code>@include grid-system((
prefix: '',
breakpoints: 0 40em 58em,
cells: (
'.foo': 12 6 4,
'.bar': 6 6 4,
'.baz': 6 12 4
)
));</code></pre>
<p></p>Those maps can then be used by Javascript through SassyJSON. In this demo, the foo cell's content is changing when it enters or leaves the 6 columns configuration (resize your window).</p>
<h2 id="demo">The demo</h2>
<div class="semantic-grid">
<div class="foo">foo</div>
<div class="bar">bar</div>
<div class="baz">baz</div>
</div>
<h2>Enhancements</h2>
<p>The JS code is still relying on the size, but not the width in pixels. Even if columns is a better parameter than width, it still comes from the CSS. I think we should not choose between having the directives parameters on one side or another, whether it's CSS or JS. Another way could be defining the "responsive logic" in a dedicated JSON file, both Sass &amp; JS <sreong>could</strong> then read it, store it and use it in <code>@media</code> and <code>matchMedia()</code>. This kind of separation of concerns could have been done before through dirty hacks (like parsing Sass variables file with JS), it's much cleaner and learnable to use JSON for this.</p>
<p>Unfortunately, and as far as I know, there's no way right now for SassyJSON to use an external JSON file, only decoding from a string is possible. A workaround could be to use <em>grunt-contrib-concat</em> to generate a Sass file from the JSON, and watch it...</p>
<script>
(function() {
// Read the JSON (better way ?);
var head = document.getElementsByTagName('head')[0];
var exports = window.getComputedStyle(head)["font-family"];
var cssExports = JSON.parse(exports.slice(1, -1));
// Extract useful datas
var responsiveGrid = cssExports.grids[0];
var breakpoints = responsiveGrid.breakpoints;
var cells = responsiveGrid.cells;
// Target only one element
var foo = document.querySelector('.foo');
var previousSize = false;
// Demo only on C cell
var onresize = window.onresize = function() {
var bp = false;
var size = false;
// find the current layout
for(var i in cells['.foo']) {
if(i >= breakpoints.length) {
bp = breakpoints[breakpoints.length - 1]
} else {
bp = breakpoints[i];
}
if(matchMedia('only screen and (min-width: ' + bp + ')').matches) {
size = cells['.foo'][i];
}
}
// Logic goes here
// We can trigger JS actions only when the layout changes
if(size !== previousSize) {
// Entering the layout target
if(size == 6) {
foo.innerHTML = 'foo >>> 6 columns <<<';
// Leaving the layout target
} else if(previousSize == 6) {
foo.innerHTML = 'foo';
}
previousSize = size;
}
console.log('size', size);
};
onresize();
})();
</script>
// ----
// Sass (v3.3.0.rc.2)
// Compass (v1.0.0.alpha.17)
// Sassy Maps (v0.3.1)
// SassyJSON (v1.0.9)
// ----
// SassyJSON can be useful for sharing layout logic with JS !
@import "SassyJSON";
@import "sassy-maps";
// This looks like module.exports :)
$exports: () !default;
// NB : This map-based grid implementation is WIP and not the point of the demo
$default-grid: (
columns: 12,
width: 100%,
gutter: 2%,
breakpoints: 0,
cells: false,
outside: false,
method: "compat",
prefix: ".grid-cell-"
) !default;
// homemade media-query mixin : simplified version for the demo
@mixin for-min-width($width) {
// Use to check for MQ support (ala TeamSass/Breakpoint)
@if $width > 0 {
@media only screen and (min-width: #{$width}) {
@content;
}
} @else {
@content;
}
}
// Obvious
@mixin clearfix {
*zoom: 1;
&:before, &:after { display: table; content: " "; line-height: 0; }
&:after { clear: both; }
}
// Create a grid map by extending the default and adding a unique id
@function create-grid($grid) {
$grid: map-merge($default-grid, $grid);
$grid: map-set($grid, "uid", "%" + unique-id());
// Export every grids created
@if map-get($exports, 'grids') == null {
$exports: map-merge($exports, (grids: ())) !global;
}
$exports: map-merge($exports, (grids: append(map-get($exports, 'grids'), $grid))) !global;
@return $grid;
}
// Force list type (better way ?)
@function grid-force-list($value) {
@if type-of($value) != "list" {
$value: ($value, );
}
@return $value;
}
// Always returns a *list* of breakpoints
@function grid-get-breakpoints($grid) {
@return grid-force-list(map-get($grid, "breakpoints"));
}
// Same as nth() but works on *non-list* vars and off limit index
@function grid-nth($list, $index: 0) {
// Force list
$list: grid-force-list($list);
// Check the index
@if $index <= length($list) {
@return nth($list, $index);
// Otherwise, take the last item in the list as a fallback
} @else {
@return nth($list, length($list));
}
}
@function grid-columns-to-cells-map($length) {
$map: ();
@for $i from 1 through $length {
$map: map-merge($map, ($i: $i));
}
@return $map;
}
// Creates a grid row style for each breakpoints
@mixin grid-row($grid: $default-grid) {
@include clearfix;
// Cache previous values
$gutter: false;
$outside: false;
// Sass scope make this not reusable
$breakpoints: grid-get-breakpoints($grid);
@for $i from 1 through length($breakpoints) {
$grid-gutter: grid-nth(map-get($grid, "gutter"), $i);
$grid-outside: grid-nth(map-get($grid, "outside"), $i);
// Check if style changed
@if $grid-gutter != $gutter or $grid-outside != $outside {
// Update the cache
$gutter: $grid-gutter;
$outside: $grid-outside;
// Apply the current breakpoint
@include for-min-width(nth($breakpoints, $i)) {
// Apply the style
@include grid-row-style($gutter, $outside);
}
}
}
}
// Row style : old school method
@mixin grid-row-style($gutter, $outside) {
@if $outside == false {
$gutter: -$gutter;
}
margin-left: $gutter / 2;
margin-right: $gutter / 2;
}
// Cell base style : old school method
@mixin grid-base-cell($gutter) {
float: left;
margin-left: $gutter / 2;
margin-right: $gutter / 2;
}
// Creates cell style from grid properties and size
@mixin grid-cell($size: 1, $grid: $default-grid, $index: 1) {
@include grid-cell-style(
$width: grid-nth(map-get($grid, "width"), $index),
$columns: grid-nth(map-get($grid, "columns"), $index),
$gutter: grid-nth(map-get($grid, "gutter"), $index),
$size: $size
);
}
// Simple math
@mixin grid-cell-style($width, $columns, $gutter, $size) {
width: ($width / $columns * $size) - $gutter;
}
//
@mixin grid-cells($grid: $default-grid) {
$uid: map-get($grid, "uid");
$prefix: map-get($grid, "prefix");
$cells: map-get($grid, "cells");
// Cells can be named
@if $cells == false {
$cells: grid-columns-to-cells-map(map-get($grid, "columns"));
}
// Sass scope make it not reusable
$breakpoints: grid-get-breakpoints($grid);
@for $i from 1 through length($breakpoints) {
$bp: nth($breakpoints, $i);
@include for-min-width($bp) {
@if $uid {
#{$uid + "-" + $bp} {
// Base style avoids bloat
@include grid-base-cell(grid-nth(map-get($grid, "gutter"), $i));
}
}
@each $cell-name, $cell-sizes in $cells {
$size: grid-nth($cell-sizes, $i);
@if $size > 0 {
> #{$prefix + $cell-name} {
@if $uid {
@extend #{$uid + "-" + $bp} !optional;
}
@include grid-cell($size: $size, $grid: $grid, $index: $i);
}
}
}
}
}
}
// This is the main API
@mixin grid-system($grid: $default-grid) {
@if map-get($grid, "uid") == null {
$grid: create-grid($grid);
}
@include grid-row($grid);
@include grid-cells($grid);
}
body {
margin: 0 auto;
padding: 10px;
max-width: 980px;
color: #444;
font-family: "Times new roman", Times, serif;
font-size: 20px;
line-height: 1.4;
}
.semantic-grid {
@include grid-system((
prefix: '',
breakpoints: 0 40em 58em,
cells: (
'.foo': 12 6 4,
'.bar': 6 6 4,
'.baz': 6 12 4
)
));
> * {
margin-top: 1em;
padding: 1em 0;
background: gray;
color: white;
text-align: center;
}
}
.foo {
background: green;
}
@include json-encode($exports);
body {
margin: 0 auto;
padding: 10px;
max-width: 980px;
color: #444;
font-family: "Times new roman", Times, serif;
font-size: 20px;
line-height: 1.4;
}
.semantic-grid {
*zoom: 1;
margin-left: -1%;
margin-right: -1%;
}
.semantic-grid:before, .semantic-grid:after {
display: table;
content: " ";
line-height: 0;
}
.semantic-grid:after {
clear: both;
}
.semantic-grid > .foo, .semantic-grid > .bar, .semantic-grid > .baz {
float: left;
margin-left: 1%;
margin-right: 1%;
}
.semantic-grid > .foo {
width: 98%;
}
.semantic-grid > .bar {
width: 48%;
}
.semantic-grid > .baz {
width: 48%;
}
@media only screen and (min-width: 40em) {
.semantic-grid > .foo, .semantic-grid > .bar, .semantic-grid > .baz {
float: left;
margin-left: 1%;
margin-right: 1%;
}
.semantic-grid > .foo {
width: 48%;
}
.semantic-grid > .bar {
width: 48%;
}
.semantic-grid > .baz {
width: 98%;
}
}
@media only screen and (min-width: 58em) {
.semantic-grid > .foo, .semantic-grid > .bar, .semantic-grid > .baz {
float: left;
margin-left: 1%;
margin-right: 1%;
}
.semantic-grid > .foo {
width: 31.33333%;
}
.semantic-grid > .bar {
width: 31.33333%;
}
.semantic-grid > .baz {
width: 31.33333%;
}
}
.semantic-grid > * {
margin-top: 1em;
padding: 1em 0;
background: gray;
color: white;
text-align: center;
}
.foo {
background: green;
}
/*! json-encode: {"grids": [{"columns": 12, "width": "100%", "gutter": "2%", "breakpoints": [0, "40em", "58em"], "cells": {".foo": [12, 6, 4], ".bar": [6, 6, 4], ".baz": [6, 12, 4]}, "outside": false, "method": "compat", "prefix": "", "uid": "%u63vxoezo"}]} */
body::before {
display: none !important;
content: '{"grids": [{"columns": 12, "width": "100%", "gutter": "2%", "breakpoints": [0, "40em", "58em"], "cells": {".foo": [12, 6, 4], ".bar": [6, 6, 4], ".baz": [6, 12, 4]}, "outside": false, "method": "compat", "prefix": "", "uid": "%u63vxoezo"}]}';
}
head {
font-family: '{"grids": [{"columns": 12, "width": "100%", "gutter": "2%", "breakpoints": [0, "40em", "58em"], "cells": {".foo": [12, 6, 4], ".bar": [6, 6, 4], ".baz": [6, 12, 4]}, "outside": false, "method": "compat", "prefix": "", "uid": "%u63vxoezo"}]}';
}
@media -json-encode {
json {
json: '{"grids": [{"columns": 12, "width": "100%", "gutter": "2%", "breakpoints": [0, "40em", "58em"], "cells": {".foo": [12, 6, 4], ".bar": [6, 6, 4], ".baz": [6, 12, 4]}, "outside": false, "method": "compat", "prefix": "", "uid": "%u63vxoezo"}]}';
}
}
<h1>Responsive logic with <a href="https://github.com/HugoGiraudel/SassyJSON">SassyJSON</a></h1>
<p><a href="#demo">This is a demonstration</a> of how SassyJSON can be useful for Responsive Design.</p>
<h2>The <em>real world</em> problem</h2>
<p>Sometimes you need to trigger some JS actions, like initializing a lib, changing some plugin options, but only when the layout is in a specific configuration. For instance, changing the zoom of a map depending on how many columns it fills, or switching from a simple lightweight list of images to a full featured gallery or just <a href="http://24ways.org/2011/conditional-loading-for-responsive-designs/">loading content</a>...</p>
<p>There is some micro-libraries for this kind of thing, or you can do it yourself. One solution is to hard-code in your JS the same media-queries you used in your CSS. Another solution is to check if an element is visible or not, and bind this visibility with the same media-queries that drives the layout. The same hack can be implemented in many ways, with the <code>content</code> property for instance but none of those is clean :</p>
<ul>
<li>Media-queries in JS &amp; CSS is not DRY, you could forget one of them</li>
<li>Adding visibility rules is a hack, checking this in JS is not good for perf</li>
</ul>
<h2>The solution</h2>
<p>That being said, this demo is neither efficient nor clean. It is just a proof-of-concept about how we can solve this kind of problematics with SassyJSON.</p>
<p>The idea here is to use Sass maps (the variable, not the geographical) for defining grids.</p>
<pre><code>@include grid-system((
prefix: '',
breakpoints: 0 40em 58em,
cells: (
'.foo': 12 6 4,
'.bar': 6 6 4,
'.baz': 6 12 4
)
));</code></pre>
<p></p>Those maps can then be used by Javascript through SassyJSON. In this demo, the foo cell's content is changing when it enters or leaves the 6 columns configuration (resize your window).</p>
<h2 id="demo">The demo</h2>
<div class="semantic-grid">
<div class="foo">foo</div>
<div class="bar">bar</div>
<div class="baz">baz</div>
</div>
<h2>Enhancements</h2>
<p>The JS code is still relying on the size, but not the width in pixels. Even if columns is a better parameter than width, it still comes from the CSS. I think we should not choose between having the directives parameters on one side or another, whether it's CSS or JS. Another way could be defining the "responsive logic" in a dedicated JSON file, both Sass &amp; JS <sreong>could</strong> then read it, store it and use it in <code>@media</code> and <code>matchMedia()</code>. This kind of separation of concerns could have been done before through dirty hacks (like parsing Sass variables file with JS), it's much cleaner and learnable to use JSON for this.</p>
<p>Unfortunately, and as far as I know, there's no way right now for SassyJSON to use an external JSON file, only decoding from a string is possible. A workaround could be to use <em>grunt-contrib-concat</em> to generate a Sass file from the JSON, and watch it...</p>
<script>
(function() {
// Read the JSON (better way ?);
var head = document.getElementsByTagName('head')[0];
var exports = window.getComputedStyle(head)["font-family"];
var cssExports = JSON.parse(exports.slice(1, -1));
// Extract useful datas
var responsiveGrid = cssExports.grids[0];
var breakpoints = responsiveGrid.breakpoints;
var cells = responsiveGrid.cells;
// Target only one element
var foo = document.querySelector('.foo');
var previousSize = false;
// Demo only on C cell
var onresize = window.onresize = function() {
var bp = false;
var size = false;
// find the current layout
for(var i in cells['.foo']) {
if(i >= breakpoints.length) {
bp = breakpoints[breakpoints.length - 1]
} else {
bp = breakpoints[i];
}
if(matchMedia('only screen and (min-width: ' + bp + ')').matches) {
size = cells['.foo'][i];
}
}
// Logic goes here
// We can trigger JS actions only when the layout changes
if(size !== previousSize) {
// Entering the layout target
if(size == 6) {
foo.innerHTML = 'foo >>> 6 columns <<<';
// Leaving the layout target
} else if(previousSize == 6) {
foo.innerHTML = 'foo';
}
previousSize = size;
}
console.log('size', size);
};
onresize();
})();
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment