Search code examples
angularwebpackasp.net-coreangular2-universalsvgpanzoom

angular2 svgPanZoom, window is not defined, webpack issue?


I would like to implement a zoom feature on my website which has a SVG image displayed.

I saw this library github.com/ariutta/svg-pan-zoom which provides the exact features i need,

However i can't seem to get it to work with angular2, window is not reachable.

after some research i figured i have to shim window into the webpack export for svg-pan-zoom. Maybe i'm not looking for the right thing but i think it is quite amazing there is so much work that needs to be done just to import a 3rd party javascript. best clue i could find is this : https://github.com/ariutta/svg-pan-zoom/issues/207 EDIT: See answer.

I used this angular 2 aspnet core starting project: https://damienbod.com/2017/01/01/building-production-ready-angular-apps-with-visual-studio-and-asp-net-core/

EDIT: actually it was this one https://github.com/MarkPieszak/aspnetcore-angular2-universal but the branch got updated at the time i made this post which got me confused


I have this service here,

svg-pan-zoom.service.ts

import { Injectable } from '@angular/core'
import { isBrowser } from 'angular2-universal';
import * as svgPanZoom from 'svg-pan-zoom';

@Injectable()
export class SvgPanZoomService {

    getPanZoom(element: any) {
        if (isBrowser) {
            svgPanZoom(element);
        }
    }
}

which is called here map.component.ts

import { Component, AfterViewInit } from '@angular/core';
import { SvgPanZoomService } from '../../injectables/svg-pan-zoom.service';
import { isBrowser } from 'angular2-universal';

@Component({
    selector: 'map-full',
    template: require('./map.component.html'),
    styles: [require('./map.component.css')]
})
export class MapComponent implements AfterViewInit {

    constructor(private svgZoom: SvgPanZoomService) {
        if (isBrowser) {
            this.svgZoom.getPanZoom('#evSvgMap');
        }
    }
}


The SvgPanZoomService service is referenced in my app.module

the Svg-pan-zoom lib is referenced in my package.json,

In the end my build gives me 2 js files, main-client.js and vendor.js

I can browse main-client.js and see it has references to svg-pan-zoom,
it is present in my browser sources when my page loads

But when it comes to loading the part where it should be doing stuff, i get this error.

An unhandled exception occurred while processing the request.

Exception: Call to Node module failed with error: 
Prerendering failed because of error: ReferenceError: window is not defined
at D:\[mystuff]\node_modules\svg-pan-zoom\dist\svg-pan-zoom.js:1493:8

Now i read i'm not meant to access window from a component, but i have no say in what the lib calls, i read somewhere here adding (isBrowser) should verify im not calling this server-side (why would i want the server to zoom this is ui right?)

here is my webpack.config.js

var isDevBuild = process.argv.indexOf('--env.prod') < 0;
var path = require('path');
var webpack = require('webpack');
var nodeExternals = require('webpack-node-externals');
var merge = require('webpack-merge');
var allFilenamesExceptJavaScript = /\.(?!js(\?|$))([^.]+(\?|$))/;

// Configuration in common to both client-side and server-side bundles
var sharedConfig = {
    resolve: { extensions: [ '', '.js', '.ts' ] },
    output: {
        filename: '[name].js',
        publicPath: '/dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
    },
    module: {
        loaders: [
            { test: /\.ts$/, include: /ClientApp/, loader: 'ts', query: { silent: true } },
            { test: /\.html$/, loader: 'raw' },
            { test: /\.css$/, loader: 'to-string!css' },
            { test: /\.(png|jpg|jpeg|gif|svg)$/, loader: 'url', query: { limit: 25000 } }
        ]
    }
};

// Configuration for client-side bundle suitable for running in browsers
var clientBundleConfig = merge(sharedConfig, {
    entry: {
        'main-client': './ClientApp/boot-client.ts'
    },
    output: { path: path.join(__dirname, './wwwroot/dist') },
    devtool: isDevBuild ? 'inline-source-map' : null,
    plugins: [
        new webpack.DllReferencePlugin({
            context: __dirname,
            manifest: require('./wwwroot/dist/vendor-manifest.json')
        })
    ].concat(isDevBuild ? [] : [
        // Plugins that apply in production builds only
        new webpack.optimize.OccurenceOrderPlugin(),
        new webpack.optimize.UglifyJsPlugin()
    ])
});

// Configuration for server-side (prerendering) bundle suitable for running in Node
var serverBundleConfig = merge(sharedConfig, {
    entry: { 'main-server': './ClientApp/boot-server.ts' },
    output: {
        libraryTarget: 'commonjs',
        path: path.join(__dirname, './ClientApp/dist')
    },
    target: 'node',
    devtool: 'inline-source-map',
    externals: [nodeExternals({ whitelist: [allFilenamesExceptJavaScript] })] // Don't bundle .js files from node_modules
});

