Getting

Sassy

with

NODE INTERACTIVE 2015

DAVID KHOURSHID, COUNSYL

Who am I?

What can Sass do?

TL;DR: Not a whole lot.

What can Sass do?

TL;DR: Not a whole lot.

  • $variables
    • $list: (1, 2, 3, 4)
    • $map: (a: 1, b: 2, c: 3)
  • @functions
    • many built-in functions
  • @mixins
  • @each
  • @for
  • @while
  • @if ... @else
  • @debug, @error
function double(val) {
  return val * 2;
}

_.map([1, 2, 3], double);

// => [2, 4, 6]
@function double($val) {
  @return $val * 2;
}

$foo: _map((1, 2, 3), double);

// => (2, 4, 6)

What can't Sass do?

  • First-class functions
  • Prototypes/classes
  • Continue in loops
  • Lambdas/anonymous functions
  • Closures
  • Mutable values (GOOD!)
  • Modules
  • Function/variable hoisting

What is Sass great at?

  • Values with units (smart!)
    • 1s + 10ms = 1010ms
    • 10deg + 10rad = 582.9578deg
  • Color operations
    • #777 + #888 = white
      
  • Selector operations
    • & - parent selector
    • @extend and selector unification
    • nesting

Anything CSS.

Powerful languages inhibit information reuse.

Use the least powerful language suitable for expressing information, constraints or programs on the World Wide Web.

PRINCIPLE

GOOD PRACTICE

Node-Sass

var sass = require('node-sass');

sass.render({
  file: scss_filename,

  // options...

}, function(err, result) {

  // render CSS

});

Node-Sass importer

  • Experimental feature
  • url (string) - the path to be imported
  • prev (string) - the previously resolved path
  • done (function) - callback for async completion
  • Can return:
    • { file: ... }
    • { contents: ... }
  • Can skip with sass.NULL
sass.render({
  file: 'path/to/stylesheet.scss',
  importer: (url, prev, done) => {
    if (url == 'goat-styles') {
      return {
        file: 'path/to/secret/goat.css'
      };
    }

    return sass.NULL;
  }
}, (err, res) => { ... });
// path/to/stylesheet.scss
@import 'goat-styles';

// ... will import path/to/secret/goat.css


@import 'path/to/partial';

// ... will do default import behavior

Node-Sass functions

  • Experimental feature
  • key: Sass function signature
    • 'pow($val, $exp)'
  • value: actual function
  • Going async? use done
    • Last argument
  • Uses node-sass types
    • Input: node-sass types
    • Output: node-sass types
sass.render({
  file: 'path/to/stylesheet.scss',
  functions: {
    'pow($val, $exp: 1)': (val, exp) => {
      let jsVal = val.getValue();
      let jsExp = val.getValue();

      let result = Math.pow(jsVal, jsExp);
      
      return sass.types.Number(result);
    }
  }
}, (err, res) => { ... });
// path/to/stylesheet.scss

$foo: pow(2, 3);

test {
  font-size: $foo * 1px;
}

// CSS
test {
  font-size: 8px;
}

Yes, this is very verbose.

Let's have some fun with this.

Sass Eyeglass

"Getting some NPM in your Sass"

Sass Eyeglass

  • Node-Sass extension manager
  • Built on top of NPM
    • ​Import Sass from Eyeglass modules
  • The successor to Sass Compass
  • Asset management
  • Asset URL resolution
  • Import from index.scss
  • File system API
  • http://www.eyeglass.rocks

Installing Sass Eyeglass

var path = require("path");
var sass = require("node-sass");
var eyeglass = require("eyeglass");
var rootDir = __dirname;
var assetsDir = path.join(rootDir, "assets");

var options = { ... node-sass options ... };


options.eyeglass = {
  root: rootDir,

  buildDir: path.join(rootDir, "dist"),

  assets: {
    httpPrefix: "assets",

    sources: [
      {
        directory: assetsDir,
        globOpts: {
          ignore: ["**/*.js", "**/*.scss"]
        }
      }
    ]
  }
}

// Standard node-sass rendering of a single file.
sass.render(eyeglass(options, sass),
  function(err, result) {
    // handle results
  });
  • NPM install node-sass and eyeglass
  • Specify Sass options
  • Specify Eyeglass options inside Sass options
    • Add proper root and build directories
    • Add asset configuration
  • Pass options inside of Eyeglass
  • Pass Sass inside of Eyeglass (for good measure)
  • Render using Node-Sass

In eight "easy" steps!

Creating an Eyeglass Module

{
  ...
  "keywords": ["eyeglass-module", "sass", ...],
  "eyeglass": {
    "sassDir": "sass",
    "exports": "eyeglass-exports.js",
    "name": "greetings",
    "needs": "^0.6.0"
  },
  ...
}
  • NPM init to create a new NPM module
  • Add "eyeglass-module" keyword to package.json
  • Add Eyeglass config to package.json
    • sassDir
    • exports (defaults to main)
    • name (required)
    • needs (yes, required)
  • Create eyeglass-exports.js
  • Module export = function
    • Takes 2 args: eyeglass, sass
    • Returns config object

In just five or so steps!

// eyeglass-exports.js

var path = require("path");

module.exports = function(eyeglass, sass) {
  return {
    functions: {
      "greetings-hello($name: 'World')": (name, done) => {
        done(sass.types.String("Hello, " + name.getValue()));
      }
    }
  }
};

Thankfully, there's a helper library.

Using 3rd-Party Eyeglass Modules

Okay, this is actually really easy.

  1. npm install eyeglass-math
  2. @import 'math';
  3. Use it!
