Repository: NathanWalker/ng2-image-lazy-load Branch: master Commit: eee119fe3a69 Files: 31 Total size: 56.4 KB Directory structure: gitextract_n04mij4e/ ├── .editorconfig ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.d.ts ├── karma-shim.js ├── karma.conf.js ├── make.js ├── ng2-image-lazy-load.ts ├── package.json ├── protractor.conf.js ├── src/ │ ├── app/ │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── directives/ │ │ │ └── image-lazy-load.directive.ts │ │ └── services/ │ │ ├── image-lazy-load.service.ts │ │ ├── web-worker.service.stub.ts │ │ └── web-worker.service.ts │ ├── main.ts │ ├── polyfills.ts │ ├── public/ │ │ ├── index.html │ │ └── xhrWorker.js │ ├── style/ │ │ └── app.scss │ └── vendor.ts ├── tsconfig.json ├── tslint.json ├── typedoc.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # EditorConfig is awesome: http://EditorConfig.org # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .gitignore ================================================ # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Users Environment Variables .lock-wscript # OS generated files # .DS_Store ehthumbs.db Icon? Thumbs.db # Node Files # /node_modules /bower_components # Coverage # /coverage/ # Typing # /src/typings/tsd/ /typings/ /tsd_typings/ # Dist # /dist /public/__build__/ /src/*/__build__/ __build__/** .webpack.json # Doc # /doc/ # IDE # .idea/ *.swp *.js *.js.map *.d.ts !src/public/xhrWorker.js !index.d.ts !*.e2e.js !make.js !karma*.js !protractor.conf.js !webpack.config.js ================================================ FILE: .npmignore ================================================ # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory # Commenting this out is preferred by some people, see # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- node_modules # Users Environment Variables .lock-wscript .tsdrc #IntelliJ configuration files .idea dist dev docs lib test Thumbs.db .DS_Store *.yml *.ts !*.d.ts src/app/app.* src/public src/shims src/app.scss src/main.ts src/polyfills.ts src/vendor.ts src/*.d.ts src/*.js src/*.map src/style *.spec.* *.e2e.* CONTRIBUTING.md karma-shim.js karma.conf.js make.js protractor.conf.js tsconfig.json tslint.json typedoc.json typings.json typings webpack.config.js .travis.yml .jshintrc .editorconfig ================================================ FILE: CONTRIBUTING.md ================================================ ## Submitting Pull Requests **Please follow these basic steps to simplify pull request reviews - if you don't you'll probably just be asked to anyway.** * Please rebase your branch against the current master * Please ensure that the test suite passes **and** that code is lint free before submitting a PR by running: * ```npm test``` * If you've added new functionality, **please** include tests which validate its behaviour * Make reference to possible [issues](https://github.com/preboot/angular2-library-seed/issues) on PR comment ## Submitting bug reports * Please detail the affected browser(s) and operating system(s) * Please be sure to state which version of node **and** npm you're using ## How to get setup and run the code as well as test **Note** To run the demo, you must have node v4.x.x or higher and npm 3.x.x. ```bash git clone https://github.com/preboot/angular2-library-seed.git cd angular2-library-seed npm install # or `npm run reinstall` if you get an error npm start # start with --env dev ``` # Running tests ```bash npm test ``` ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Preboot team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![Dependency Status](https://david-dm.org/NathanWalker/ng2-image-lazy-load.svg)](https://david-dm.org/NathanWalker/ng2-image-lazy-load) [![devDependency Status](https://david-dm.org/NathanWalker/ng2-image-lazy-load/dev-status.svg)](https://david-dm.org/NathanWalker/ng2-image-lazy-load#info=devDependencies) # Not currently maintained You might try using [this great lazy load lib for now](https://github.com/tjoskar/ng-lazyload-image). The only thing the above lib doesn't have is the Web Worker support - I hope to circle back and update this lib at some point in future or contribute worker support to tjoskar's lib. # ng2-image-lazy-load Demo: https://ng2-image-lazy-load-demo.herokuapp.com ## Installation ```sh npm i ng2-image-lazy-load --save ``` ## Example implementation This library utilizes `WebWorkers` (https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) for background loading of images. By default, the location of the worker file is `assets/js/xhrWorker.js`. You can copy this [xhrWorker.js](https://github.com/NathanWalker/ng2-image-lazy-load/blob/master/src/public/xhrWorker.js) file for your own use from this repo or you can create your own. To set a custom path to load your worker file relative to your web server root: ``` WebWorkerService.workerUrl = 'path/to/your/custom_worker.js' ``` The example below will help illustrate this. Also, ensure you've loaded the angular/http bundle as well as this library falls back to using `Http` wherever `Worker` is not supported. ```ts import {BrowserModule} from "@angular/platform-browser"; import {NgModule, Component} from '@angular/core'; import {HttpModule} from '@angular/http'; import {ImageLazyLoadModule, WebWorkerService} from 'ng2-image-lazy-load'; // default: 'assets/js/xhrWorker.js' WebWorkerService.workerUrl = 'path/to/your/xhrWorker.js'; // default: true // set to false if you want to force Http instead of WebWorker WebWorkerService.enabled = true; @Component({ selector: 'app', template: `
` }) export class AppComponent { public images: Array = [ { name:`image 1`, url:`image.jpg` }, { name:`image 2`, url:`image_2.jpg` } ]; } @NgModule({ imports: [ BrowserModule, HttpModule, ImageLazyLoadModule ], declarations: [AppComponent], bootstrap: [AppComponent] }) export class AppModule { } ``` ### Configuration You can configure custom headers as well as custom loading, loaded and error classes by using the `imageLazyLoadConfig` directive: ``` // view template
// Component public lazyLoadConfig: IImageLazyLoadConfig = { headers: { 'Authorization': 'Bearer auth-token' }, loadingClass: 'custom-loading', loadedClass: 'custom-loaded', errorClass: 'custom-error' }; ``` ## API ### ImageLazyLoaderService #### Properties: - `imageCache:any`: Object where the key is the url of the image the library has already loaded and doesn't need to be loaded again. i.e., {'http://domain.com/image.png':true} #### Methods: - `load(url: string, headers?: any): Promise`: Load url with optional custom headers - `loadViaWorker(url: string, headers?: any): Promise`: Use a webworker directly to load url with optional custom headers - `loadViaHttp(url: string, headers?: any): Promise`: Use the `Http` service directly to load url with optional custom headers ### WebWorkerService ##### This is a helper service used by the library that wraps the usage of the browser's `Worker` api, however you can use it directly if you'd like to interact with it. #### Properties: - `static supported: boolean`: Determine if workers are supported - `static workerUrl: string`: Used to set the path to a worker file. Defaults to 'assets/js/xhrWorker.js' - `activeWorkers: Array`: At any given moment, this can be checked to see how many workers are currently activated #### Methods: - `load(config: any, msgFn: any, errorFn?: any):number`: Load a configuration with your worker and wire it to a `message` function and/or an `error` function. Returns an `id` which can be used to terminate the worker. - `terminate(id: number)`: Terminate the worker # How to contribute See [CONTRIBUTING](https://github.com/NathanWalker/ng2-image-lazy-load/blob/master/CONTRIBUTING.md) # Big Thank You This library was made possible with help from this article by [Olivier Combe](https://github.com/ocombe): https://medium.com/@OCombe/how-to-publish-a-library-for-angular-2-on-npm-5f48cdabf435 Also, this project setup is based on the excellent [angular2-seed](https://github.com/mgechev/angular2-seed) by [Minko Gechev](https://github.com/mgechev). # License MIT ================================================ FILE: index.d.ts ================================================ import { ModuleWithProviders } from "@angular/core"; export * from './src/app/services/image-lazy-load.service'; export * from './src/app/services/web-worker.service'; export * from './src/app/directives/image-lazy-load.directive'; export declare class ImageLazyLoadModule { static forRoot(configuredProviders: Array): ModuleWithProviders; } ================================================ FILE: karma-shim.js ================================================ Error.stackTraceLimit = Infinity; require('core-js/client/shim'); require('reflect-metadata'); require('ts-helpers'); require('zone.js/dist/zone'); require('zone.js/dist/long-stack-trace-zone'); require('zone.js/dist/proxy'); require('zone.js/dist/sync-test'); require('zone.js/dist/jasmine-patch'); require('zone.js/dist/async-test'); require('zone.js/dist/fake-async-test'); /* Ok, this is kinda crazy. We can use the the context method on require that webpack created in order to tell webpack what files we actually want to require or import. Below, context will be a function/object with file names as keys. using that regex we are saying look in client/app and find any file that ends with '.spec.ts' and get its path. By passing in true we say do this recursively */ var appContext = require.context('./src', true, /\.spec\.ts/); // get all the files, for each file, call the context function // that will require the file and load it up here. Context will // loop and require those spec files here appContext.keys().forEach(appContext); // Select BrowserDomAdapter. // see https://github.com/AngularClass/angular2-webpack-starter/issues/124 // Somewhere in the test setup var testing = require('@angular/core/testing'); var browser = require('@angular/platform-browser-dynamic/testing'); testing.TestBed.initTestEnvironment(browser.BrowserDynamicTestingModule, browser.platformBrowserDynamicTesting()); ================================================ FILE: karma.conf.js ================================================ var path = require('path'); var webpackConfig = require('./webpack.config'); var ENV = process.env.npm_lifecycle_event; var isTestWatch = ENV === 'test-watch'; module.exports = function (config) { var _config = { // base path that will be used to resolve all patterns (eg. files, exclude) basePath: '', // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter frameworks: ['jasmine'], // list of files / patterns to load in the browser files: [ { pattern: './karma-shim.js', watched: false } ], // list of files to exclude exclude: [], // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { './karma-shim.js': ['webpack', 'sourcemap'] }, webpack: webpackConfig, webpackMiddleware: { // webpack-dev-middleware configuration // i. e. stats: 'errors-only' }, webpackServer: { noInfo: true // please don't spam the console when running in karma! }, // test results reporter to use // possible values: 'dots', 'progress', 'mocha' // available reporters: https://npmjs.org/browse/keyword/karma-reporter reporters: ["mocha"], // web server port port: 9876, // enable / disable colors in the output (reporters and logs) colors: true, // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG logLevel: config.LOG_INFO, // enable / disable watching file and executing tests whenever any file changes autoWatch: false, // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher browsers: isTestWatch ? ['Chrome'] : ['PhantomJS'], // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits singleRun: true }; if (!isTestWatch) { _config.reporters.push("coverage"); _config.coverageReporter = { dir: 'coverage/', reporters: [{ type: 'json', dir: 'coverage', subdir: 'json', file: 'coverage-final.json' }] }; } config.set(_config); }; ================================================ FILE: make.js ================================================ var pkg = require('./package.json'); var path = require('path'); var Builder = require('systemjs-builder'); var name = pkg.name; var builder = new Builder(); var config = { baseURL: '.', transpiler: 'typescript', typescriptOptions: { module: 'cjs' }, map: { typescript: './node_modules/typescript/lib/typescript.js', '@angular': './node_modules/@angular', rxjs: './node_modules/rxjs' }, paths: { '*': '*.js' }, meta: { './node_modules/@angular/*': { build: false }, './node_modules/rxjs/*': { build: false } } }; builder.config(config); builder .bundle(name, path.resolve(__dirname, 'bundles/', name + '.js')) .then(function() { console.log('Build complete.'); }) .catch(function(err) { console.log('Error', err); }); ================================================ FILE: ng2-image-lazy-load.ts ================================================ import {NgModule, ModuleWithProviders} from "@angular/core"; import {IMAGELAZYLOAD_DIRECTIVES} from './src/app/directives/image-lazy-load.directive'; import {ImageLazyLoaderService} from './src/app/services/image-lazy-load.service'; import {WebWorkerService} from './src/app/services/web-worker.service'; // for manual imports export * from './src/app/services/image-lazy-load.service'; export * from './src/app/services/web-worker.service'; export * from './src/app/directives/image-lazy-load.directive'; @NgModule({ declarations: [ IMAGELAZYLOAD_DIRECTIVES ], providers: [ ImageLazyLoaderService, WebWorkerService ], exports: [ IMAGELAZYLOAD_DIRECTIVES ] }) export class ImageLazyLoadModule { static forRoot(configuredProviders: Array): ModuleWithProviders { return { ngModule: ImageLazyLoadModule, providers: configuredProviders }; } } ================================================ FILE: package.json ================================================ { "name": "ng2-image-lazy-load", "version": "2.0.9", "description": "Angular2 image lazy loader library", "repository": { "url": "https://github.com/NathanWalker/ng2-image-lazy-load" }, "main": "ng2-image-lazy-load.js", "typings": "./index.d.ts", "author": "Nathan Walker ", "license": "MIT", "scripts": { "clean": "rimraf node_modules doc dist && npm cache clean", "clean-install": "npm run clean && npm install", "clean-start": "npm run clean-install && npm start", "watch": "webpack --watch --progress --profile", "build": "rimraf dist && webpack --progress --profile --bail", "server": "webpack-dashboard -- webpack-dev-server --inline --port 8080", "webdriver-update": "webdriver-manager update", "webdriver-start": "webdriver-manager start", "lint": "tslint --force \"src/**/*.ts\" --exclude \"src/**/*.d.ts\"", "e2e": "protractor", "e2e-live": "protractor --elementExplorer", "pretest": "npm run lint", "test": "karma start", "posttest": "remap-istanbul -i coverage/json/coverage-final.json -o coverage/html -t html", "test-watch": "karma start --no-single-run --auto-watch", "ci": "npm run e2e && npm run test", "docs": "typedoc --options typedoc.json src/app/app.component.ts", "start": "npm run server", "start:hmr": "npm run server -- --hot", "postinstall": "npm run webdriver-update" }, "dependencies": { "@angular/common": "2.0.1", "@angular/compiler": "2.0.1", "@angular/core": "2.0.1", "@angular/forms": "2.0.1", "@angular/http": "2.0.1", "@angular/platform-browser": "2.0.1", "@angular/platform-browser-dynamic": "2.0.1", "@angular/router": "3.0.1", "core-js": "^2.4.1", "reflect-metadata": "^0.1.3", "rxjs": "5.0.0-beta.12", "zone.js": "^0.6.21" }, "devDependencies": { "@angularclass/hmr": "^1.0.1", "@angularclass/hmr-loader": "^3.0.2", "@types/core-js": "^0.9.0", "@types/jasmine": "^2.2.29", "@types/node": "^6.0.38", "@types/protractor": "^1.5.16", "@types/selenium-webdriver": "2.44.26", "angular2-template-loader": "^0.4.0", "autoprefixer": "^6.3.2", "awesome-typescript-loader": "^2.2.4", "codelyzer": "0.0.26", "copy-webpack-plugin": "^3.0.0", "css-loader": "^0.25.0", "extract-text-webpack-plugin": "^2.0.0-beta.4", "file-loader": "^0.9.0", "html-loader": "^0.4.0", "html-webpack-plugin": "^2.8.1", "istanbul-instrumenter-loader": "^0.2.0", "jasmine-core": "^2.3.4", "jasmine-spec-reporter": "^2.4.0", "json-loader": "^0.5.3", "karma": "1.3.0", "karma-chrome-launcher": "^2.0.0", "karma-coverage": "^1.0.0", "karma-jasmine": "^1.0.2", "karma-mocha-reporter": "^2.0.3", "karma-phantomjs-launcher": "^1.0.0", "karma-remap-istanbul": "0.2.1", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "2.0.1", "node-sass": "^3.4.2", "null-loader": "0.1.1", "phantomjs-prebuilt": "^2.1.4", "postcss-loader": "^0.13.0", "protractor": "^3.1.1", "raw-loader": "0.5.1", "remap-istanbul": "^0.6.4", "rimraf": "^2.5.1", "sass-loader": "^4.0.0", "shelljs": "^0.7.0", "style-loader": "^0.13.0", "ts-helpers": "^1.1.1", "tslint": "^3.4.0", "tslint-loader": "^2.1.0", "typedoc": "^0.4.4", "typescript": "2.0.2", "url-loader": "^0.5.6", "webpack": "^2.1.0-beta.26", "webpack-dashboard": "^0.1.8", "webpack-dev-server": "2.1.0-beta.5" } } ================================================ FILE: protractor.conf.js ================================================ exports.config = { baseUrl: 'http://localhost:8080/', specs: [ 'src/**/*.e2e-spec.js' ], exclude: [], framework: 'jasmine2', allScriptsTimeout: 110000, jasmineNodeOpts: { showTiming: true, showColors: true, isVerbose: false, includeStackTrace: false, defaultTimeoutInterval: 400000 }, directConnect: true, capabilities: { 'browserName': 'chrome' }, onPrepare: function () { var SpecReporter = require('jasmine-spec-reporter'); // add jasmine spec reporter jasmine.getEnv().addReporter(new SpecReporter({displayStacktrace: true})); browser.ignoreSynchronization = true; }, /** * Angular 2 configuration * * useAllAngular2AppRoots: tells Protractor to wait for any angular2 apps on the page instead of just the one matching * `rootEl` * */ useAllAngular2AppRoots: true }; ================================================ FILE: src/app/app.component.html ================================================