module.exports = [clientBundleConfig, serverBundleConfig];

the app then loads another webpack.config.vendor.js

var isDevBuild = process.argv.indexOf('--env.prod') < 0;
var path = require('path');
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var extractCSS = new ExtractTextPlugin('vendor.css');

module.exports = {
    resolve: {
        extensions: [ '', '.js' ]
    },
    module: {
        loaders: [
            { test: /\.(png|woff|woff2|eot|ttf|svg)(\?|$)/, loader: 'url-loader?limit=100000' },
            { test: /\.css(\?|$)/, loader: extractCSS.extract(['css']) }
            { test: require("svg-pan-zoom"),
                loader: "imports-loader?window=>window./svg-pan-zoom.js"} // wild try
        ]
    },
    entry: {
        vendor: [   
            '@angular/common',
            '@angular/compiler',
            '@angular/core',
            '@angular/http',
            '@angular/platform-browser',
            '@angular/platform-browser-dynamic',
            '@angular/router',
            '@angular/platform-server',
            'angular2-universal',
            'angular2-universal-polyfills',
            'bootstrap',
            'bootstrap/dist/css/bootstrap.css',
            'es6-shim',
            'es6-promise',
            'jquery',
            'zone.js',
            'svg-pan-zoom' //i added this, no clue if it's relevant.
                           //EDIT :Turns out it was important, very much so.
        ]
    },
    output: {
        path: path.join(__dirname, 'wwwroot', 'dist'),
        filename: '[name].js',
        library: '[name]_[hash]',
    },
    plugins: [
        extractCSS,
        new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery'}), // Maps these identifiers to the jQuery package (because Bootstrap expects it to be a global variable)
        new webpack.optimize.OccurenceOrderPlugin(),
        new webpack.DllPlugin({
            path: path.join(__dirname, 'wwwroot', 'dist', '[name]-manifest.json'),
            name: '[name]_[hash]'
        })
    ].concat(isDevBuild ? [] : [
        new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } })
    ])
};

i also have 2 files boot-client and boot-server related to the webpack.config.js which are instructions for the packaging of the server and client files.

thanks.


Solution

  • This works

    https://github.com/MarkPieszak/aspnetcore-angular2-universal#universal-gotchas

    When building Universal components in Angular 2 there are a few things to keep in mind. window, document, navigator, and other browser types - do not exist on the server - so using them, or any library that uses them (jQuery for example) will not work. You do have some options, if you truly need some of this functionality:

    If you need to use them, consider limiting them to only your client and wrapping them situationally. You can use the Object injected using the PLATFORM_ID token to check whether the current platform is browser or server.

    import { PLATFORM_ID } from '@angular/core';
     import { isPlatformBrowser, isPlatformServer } from '@angular/common';
    
     constructor(@Inject(PLATFORM_ID) private platformId: Object) { ... }
    
     ngOnInit() {
       if (isPlatformBrowser(this.platformId)) {
          // Client only code.
          ...
       }
       if (isPlatformServer(this.platformId)) {
         // Server only code.
         ...
       }
     }
    

    However keep in mind that if you're using webpack you need to separate your client and server side code by making two separate bundles, the server side bundle must not contain references to the client side javascript libraries you are using, in this case svg-pan-zoom calls window which does not exist server side.

    this separation can be achieved by adding a section such as below inside your webpack.config

        module: {
            rules:
            [{test: /svg-pan-zoom/,loader: 'null-loader'}]
        }
    

    this null-loader requires a npm install null-loader --save
    more info : https://github.com/webpack-contrib/null-loader

    Once you've separated the bundles, make sure to make every call to your scripts that may require window, document, navigator, i encountered issues with localStorage too, inside the if (isPlatformBrowser(this.platformId)) { //your code } blocks


    EDIT now it's all good: Now i got it working I'll still keep this under here as it kept my spirits up when i could not figure out how to make anything work. webpack is very different from all the things i had seen before and i just was too lazy to read the docs but they do exist and in the end it is a very powerful tool that can get things done. The difference is that if you do not use server side rendering, features such as adding meta tags and descriptions will not be executed at server time. I believe the angular2 app is considered as client javascript by a google crawler for instance and thus will not be loaded making your SEO efforts worthless.


    Another suggestion was to get rid of the server side rendering feature, which i did, and it worked for this and any other client related issue that arised later on. https://github.com/MarkPieszak/aspnetcore-angular2-universal#faq---also-check-out-the-faq-issues-label

    How can I disable Universal / SSR (Server-side rendering)?

    Simply comment out the logic within HomeController, and replace @Html.Raw(ViewData["SpaHtml"]) with just your applications root AppComponent tag ("app" in our case): .

    You could also remove any isPlatformBrowser/etc logic, and delete the boot-server, browser-app.module & server-app.module files, just make sure your boot-client file points to app.module.