// Import the Eyeglass module name
@import 'math';

test {
  font-size: pow(2, 3) * 1px;
  pi: $PI;
}


// Result
test {
  font-size: 8px;
  pi: 3.1415;
}

Sassport

Sass with JavaScript Superpowers.

Sassport

  • Node-Sass extension manager
  • NPM Any JavaScript modules
    • ​Even modules not made for Sassport!
  • ​Extensible loaders
  • Extremely JS-friendly
  • ​Asset management and URL resolution capable
    • ​Not its main objective
  • github.com/davidkpiano/sassport

Installing Sassport

  1. npm install sassport
  2. Add your modules
    1. Just like PostCSS
  3. Add minimal asset config
    (if you want to)
    1. localPath
    2. remotePath
  4. Use Sassport just like you would use Node-Sass
import sassport from 'sassport';
import sassportMath from 'sassport-math';

sassport([ sassportMath ])
  .render({
    file: 'main.scss',
    // other Sass options
  }, (err, result) => {
    // output the CSS
  });




// If you want asset management...

sassport([ ... ])
  .assets(__dirname + '/assets', 'public/assets')
  .render( ... );

Creating a Sassport module

  1. npm install sassport
  2. Define your module:
    sassport.module(name)
  3. Add (optional):
    1. .functions
    2. .variables
    3. .exports
    4. .loaders
  4. Export your module.
    No need for NPM.
import sassport from 'sassport';

const wrap = sassport.wrap;

const myModule = sassport.module('greetings')
  .functions({
    'greetings-hello($name: "World")': wrap(
       (name) => `Hello, ${name}!`
    )
  });

export default myModule;
      

What's sassport.wrap()? Looks magical.

sassport.wrap(fn[, opts])

For when you want plain values.

Sass value -> plain JS value -> Sass value

// Without sassport.wrap

import sass from 'node-sass';

sassport.module('greeting')
  .functions({
    'greet($val)': (val) => {
      let jsVal = val.getValue();
      let jsResult = 'Hello, ' + jsVal;

      return sass.types.String(jsResult);
    }
  })
// With sassport.wrap

const wrap = sassport.wrap;

sassport.module('greeting')
  .functions({
    'greet($val)': wrap((val) => {
      return "Hello, " + val;
    })
  })
sassport.wrapAll(obj[, opts])

For wrapping entire objects/libraries.

MyLib($method, $args...)

method: call with $args

property: no $args

const wrapAll = sassport.wrapAll;

const mathModule = sassport.module('math')
  .functions({
    'Math($method, $args...)': wrapAll(Math)
  });

export default mathModule;
test {
  font-size: Math(pow, 2, 3) * 1px;
  pi: Math(PI);
}


// Result CSS:
test {
  font-size: 8px;
  pi: 3.1415;
}

require() in Sass

Yes, really.

// path/to/my-colors.js

const colors = {
  primary: '#C0FF33',
  secondary: '#B4D455'
};

export default colors;
// path/to/stylesheet.scss

// Just like Node require()!
$colors: require('path/to/my-colors');
$primary-color: map-get($colors, primary);

.foo {
  color: $primary-color;

  &:hover {
    color: lighten($primary-color, 10%);
  }
}


// Result CSS:
.foo {
  color: #c0ff33;
}

.foo:hover {
  color: #d0ff66;
}

Sassport uses
inferred values.

Next release!

  • @import by reference
  • @import once
  • @import index by default
    • index.scss
    • index.sass
    • _index.scss
    • _index.sass
  • Improved integration with Gulp, Grunt, Webpack, etc.

Eyeglass

Sassport

Pain in the Assets

import eyeglass from 'eyeglass';

module.exports = function(eyeglass, sass) {
  return {
    sassDir: path.join(
      __dirname, "stylesheets"),
    assets: eyeglass.assets.export(
      path.join(__dirname, "images"))
  };
};
import sassport from 'sassport';

module.exports = sassport.module('test')
  .exports({
    'default': path.join(
      __dirname, 'stylesheets/main.scss'),
    'images': path.join(
      __dirname, 'images')
  });
@import 'test/images';

// other SCSS code here...

with Eyeglass

with Sassport

Pain in the Assets

@import "test/images";

.test {
  background: asset-url("test/images/foo.jpg");
}


// Can list all assets
.all-assets {
  app-assets: asset-list();
  test-assets: asset-list("test");
}
@import "test/images";

.test {
  background: resolve-url("test/images/foo.jpg");
}


// Can also get local path
$image-path: resolve-path("images/bar.jpg", "test");

Playing with Assets

// index.js
import sassport from 'sassport';
import imageSize from 'image-size';

const wrap = sassport.wrap;

sassport()
  .functions({
    'size-of($path)': wrap((path) => {
      return sizeOf(path);
    })
  })
  .assets('./assets', 'public/assets')
  .render({
    file: 'stylesheet.scss'
  }, function(err, res) {
    console.log(res.css.toString());
  });
// stylesheet.scss
$image-path: 'sassport-sm.png';
$image-size: size-of(resolve-path($image-path));

.my-image {
  background-image: resolve-url($image-path);
  width: map-get($image-size, 'width') * 1px;
  height: map-get($image-size, 'height') * 1px;
}
// Result CSS
.my-image {
  background-image:
    url(public/assets/sassport-sm.png);
  width: 145px;
  height: 175px;
}

in Sassport

Thanks, everyone!

NODE INTERACTIVE 2015

DAVID KHOURSHID, COUNSYL

GOAT ANY QUESTIONS?