Angular2 Image Lazy Loader Demo

Using img tag

As you scroll, the img's lazily load as they scroll into view.

Using background-image

As you scroll, images lazily load into the background-image of the containers as they scroll into view.

Using img tag with custom loading container

By default, when using img, the immediate container surrounding it will be used for the 'loading' and 'loaded' class. However you can set [imageLazyLoadingContainer] to a String which represents the selector of the parent to use for the classes. As you scroll, img's lazily load into the background-image of the containers as they scroll into view and notice that the 'loading-container' gets the classes.
Top
================================================ FILE: src/app/app.component.scss ================================================ // styles applied on :host are applied on the current component, "app" in this case :host { display: block; } header { background-color: #fff; padding:0; position: fixed; top: 0; left: 0; width: 100%; box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.5); } h1 { color: #000; text-align: center; font-family: sans-serif; } img { width:50%; } main { padding: 1em; font-family: Arial, Helvetica, sans-serif; text-align: center; margin-top: 50px; display: block; } footer { text-align: center; font-size: 0.8em; width: 100%; position: absolute; bottom: 20px; a { color: #fff; text-decoration: none; font-family: sans-serif; &:hover { text-decoration: underline; } } } ================================================ FILE: src/app/app.component.ts ================================================ import { Component, ViewEncapsulation } from '@angular/core'; import {Observable} from 'rxjs/Observable'; import 'rxjs/add/observable/fromEvent'; import 'rxjs/add/operator/debounceTime'; import '../style/app.scss'; @Component({ selector: 'my-app', // templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], encapsulation: ViewEncapsulation.None }) export class AppComponent { public atTop: boolean = true; public images: Array = [ { id: 1, name: `Oneonta Falls 1`, url: `img/oneonta.jpg` }, { id: 2, name: `Oneonta Falls 2`, url: `img/DSC01920-L.jpg` }, { id: 3, name: `Oneonta Falls 3`, url: `img/oneonta-gorge-800x600.jpg` }, { id: 4, name: `Oneonta Falls 4`, url: `img/oneonta-gorge-11.jpg` }, { id: 5, name: `Oneonta Falls 5`, url: `img/IMG_2039.jpg` }, { id: 6, name: `Oneonta Falls 6`, url: `img/horsetail_falls_9-b.jpg` }, { id: 7, name: `Oneonta Falls 7`, url: `img/P1000662-L.jpg` }, { id: 8, name: `Oneonta Falls 8`, url: `img/DSC01920-L.jpg` }, { id: 9, name: `Oneonta Falls 9`, url: `img/IMG_8356_Medium.JPG` }, { id: 10, name: `Oneonta Falls 10`, url: `img/IMG_2036.jpg` } ]; constructor() { let scrollStream = Observable.fromEvent(window, 'scroll').debounceTime(500); scrollStream.subscribe(() => { this.atTop = window.pageYOffset < 25; }); } public addImage(): void { this.images.push({ id: 11, name: `Simpsons as the ${this.images.length + 1}th image!`, url: `img/The-Simpsons-post2.jpg`, added: true }); if (window.confirm(`You added an image dynamically, click "Ok" to scroll down to see it load in when you get there!`)) { setTimeout(function(){ location.hash = '#11'; }, 100); } } } ================================================ FILE: src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { HttpModule } from '@angular/http'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { IMAGELAZYLOAD_DIRECTIVES } from './directives/image-lazy-load.directive'; import { ImageLazyLoaderService } from './services/image-lazy-load.service'; import { WebWorkerService } from './services/web-worker.service'; WebWorkerService.workerUrl = 'xhrWorker.js'; @NgModule({ imports: [ BrowserModule, HttpModule, FormsModule ], declarations: [ AppComponent, IMAGELAZYLOAD_DIRECTIVES ], providers: [ ImageLazyLoaderService, WebWorkerService ], exports: [ IMAGELAZYLOAD_DIRECTIVES ], bootstrap: [AppComponent] }) export class AppModule { } ================================================ FILE: src/app/directives/image-lazy-load.directive.ts ================================================ import { Directive, ContentChildren, QueryList, Input, ElementRef, Renderer, OnInit, AfterContentInit } from '@angular/core'; import {Observable} from 'rxjs/Observable'; import {Subscription} from 'rxjs/Subscription'; import 'rxjs/add/observable/fromEvent'; import 'rxjs/add/operator/debounceTime'; import {ImageLazyLoaderService, IImageLazyLoadConfig} from '../services/image-lazy-load.service'; @Directive({ selector: '[imageLazyLoadItem]' }) export class ImageLazyLoadItemDirective { @Input('imageLazyLoadItem') imageLazyLoadItem: string; @Input() imageLazyLoadingContainer: string; public loading: boolean = false; public loaded: boolean = false; public error: boolean = false; private tagName: string; constructor(private el: ElementRef, private renderer: Renderer, private lazyLoader: ImageLazyLoaderService) { this.tagName = el.nativeElement.tagName; } /* * @returns return position/dimension info as an Object `{top, left, bottom}`. */ getPosition() { let box = this.el.nativeElement.getBoundingClientRect(); let top = box.top + (window.pageYOffset - document.documentElement.clientTop); return { top: top, left: box.left + (window.pageXOffset - document.documentElement.clientLeft), bottom: top + this.el.nativeElement.clientHeight }; } /* * @returns container target to place `loading`/`loaded` classes onto. */ getLoadingContainer() { if (this.imageLazyLoadingContainer) { // find parent node with specified selector let collectionHas = (a: any, b: any) => { for (let i in a) { if (a[i] === b) { return true; } } return false; }; let all = document.querySelectorAll(this.imageLazyLoadingContainer); let cur = this.el.nativeElement.parentNode; while (cur && !collectionHas(all, cur)) { cur = cur.parentNode; } if (cur) { return cur; } else { // fallback to direct parentNode if not found return this.el.nativeElement.parentNode; } } else { // default is direct parentNode for IMG and the node itself for background-image if (this.tagName === 'IMG') { return this.el.nativeElement.parentNode; } else { return this.el.nativeElement; } } } hasClassName(name: string) { return new RegExp('(?:^|\\s+)' + name + '(?:\\s+|$)').test(this.getLoadingContainer().className); } addClassName(name: string) { if (!this.hasClassName(name)) { let container = this.getLoadingContainer(); container.className = container.className ? [container.className, name].join(' ') : name; } } removeClassName(name: string) { if (this.hasClassName(name)) { let container = this.getLoadingContainer(); let c = container.className; container.className = c.replace(new RegExp('(?:^|\\s+)' + name + '(?:\\s+|$)', 'g'), ''); } } toggleLoaded(enable: boolean) { this.loaded = enable; if (enable) { this.removeClassName(this.lazyLoader.config.loadingClass); this.addClassName(this.lazyLoader.config.loadedClass); } else { this.removeClassName(this.lazyLoader.config.loadedClass); } } /* * starts loading the image in the background. */ loadImage() { if (!this.loaded && !this.loading) { this.loading = true; this.addClassName(this.lazyLoader.config.loadingClass); let customHeaders: any = this.lazyLoader.config.headers ? this.lazyLoader.config.headers : null; this.lazyLoader.load(this.imageLazyLoadItem, customHeaders).then(() => { this.setImage(); }, (err) => { this.error = true; this.loading = false; this.removeClassName(this.lazyLoader.config.loadingClass); this.addClassName(this.lazyLoader.config.errorClass); }); } } /* * sets the image to `imageLazyLoadItem`. */ setImage() { if (!this.loaded) { if (this.tagName === 'IMG') { this.renderer.setElementAttribute(this.el.nativeElement, 'src', this.imageLazyLoadItem); } else { this.renderer.setElementAttribute(this.el.nativeElement, 'style', `background-image:url('${this.imageLazyLoadItem}')`); } this.loading = false; this.toggleLoaded(true); } } } @Directive({ selector: '[imageLazyLoadArea]' }) export class ImageLazyLoadAreaDirective implements OnInit, AfterContentInit { @Input('imageLazyLoadArea') threshold: number; /** * Object that implements IImageLazyLoadConfig: * headers?: any = custom headers * loadingClass?: string = 'custom-loading-class' * loadedClass?: string = 'custom-loaded-class' * errorClass?: string = 'custom-error-class' */ @Input() imageLazyLoadConfig: IImageLazyLoadConfig; @ContentChildren(ImageLazyLoadItemDirective) private items: QueryList; private itemsToLoad: Array; private _sub: Subscription; constructor(private lazyLoader: ImageLazyLoaderService) {} private loadInView(list?: Array): void { this.itemsToLoad = (list || this.itemsToLoad).filter((item) => !item.loaded && !item.loading); for (let item of this.itemsToLoad) { let ePos = item.getPosition(); if (ePos.bottom > 0 && (ePos.bottom >= (window.pageYOffset - this.threshold)) && (ePos.top <= ((window.pageYOffset + window.innerHeight) + this.threshold))) { item.loadImage(); } } if (this.itemsToLoad.length === 0 && this._sub) { // subscription is no longer needed this._sub.unsubscribe(); this._sub = undefined; } } private scrollSubscribe() { let scrollStream = Observable.fromEvent(window, 'scroll').debounceTime(250); this._sub = scrollStream.subscribe(() => { this.loadInView(); }); } private init() { let subScroll = () => { if (!this._sub) { this.scrollSubscribe(); } }; // load the initial children in view if (this.items.length) { // using setTimeout to ensure styles have applied before triggering load setTimeout(() => { this.loadInView(this.items.toArray()); }); } // listen to scroll event subScroll(); // fired with subsequent changes // ideally this would fire on subscribe but it doesn't // therefore the above ensures it's handled on ngAfterContentInit this.items.changes.subscribe((list) => { this.loadInView(list.toArray()); // since scroll subscription is unsuscribed when all items have loaded // ensure it is re-subscribed when changes occur subScroll(); }); } ngOnInit() { this.threshold = +this.threshold || 100; if (typeof (this.imageLazyLoadConfig) === 'object') { this.lazyLoader.config = this.imageLazyLoadConfig; } } ngAfterContentInit() { this.init(); } } export const IMAGELAZYLOAD_DIRECTIVES: any[] = [ImageLazyLoadAreaDirective, ImageLazyLoadItemDirective]; ================================================ FILE: src/app/services/image-lazy-load.service.ts ================================================ import {Injectable} from '@angular/core'; import {Http, Headers, RequestOptions} from '@angular/http'; import {WebWorkerService} from './web-worker.service'; export interface IImageLazyLoadConfig { headers?: any; loadingClass?: string; loadedClass?: string; errorClass?: string; } @Injectable() export class ImageLazyLoaderService { public imageCache: any = {}; // default config private _config: IImageLazyLoadConfig = { loadingClass: 'loading', loadedClass: 'loaded', errorClass: 'error' }; constructor(private http: Http, private worker: WebWorkerService) {} /* * Loads the url via `WebWorker` where supported and gracefully degrades to using `Http` if needed. * @param url url of image to load. * @param headers **(optional)** any custom headers (as an `Object`) that may be required to load the image. * @returns return `Promise` */ public load(url: string, headers?: any): Promise { if (this.imageCache[url]) { // image has been previously loaded, rely on browser cache return Promise.resolve(true); } else if (WebWorkerService.supported && WebWorkerService.enabled) { return this.loadViaWorker(url, headers); } else { return this.loadViaHttp(url, headers); } } /* * Loads the url via `WebWorker` directly. * @param url url of image to load. * @param headers **(optional)** any custom headers (as an `Object`) that may be required to load the image. * @returns return `Promise` */ public loadViaWorker(url: string, headers?: any): Promise { return new Promise((resolve, reject) => { let id: any, completeHandler: any, msgFn: any, errorFn: any; completeHandler = (success: boolean, err?: any) => { this.worker.terminate(id); if (success) { this.imageCache[url] = true; resolve(true); } else { reject(err); } }; msgFn = (e: any) => { if (e && e.data !== 'ERROR') { completeHandler(true); } else { completeHandler(false, e); } }; errorFn = (e: any) => { completeHandler(false, e); }; let config: any = { method: 'GET', url: url }; // optionally set headers if (headers) { config.headers = headers; } id = this.worker.load(config, msgFn, errorFn); }); } /* * Loads the url via `Http` directly. * @param url url of image to load. * @param headers **(optional)** any custom headers (as an `Object`) that may be required to load the image. * @returns return `Promise` */ public loadViaHttp(url: string, headers?: any): Promise { let ro: RequestOptions = null; if (headers) { ro = new RequestOptions({ headers: new Headers(headers) }); } return new Promise((resolve, reject) => { this.http.get(url, ro) .subscribe(res => { this.imageCache[url] = true; resolve(true); }); }); } /** * Custom config **/ public set config(value: IImageLazyLoadConfig) { if (value) { if (value.headers) { this._config.headers = value.headers; } if (value.loadingClass) { this._config.loadingClass = value.loadingClass; } if (value.loadedClass) { this._config.loadedClass = value.loadedClass; } if (value.errorClass) { this._config.errorClass = value.errorClass; } } } public get config(): IImageLazyLoadConfig { return this._config; } } ================================================ FILE: src/app/services/web-worker.service.stub.ts ================================================ import {Injectable} from '@angular/core'; @Injectable() export class WebWorkerStub { static supported: boolean = true; static workerUrl: string = 'assets/js/xhrWorker.js'; public activeWorkers: Array = []; // public workersCalled: number = 0; load(config: any, msgFn: any, errorFn?: any): number { let id: number = Math.floor(Math.random() * 1000000000000); this.activeWorkers.push({ id: id, config: config }); setTimeout(() => { if (msgFn) { msgFn({data: 'success'}); } else if (errorFn) { errorFn({data: 'ERROR'}); } }); // this.workersCalled++; return id; } terminate(id: number): void { let index = this.activeWorkers.findIndex(item => item.id === id); if (index > -1) { this.activeWorkers.splice(index, 1); } } } ================================================ FILE: src/app/services/web-worker.service.ts ================================================ import {Injectable} from '@angular/core'; @Injectable() export class WebWorkerService { static supported: boolean = typeof (Worker) !== 'undefined'; static enabled: boolean = true; // enabled by default, however can be manually turned off static workerUrl: string = 'assets/js/xhrWorker.js'; public activeWorkers: Array = []; load(config: any, msgFn: any, errorFn?: any): number { if (typeof(config) !== 'object') { throw(`config must be an Object with method and url defined.`); } else if (!config.url) { throw(`config.url must be defined.`); } let id: number = Math.floor(Math.random() * 1000000000000); let w = new Worker(WebWorkerService.workerUrl); if (msgFn) { w.addEventListener('message', msgFn, false); } if (errorFn) { w.addEventListener('error', errorFn, false); } this.activeWorkers.push({ id: id, worker: w, msgFn: msgFn, errorFn: errorFn }); w.postMessage(config); return id; } terminate(id: number): void { let index = this.activeWorkers.findIndex(item => item.id === id); if (index > -1) { let activeWorker = this.activeWorkers[index]; if (activeWorker && activeWorker.worker) { if (activeWorker.worker.msgFn) { activeWorker.worker.removeEventListener('message', activeWorker.worker.msgFn); } if (activeWorker.worker.errorFn) { activeWorker.worker.removeEventListener('error', activeWorker.worker.errorFn); } activeWorker.worker.terminate(); } this.activeWorkers.splice(index, 1); } } } ================================================ FILE: src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; // depending on the env mode, enable prod mode or add debugging modules if (process.env.ENV === 'build') { enableProdMode(); } export function main() { return platformBrowserDynamic().bootstrapModule(AppModule); } if (document.readyState === 'complete') { main(); } else { document.addEventListener('DOMContentLoaded', main); } ================================================ FILE: src/polyfills.ts ================================================ import 'core-js/client/shim'; import 'reflect-metadata'; require('zone.js/dist/zone'); import 'ts-helpers'; if (process.env.ENV === 'build') { // Production } else { // Development Error['stackTraceLimit'] = Infinity; require('zone.js/dist/long-stack-trace-zone'); } ================================================ FILE: src/public/index.html ================================================ Angular2 Image Lazy Loader
================================================ FILE: src/public/xhrWorker.js ================================================ self.addEventListener('message', function(e) { var data, header, xhr; data = e.data; xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status === 200) { postMessage(xhr.responseText); } else { postMessage('ERROR'); } return self.close(); } }; xhr.onerror = function() { postMessage('ERROR'); return self.close(); }; xhr.open(data.method, data.url); if (data.headers) { for (header in data.headers) { if (data.headers[header]) { xhr.setRequestHeader(header, data.headers[header]); } } } return xhr.send(); }); ================================================ FILE: src/style/app.scss ================================================ /* Reset */ html, body, div { border: 0; margin: 0; padding: 0; } /* Box-sizing border-box */ * { box-sizing: border-box; } /* Set up a default font and some padding to provide breathing room */ body { font-family: Roboto, "Helvetica Neue", sans-serif; font-size: 16px; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } p { font-weight: 400; letter-spacing: 0.01em; line-height: 20px; margin-bottom: 1em; margin-top: 1em; } ul { margin: 10px 0 0 0; padding: 0 0 0 20px; } li { font-weight: 400; margin-top: 4px; } input { border: 1px solid #106cc8; font-size: 14px; height: 40px; outline: none; padding: 8px; } button { background-color: #106cc8; border-style: none; color: rgba(255, 255, 255, 0.87); cursor: pointer; display: inline-block; font-size: 14px; height: 40px; padding: 8px 18px; text-decoration: none; } button:hover { background-color: #28739e; } body { background-color:rgba(0,0,0,.4); } h1, h2, h3, h5 { text-align: center; width: 90%; margin: 10px auto; } .spinner:before { transform: translate3d(0, 0, 0); -ms-transform: translate3d(0, 0, 0); -moz-transform: translate3d(0, 0, 0); -webkit-transform: translate3d(0, 0, 0); -o-transform: translate3d(0, 0, 0); -webkit-transition: opacity 0.5s ease-out; -moz-transition: opacity 0.5s ease-out; -o-transition: opacity 0.5s ease-out; transition: opacity 0.5s ease-out; -webkit-animation: rotating 1s linear infinite; -moz-animation: rotating 1s linear infinite; -ms-animation: rotating 1s linear infinite; -o-animation: rotating 1s linear infinite; animation: rotating 1s linear infinite; opacity:1; top: 0; left: 0; bottom: 0; right: 0; position: absolute; margin: auto; color: rgba(0, 0, 0, 0.3); content: ""; color: #fff; background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxOC4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KICAgdmlld0JveD0iMCAwIDI5MCAyOTAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI5MCAyOTAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZmlsbD0iI2ZmZmZmZiIgZD0iTTE0NSwyNDEuNmMtNTMuMywwLTk2LjYtNDMuMi05Ni42LTk2LjZjMC01My4zLDQzLjItOTYuNiw5Ni42LTk2LjZjNTMuMywwLDk2LjYsNDMuMiw5Ni42LDk2LjYNCiAgYzAsMjYuNy0xMC44LDUwLjktMjguMyw2OC4zbDcuNiw3LjZjMTkuNC0xOS40LDMxLjUtNDYuMywzMS41LTc1LjljMC01OS4zLTQ4LTEwNy4zLTEwNy4zLTEwNy4zUzM3LjcsODUuNywzNy43LDE0NQ0KICBjMCw1OS4zLDQ4LDEwNy4zLDEwNy4zLDEwNy4zVjI0MS42eiIvPg0KPC9zdmc+DQo="); background-size: 50px 50px; height: 50px; width: 50px; background-repeat: no-repeat; } .demo { font-family: Verdana; } .demo a { display:block; text-align: center; cursor:pointer; } .demo a.top { position: fixed; bottom:10px; right:15px; font-size:14px; z-index: 10; background-color: rgba(0,0,0,.5); color:#fff; padding:8px 12px; border-radius: 4px; opacity:1; -webkit-transition: opacity 0.5s ease-out; -moz-transition: opacity 0.5s ease-out; -o-transition: opacity 0.5s ease-out; transition: opacity 0.5s ease-out; } .demo.at-top a.top { opacity: 0; } .demo ul { width: 88%; margin: 0 auto; background-color: rgba(255,255,255,.5); border-radius: 4px; padding-top: 10px; padding-bottom: 10px; max-width: 420px; } .demo ul li a { text-align: left; } .demo > div { margin:10px auto; min-height: 280px; max-height:500px; background-repeat: no-repeat; background-position: center center; background-size: cover; position: relative; } .demo > div[style] { width:95%; -webkit-box-shadow: 0 0 4px rgba(0,0,0,.6); -moz-box-shadow: 0 0 4px rgba(0,0,0,.6); box-shadow: 0 0 4px rgba(0,0,0,.6); } .demo > div img { display: block; margin:0 auto; max-width: 100%; max-height: 500px; -webkit-box-shadow: 0 0 4px rgba(0,0,0,.6); -moz-box-shadow: 0 0 4px rgba(0,0,0,.6); box-shadow: 0 0 4px rgba(0,0,0,.6); -webkit-transition: opacity 0.5s ease-out; -moz-transition: opacity 0.5s ease-out; -o-transition: opacity 0.5s ease-out; transition: opacity 0.5s ease-out; opacity:0; } .demo > div.loaded img { opacity:1; } .demo > div.loaded .spinner:before { opacity:0; } @media (min-width:670px) { .demo > div { min-height: 500px; } } @keyframes rotating { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @-moz-keyframes rotating { from { -moz-transform: rotate(0deg); } to { -moz-transform: rotate(360deg); } } @-ms-keyframes rotating { from { -ms-transform: rotate(0deg); } to { -ms-transform: rotate(360deg); } } @-o-keyframes rotating { from { -o-transform: rotate(0deg); } to { -o-transform: rotate(360deg); } } @-webkit-keyframes rotating { from { -webkit-transform: rotate(0deg); } to { -webkit-transform: rotate(360deg); } } ================================================ FILE: src/vendor.ts ================================================ // Angular 2 import '@angular/platform-browser'; import '@angular/platform-browser-dynamic'; import '@angular/core'; import '@angular/common'; import '@angular/http'; import '@angular/router'; import 'rxjs'; import '@angularclass/hmr'; // Other vendors for example jQuery, Lodash or Bootstrap // You can import js, ts, css, sass, ... ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ES5", "module": "commonjs", "emitDecoratorMetadata": true, "experimentalDecorators": true, "sourceMap": true, "noEmitHelpers": true }, "compileOnSave": false, "buildOnSave": false, "awesomeTypescriptLoaderOptions": { "forkChecker": true, "useWebpackText": true } } ================================================ FILE: tslint.json ================================================ { "rulesDirectory": [ "node_modules/codelyzer" ], "rules": { "class-name": true, "comment-format": [ true, "check-space" ], "curly": true, "eofline": true, "forin": true, "indent": [ true, "spaces" ], "label-position": true, "label-undefined": true, "max-line-length": [ true, 140 ], "member-access": false, "member-ordering": [ true, "static-before-instance", "variables-before-functions" ], "no-arg": true, "no-bitwise": true, "no-console": [ true, "debug", "info", "time", "timeEnd", "trace" ], "no-construct": true, "no-debugger": true, "no-duplicate-key": true, "no-duplicate-variable": true, "no-empty": false, "no-eval": true, "no-inferrable-types": true, "no-shadowed-variable": true, "no-string-literal": false, "no-switch-case-fall-through": true, "no-trailing-whitespace": false, "no-unused-expression": true, "no-unused-variable": true, "no-unreachable": true, "no-use-before-declare": true, "no-var-keyword": true, "object-literal-sort-keys": false, "one-line": [ true, "check-catch", "check-else", "check-whitespace" ], "quotemark": [ true, "single" ], "radix": true, "semicolon": [ "always" ], "triple-equals": [ true, "allow-null-check" ], "typedef-whitespace": [ true, { "call-signature": "nospace", "index-signature": "nospace", "parameter": "nospace", "property-declaration": "nospace", "variable-declaration": "nospace" } ], "variable-name": false, "whitespace": [ true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type" ], "directive-selector-name": [true, "camelCase"], "component-selector-name": [true, "kebab-case"], "directive-selector-type": [true, "attribute"], "component-selector-type": [true, "element"], "use-input-property-decorator": true, "use-output-property-decorator": true, "use-host-property-decorator": true, "no-input-rename": true, "no-output-rename": true, "use-life-cycle-interface": true, "use-pipe-transform-interface": true, "component-class-suffix": true, "directive-class-suffix": true, "directive-selector-prefix": [true, "my"], "component-selector-prefix": [true, "my"], "pipe-naming": [true, "camelCase", "my"] } } ================================================ FILE: typedoc.json ================================================ { "mode": "modules", "out": "doc", "theme": "default", "ignoreCompilerErrors": "true", "experimentalDecorators": "true", "emitDecoratorMetadata": "true", "target": "ES5", "moduleResolution": "node", "preserveConstEnums": "true", "stripInternal": "true", "suppressExcessPropertyErrors": "true", "suppressImplicitAnyIndexErrors": "true", "module": "commonjs" } ================================================ FILE: webpack.config.js ================================================ // Helper: root() is defined at the bottom var path = require('path'); var webpack = require('webpack'); // Webpack Plugins var CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin; var autoprefixer = require('autoprefixer'); var HtmlWebpackPlugin = require('html-webpack-plugin'); var ExtractTextPlugin = require('extract-text-webpack-plugin'); var CopyWebpackPlugin = require('copy-webpack-plugin'); var DashboardPlugin = require('webpack-dashboard/plugin'); var ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin; /** * Env * Get npm lifecycle event to identify the environment */ var ENV = process.env.npm_lifecycle_event; var isTestWatch = ENV === 'test-watch'; var isTest = ENV === 'test' || isTestWatch; var isProd = ENV === 'build'; module.exports = function makeWebpackConfig() { /** * Config * Reference: http://webpack.github.io/docs/configuration.html * This is the object where all configuration gets set */ var config = {}; /** * Devtool * Reference: http://webpack.github.io/docs/configuration.html#devtool * Type of sourcemap to use per build type */ if (isProd) { config.devtool = 'source-map'; } else if (isTest) { config.devtool = 'inline-source-map'; } else { config.devtool = 'eval-source-map'; } /** * Entry * Reference: http://webpack.github.io/docs/configuration.html#entry */ config.entry = isTest ? function(){return {}} : { 'polyfills': './src/polyfills.ts', 'vendor': './src/vendor.ts', 'app': './src/main.ts' // our angular app }; /** * Output * Reference: http://webpack.github.io/docs/configuration.html#output */ config.output = isTest ? {} : { path: root('dist'), publicPath: isProd ? '/' : 'http://localhost:8080/', filename: isProd ? 'js/[name].[hash].js' : 'js/[name].js', chunkFilename: isProd ? '[id].[hash].chunk.js' : '[id].chunk.js' }; /** * Resolve * Reference: http://webpack.github.io/docs/configuration.html#resolve */ config.resolve = { // only discover files that have those extensions extensions: ['.ts', '.js', '.json', '.css', '.scss', '.html'], }; var atlOptions = ''; if (isTest && !isTestWatch) { // awesome-typescript-loader needs to output inlineSourceMap for code coverage to work with source maps. atlOptions = 'inlineSourceMap=true&sourceMap=false'; } /** * Loaders * Reference: http://webpack.github.io/docs/configuration.html#module-loaders * List: http://webpack.github.io/docs/list-of-loaders.html * This handles most of the magic responsible for converting modules */ config.module = { rules: [ // Support for .ts files. { test: /\.ts$/, loaders: ['awesome-typescript-loader?' + atlOptions, 'angular2-template-loader', '@angularclass/hmr-loader'], exclude: [isTest ? /\.(e2e)\.ts$/ : /\.(spec|e2e)\.ts$/, /node_modules\/(?!(ng2-.+))/] }, // copy those assets to output { test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file-loader?name=fonts/[name].[hash].[ext]?' }, // Support for *.json files. {test: /\.json$/, loader: 'json-loader'}, // Support for CSS as raw text // use 'null' loader in test mode (https://github.com/webpack/null-loader) // all css in src/style will be bundled in an external css file { test: /\.css$/, exclude: root('src', 'app'), loader: isTest ? 'null-loader' : ExtractTextPlugin.extract({ fallbackLoader: 'style-loader', loader: ['css-loader', 'postcss-loader'] }) }, // all css required in src/app files will be merged in js files {test: /\.css$/, include: root('src', 'app'), loader: 'raw-loader!postcss-loader'}, // support for .scss files // use 'null' loader in test mode (https://github.com/webpack/null-loader) // all css in src/style will be bundled in an external css file { test: /\.scss$/, exclude: root('src', 'app'), loader: isTest ? 'null-loader' : ExtractTextPlugin.extract({ fallbackLoader: 'style-loader', loader: ['css-loader', 'postcss-loader', 'sass-loader'] }) }, // all css required in src/app files will be merged in js files {test: /\.scss$/, exclude: root('src', 'style'), loader: 'raw-loader!postcss-loader!sass-loader'}, // support for .html as raw text // todo: change the loader to something that adds a hash to images {test: /\.html$/, loader: 'raw-loader', exclude: root('src', 'public')} ] }; if (isTest && !isTestWatch) { // instrument only testing sources with Istanbul, covers ts files config.module.rules.push({ test: /\.ts$/, enforce: 'post', include: path.resolve('src'), loader: 'istanbul-instrumenter-loader', exclude: [/\.spec\.ts$/, /\.e2e\.ts$/, /node_modules/] }); } if (!isTest || !isTestWatch) { // tslint support config.module.rules.push({ test: /\.ts$/, enforce: 'pre', loader: 'tslint-loader' }); } /** * Plugins * Reference: http://webpack.github.io/docs/configuration.html#plugins * List: http://webpack.github.io/docs/list-of-plugins.html */ config.plugins = [ // Define env variables to help with builds // Reference: https://webpack.github.io/docs/list-of-plugins.html#defineplugin new webpack.DefinePlugin({ // Environment helpers 'process.env': { ENV: JSON.stringify(ENV) } }), // Workaround needed for angular 2 angular/angular#11580 new webpack.ContextReplacementPlugin( // The (\\|\/) piece accounts for path separators in *nix and Windows /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/, root('./src') // location of your src ), // Tslint configuration for webpack 2 new webpack.LoaderOptionsPlugin({ options: { /** * Apply the tslint loader as pre/postLoader * Reference: https://github.com/wbuchwalter/tslint-loader */ tslint: { emitErrors: false, failOnHint: false }, /** * Sass * Reference: https://github.com/jtangelder/sass-loader * Transforms .scss files to .css */ sassLoader: { //includePaths: [path.resolve(__dirname, "node_modules/foundation-sites/scss")] }, /** * PostCSS * Reference: https://github.com/postcss/autoprefixer-core * Add vendor prefixes to your css */ postcss: [ autoprefixer({ browsers: ['last 2 version'] }) ] } }) ]; if (!isTest && !isProd) { config.plugins.push(new DashboardPlugin()); } if (!isTest && !isTestWatch) { config.plugins.push( new ForkCheckerPlugin(), // Generate common chunks if necessary // Reference: https://webpack.github.io/docs/code-splitting.html // Reference: https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin new CommonsChunkPlugin({ name: ['vendor', 'polyfills'] }), // Inject script and link tags into html files // Reference: https://github.com/ampedandwired/html-webpack-plugin new HtmlWebpackPlugin({ template: './src/public/index.html', chunksSortMode: 'dependency' }), // Extract css files // Reference: https://github.com/webpack/extract-text-webpack-plugin // Disabled when in test mode or not in build mode new ExtractTextPlugin({filename: 'css/[name].[hash].css', disable: !isProd}) ); } // Add build specific plugins if (isProd) { config.plugins.push( // Reference: http://webpack.github.io/docs/list-of-plugins.html#noerrorsplugin // Only emit files when there are no errors new webpack.NoErrorsPlugin(), // // Reference: http://webpack.github.io/docs/list-of-plugins.html#dedupeplugin // // Dedupe modules in the output // new webpack.optimize.DedupePlugin(), // Reference: http://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin // Minify all javascript, switch loaders to minimizing mode new webpack.optimize.UglifyJsPlugin({sourceMap: true, mangle: { keep_fnames: true }}), // Copy assets from the public folder // Reference: https://github.com/kevlened/copy-webpack-plugin new CopyWebpackPlugin([{ from: root('src/public') }]) ); } /** * Dev server configuration * Reference: http://webpack.github.io/docs/configuration.html#devserver * Reference: http://webpack.github.io/docs/webpack-dev-server.html */ config.devServer = { contentBase: './src/public', historyApiFallback: true, quiet: true, stats: 'minimal' // none (or false), errors-only, minimal, normal (or true) and verbose }; return config; }(); // Helper functions function root(args) { args = Array.prototype.slice.call(arguments, 0); return path.join.apply(path, [__dirname].concat(args)); }