The Yeoman generator generator-gulp-angular is a great tool for building AngularJS and in my opinion is much better than the official Yeoman generator for AngularJS (generator-angular), because it uses Gulp and not Grunt as the task runner, it has a component-like folder structure for the code instead of the “drawer” style, it uses libsass instead of compass (ruby), it supports Typescript and has a lot of options for configuration.
But there is a thing that I don’t like: how it handles bootstrap-sass.
The problem is that when dealing with styles, the generator creates two separate files, one with all the styles from the libraries installed using bower and one with all the styles created by us. This means that our sass files can’t use Bootstrap variables and mixins and that we can’t override Bootstrap internals to have complete control over the framework.
The process of the sass files are done in the file “gulp/styles.js”.
gulp.task('styles', function () {
var sassOptions = {
style: 'expanded'
};
var injectFiles = gulp.src([
options.src + '/app/**/*.scss',
'!' + options.src + '/app/index.scss',
'!' + options.src + '/app/vendor.scss'
], { read: false });
var injectOptions = {
transform: function(filePath) {
filePath = filePath.replace(options.src + '/app/', '');
return '@import \'' + filePath + '\';';
},
starttag: '// injector',
endtag: '// endinjector',
addRootSlash: false
};
var indexFilter = $.filter('index.scss');
var vendorFilter = $.filter('vendor.scss');
return gulp.src([
options.src + '/app/index.scss',
options.src + '/app/vendor.scss'
])
.pipe(indexFilter)
.pipe($.inject(injectFiles, injectOptions))
.pipe(indexFilter.restore())
.pipe(vendorFilter)
.pipe(wiredep(options.wiredep))
.pipe(vendorFilter.restore())
.pipe($.sourcemaps.init())
.pipe($.sass(sassOptions)).on('error', options.errorHandler('Sass'))
.pipe($.autoprefixer()).on('error', options.errorHandler('Autoprefixer'))
.pipe($.sourcemaps.write())
.pipe(gulp.dest(options.tmp + '/serve/app/'))
.pipe(browserSync.reload({ stream: true}));
});
How the “styles” task works
To understand how this generator process the sass files, we are going to analyze step by step the above script.
Step 1: Getting the files
Let’s inspect how the generator process our app styles inside the file “gulp/styles.js“. It starts by selecting the files to process: “index.scss” and “vendor.scss”.
return gulp.src([
options.src + '/app/index.scss',
options.src + '/app/vendor.scss'
])
The variable options.src is defined in the file “gulpfile.js” and refers to the string “src”, so the above snippet is equivalent to:
return gulp.src([
'src/app/index.scss',
'src/app/vendor.scss'
])
Step 2: Inject app sass files
The next step is to inject all of our app sass styles into “index.scss”. By injecting I mean putting something like @import “my-component/my-component.style.scss”; inside “index.scss”. To do that, we need to filter first the target file (“index.scss”):
.pipe(indexFilter)
The variable indexFilter is defined as $.filter(‘index.scss’) so we are essentially telling gulp to leave only the file “index.scss” in the stream and temporary ditch “vendor.scss” which was initially in the stream also.
Now we can tell the injector to do his job and inject our files:
.pipe($.inject(injectFiles, injectOptions))
The variable injectFiles is selecting all the sass files inside the “src/app” folder, excluding the special files “index.scss” and “vendor.scss” which we are using as targets.
var injectFiles = gulp.src([
options.src + '/app/**/*.scss',
'!' + options.src + '/app/index.scss',
'!' + options.src + '/app/vendor.scss'
], { read: false });
Because the injector is not very smart, we need to tell him where to inject the files and how to inject those.
var injectOptions = {
transform: function(filePath) {
filePath = filePath.replace(options.src + '/app/', '');
return '@import \'' + filePath + '\';';
},
starttag: '// injector',
endtag: '// endinjector',
addRootSlash: false
};
The above code is telling gulp to look for a block code defined by the tags // injector and // endinjector where all of our sass code is going to be injected. To inject the files, we are defining the method transform that is telling gulp how to write the import statement to inject every file.
Looking at the file “index.scss” we can see that these tags or placeholders are defined at the bottom of the file.
/* Do not remove this comments bellow. It's the markers used by gulp-inject to inject
all your sass files automatically */
// injector
// endinjector
For example, if we have the following sass files in our app:
src
├── app
│ ├── components
│ │ └── navbar
│ │ │ └── navbar.style.scss
│ │ └── panel
│ │ │ └── panel.style.scss
│ ├── index.scss
After running the “styles” task, we are going to end up with something like this:
/* Do not remove this comments bellow. It's the markers used by gulp-inject to inject
all your sass files automatically */
// injector
@import 'components/navbar/navbar.style.scss';
@import 'components/panel/panel.style.scss';
// endinjector
Step 3: Inject vendors sass files
The task will then do a similar process to handle sass files found in our libraries (bower_components), it’s going to lookup all the libraries defined in bower.json and inject every main sass file inside “vendors.scss”. Because in our stream we only have “index.scss” (we filtered before), we are going to reverse the filter and apply a new one to get only “vendors.scss”.
.pipe(indexFilter.restore())
.pipe(vendorFilter)
.pipe(wiredep(options.wiredep))
Instead of using a custom injector like before, we are using a library called “wiredep” that expects some special placeholders to know where to inject the files and that knows how to inject them. Inside “vendors.scss” we can see the placeholder for wiredep:
/* Do not remove this comments bellow. It's the markers used by wiredep to inject
sass dependencies when defined in the bower.json of your dependencies */
// bower:scss
// endbower
This is telling wiredep to only inject sass files from our libraries, for example, bootstrap-sass.
Step 4: Process sass
Now that we have all the sass files injected (our styles and our libraries’ styles), the task is going to process both files, “index.scss” and “vendor.scss” to generate the analogous “index.css” and “vendor.css”, but first, we need to reverse the filter applied before so the task can work on both files, no only on “vendor.scss”.
.pipe(vendorFilter.restore())
.pipe($.sourcemaps.init())
.pipe($.sass(sassOptions)).on('error', options.errorHandler('Sass'))
.pipe($.autoprefixer()).on('error', options.errorHandler('Autoprefixer'))
.pipe($.sourcemaps.write())
Additional to the css files, the task is creating a source map files that help debugging the resulting styles in the browser.
Step 5: Save files and reload browser
The last step of the task is to save both resulting files “index.css” and “vendor.css” in the folder “serve/app” where the development webserver is going to serve the files. When the files are saved in their final destination, the task will tell the browser to reload using the new files.
.pipe(gulp.dest(options.tmp + '/serve/app/'))
.pipe(browserSync.reload({ stream: true }));
Modifying the “styles” task and related files
In order to have access to bootstrap sass variables and mixins in our own code, we need to merge both files “index.scss” and “vendor.scss” in just one file, in this case, in “index.scss”. To do that, we have to modify the task as follow:
gulp.task('styles', function () {
var sassOptions = {
style: 'expanded'
};
var injectFiles = gulp.src([
options.src + '/app/**/*.scss',
'!' + options.src + '/app/index.scss',
// '!' + options.src + '/app/vendor.scss'
], { read: false });
var injectOptions = {
transform: function(filePath) {
filePath = filePath.replace(options.src + '/app/', '');
return '@import \'' + filePath + '\';';
},
starttag: '// injector',
endtag: '// endinjector',
addRootSlash: false
};
// var indexFilter = $.filter('index.scss');
// var vendorFilter = $.filter('vendor.scss');
return gulp.src([
options.src + '/app/index.scss',
// options.src + '/app/vendor.scss'
])
// .pipe(indexFilter)
.pipe($.inject(injectFiles, injectOptions))
// .pipe(indexFilter.restore())
// .pipe(vendorFilter)
.pipe(wiredep(options.wiredep))
// .pipe(vendorFilter.restore())
.pipe($.sourcemaps.init())
.pipe($.sass(sassOptions)).on('error', options.errorHandler('Sass'))
.pipe($.autoprefixer()).on('error', options.errorHandler('Autoprefixer'))
.pipe($.sourcemaps.write())
.pipe(gulp.dest(options.tmp + '/serve/app/'))
.pipe(browserSync.reload({ stream: true }));
});
Notice that we erased every reference to the file “vendor.scss” and, because we end up with just one file to process (index.scss) we also delete the filtering process of the task. To make it work, we need to put the content of “vendor.scss” inside “index.scss”.
$icon-font-path: "../../bower_components/bootstrap-sass-official/assets/fonts/bootstrap/";
/* Do not remove this comments bellow. It's the markers used by wiredep to inject
sass dependencies when defined in the bower.json of your dependencies */
// bower:scss
// endbower
.browsehappy {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}
.thumbnail {
height: 200px;
img.pull-right {
width: 50px;
}
}
/* Do not remove this comments bellow. It's the markers used by gulp-inject to inject
all your sass files automatically */
// injector
// endinjector
We can now safely delete the file “vendor.scss” because it’s no longer needed. Now that “vendor.scss” is gone, gulp is never going to create the file “vendor.css” so we need to delete the reference for that file inside “index.html” (line 11).
<!-- build:css({.tmp/serve,src}) styles/vendor.css -->
<link rel="stylesheet" href="app/vendor.css"> <!-- delete this line -->
<!-- bower:css -->
<!-- run `gulp inject` to automatically populate bower styles dependencies -->
<!-- endbower -->
<!-- endbuild -->
Overwriting bootstrap variables
To take complete control over bootstrap, we need to have the ability to define (overwrite) bootstrap internal variables. For that, we are going to define a special folder called “global” inside “src/app” where we are going to put the sass files that are going to overwrite bootstrap. Then we have to modify the “styles” task like the following:
gulp.task('styles', function () {
var sassOptions = {
style: 'expanded'
};
function transformFn(filePath) {
filePath = filePath.replace(options.src + '/app/', '');
return '@import \'' + filePath + '\';';
}
var injectFiles = gulp.src([
options.src + '/app/**/*.scss',
'!' + options.src + '/app/global/**/*.scss',
'!' + options.src + '/app/index.scss',
], { read: false });
var injectOptions = {
transform: transformFn,
starttag: '// injector:app',
endtag: '// endinjector',
addRootSlash: false
};
var injectFilesGlobal = gulp.src([
options.src + '/app/global/**/*.scss',
], { read: false });
var injectOptionsGlobal = {
transform: transformFn,
starttag: '// injector:global',
endtag: '// endinjector',
addRootSlash: false
};
return gulp.src(options.src + '/app/index.scss')
.pipe($.inject(injectFilesGlobal, injectOptionsGlobal))
.pipe($.inject(injectFiles, injectOptions))
.pipe(wiredep(options.wiredep))
.pipe($.sourcemaps.init())
.pipe($.sass(sassOptions)).on('error', options.errorHandler('Sass'))
.pipe($.autoprefixer()).on('error', options.errorHandler('Autoprefixer'))
.pipe($.sourcemaps.write())
.pipe(gulp.dest(options.tmp + '/serve/app/'))
.pipe(browserSync.reload({ stream: true }));
});
First, we have already deleted the commented lines, and added some new code that is highlighted. Because we are defining a new injector that uses the same transform method, we defined a function called transformFn to use in both cases without duplicating code.
In the variable injectFiles we exclude our special folder global because we want to inject those files at the top of “index.scss”, before bootstrap itself to take effect. We need also to modify the tags so the injector knows where to put our app files and where to put the global files and include the new injector as part of the process.
Finally, we have to do some adjustments in “index.scss” to define the new placeholder.
$icon-font-path: "../../bower_components/bootstrap-sass-official/assets/fonts/bootstrap/";
/* Overwrite bootstrap global variables */
// injector:global
// endinjector
/* Do not remove this comments bellow. It's the markers used by wiredep to inject
sass dependencies when defined in the bower.json of your dependencies */
// bower:scss
// endbower
.browsehappy {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}
.thumbnail {
height: 200px;
img.pull-right {
width: 50px;
}
}
/* Do not remove this comments bellow. It's the markers used by gulp-inject to inject
all your sass files automatically */
// injector:app
// endinjector
Now we can use bootstrap-sass with total control over the generated css files.
The final version of the code can be found on github.
Thanks for posting such a detailed explanation & making the final code available on github! My only suggestion is to include a date on your posts since the half-life of programming info these days is pretty short. 😉
You’re right, I complained myself of the same thing when reading other blogs. Sadly it’s not straightforward to add the creation date of the post so I’ll need to dig a little deeper into wordpress template system first to do that. Thank your for you’re suggestion.
Thanks again David! I’ve been having trouble with how the generator-gulp-angular handles styles too. Your explanation is very detailed and has help me immeasurably. I’m looking forward to more of your content 🙂 Cheers mate!
Glad I could help Jordan but keep in mind that there is a new version of that yeoman generator and now it combines the sass files from bootstrap with your own sass files right out of the box. The only thing missing is the “global” folder with the bootstrap variables to override the default style but you can pretty much hard code those files inside index.scss. I highly recommend you to do the update.
Nice post and a great breakdown of a Gulp task in general. I wish I could read such posts detailing all gulp parts this way. Thanks for taking the time to write this.
Thanks for the detailed explanation.
But I have an issue with decalartion of the filter:
var indexFilter = $.filter(‘index.scss’);
var vendorFilter = $.filter(‘vendor.scss’);
I get an “TypeError: undefined is not a function at gulp”
I have no idea where it comes from. Do you have an idea?!