Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Universal: ReferenceError: document is not defined #567

Open
consigliory opened this issue Feb 15, 2017 · 15 comments
Open

Universal: ReferenceError: document is not defined #567

consigliory opened this issue Feb 15, 2017 · 15 comments

Comments

@consigliory
Copy link

Hi. angular universal does not have mock for document yet. How can I use it with server rendering enabled?

@jcao02
Copy link

jcao02 commented Nov 11, 2017

I think this issue can't be solved in this repo. It would be solved in dragula by checking something like 'undefined' === typeof document before using the object

@paullryan
Copy link

Pending merge of bevacqua/dragula#534

@eduardmarcinov
Copy link

Why isn't pull request merged? I need use dragula in angular-universal. Does anyone have a solution for this? Thank you

@DarthRainbows
Copy link

DarthRainbows commented Feb 27, 2018

Fixing the dragula source is only half of the problem, there's another dependency that references document: ng2-dragula -> dragula -> crossvent -> custom-event.

Edit: upgrading dragulas crossvent package to 1.5.5 would solve that.

Dragula is unnecessary on the server side, so I am looking into a way to import DragulaModule only in the browser builds. Will report back if I find a solution.

@DarthRainbows
Copy link

DarthRainbows commented Feb 28, 2018

I found a work-around that should help developers using Angular Universal. Create a set of stubbed out classes representing DragulaModule, DragulaDirective, and DragulaService. Declare DragulaDirective and provide DragulaService in DragulaModule.

// dragula.directive.ts
import { Directive, Input } from '@angular/core';

/**
 * this class stubs out ng2-dragula's DragulaDirective for loading server-side
 * you don't need any of this functionality on the server
 */
@Directive({
  /* tslint:disable */
  // disable linting here, because you need the selector to match ng2-dragula's DragulaDirective
  // which violates the selector prefix rules, if you have not disabled them
  selector: '[dragula]'
  /* tslint:enable */
})
export class DragulaDirective {
  @Input() dragula: string;
  @Input() dragulaModel: any;
  @Input() dragulaOptions: any;
}
// dragula.module.ts
import { NgModule } from '@angular/core';
import { DragulaDirective } from './dragula.directive';
import { DragulaService } from './dragula.service';

@NgModule({
  declarations: [ DragulaDirective ],
  exports: [ DragulaDirective ],
  providers: [ DragulaService ],
})
class DragulaModule { }

export { DragulaDirective, DragulaModule, DragulaService };
// dragula.service.ts
import { Injectable, EventEmitter } from '@angular/core';

/**
 * this class stubs out ng2-dragula's DragulaService for loading server-side
 * you don't need any of this functionality on the server
 */
@Injectable()
export class DragulaService {
  cancel: EventEmitter<any> = new EventEmitter();
  cloned: EventEmitter<any> = new EventEmitter();
  drag: EventEmitter<any> = new EventEmitter();
  dragend: EventEmitter<any> = new EventEmitter();
  drop: EventEmitter<any> = new EventEmitter();
  out: EventEmitter<any> = new EventEmitter();
  over: EventEmitter<any> = new EventEmitter();
  remove: EventEmitter<any> = new EventEmitter();
  shadow: EventEmitter<any> = new EventEmitter();
  dropModel: EventEmitter<any> = new EventEmitter();
  removeModel: EventEmitter<any> = new EventEmitter();

  add(name: string, drake: any): any {}
  find(name: string): any {}
  destroy(name: string): void {}
  setOptions(name: string, options: any): void {}
}

Modify your tsconfig.server.json (or whatever tsconfig.json file you use for your SSR build - it should be different from the one you use for browser builds). In the "compilerOptions" object, add a new property "paths":

"paths": {
    "ng2-dragula": "path/to/dragula/stubs/dragula.module"
}

When the SSR compiles, it will use this path to import the DragulaModule. The SSR app will use the stubbed out directive and service, instead of the real ng2-dragula. There's no functionality present - all the methods are no-ops - but it doesn't matter, since the SSR isn't going to be dragging and dropping anything anyway.

@jiggy1com
Copy link

jiggy1com commented May 10, 2018

@asgallant - Sweet! Thanks. I managed to create those files, and get the build to work.

Here's some issues and fixes I found (for my codebase), it might help others.

In my tsconfig.server.json file the path needed to be an array

"paths": {
  "ng2-dragula": [ "path/to/ng2-dragula-stub/dragula.module" ]
}

... but then I had to update webpack.server.config.js by adding the following:

// near the top
const NormalModuleReplacementPlugin = require('webpack/lib/NormalModuleReplacementPlugin');

// in the plugins
  plugins: [  
    
    // add this line  
    new NormalModuleReplacementPlugin(    
      /ng2-dragula/,    
      path.resolve('./src/path/to/ng2-dragula-stub/dragula.module.ts')
    ),
    ...
]

.. and actually it built fine w/o the change to tsconfig.server.json and everything worked fine! Whoo!

@cormacrelf
Copy link
Contributor

Note that in v2 you might have to mock a few more items on those classes. But this seems good enough for now, pending merge upstream.

@cormacrelf cormacrelf changed the title ReferenceError: document is not defined Universal: ReferenceError: document is not defined Jul 18, 2018
@sjsnider
Copy link

@asgallant @jiggy1com I mocked everything out like in the example (replaced setOptions with createGroup) and used NormalModuleReplacementPlugin and everything builds just fine, but then when I actually try to load up my app the server side spits out this error ERROR TypeError: Cannot read property 'ngInjectableDef' of undefined. Any idea what could be the problem there?

@sjsnider
Copy link

got it working, took some small updates to those 3 files and then just adding the path in tsconfig.server.json rather than using NormalModuleReplacementPlugin in my webpack.server.config

@koraysels
Copy link

koraysels commented Jan 11, 2019

I'm almost there I guess.. but I still get one last error

Function calls are not supported in decorators but 'DragulaModule' was called.

EDIT: for future reference:

I fixed the build. but it still uses the normal module when i try to start my server. So it fails..

// dragula.module.ts 
import {ModuleWithProviders, NgModule} from '@angular/core';
import {DragulaDirective} from './dragula.directive';
import {DragulaService} from './dragula.service';

@NgModule({
  declarations: [DragulaDirective],
  exports: [DragulaDirective],
  providers: [DragulaService],
})
class DragulaModule {
  static forRoot(): any {
    return {
      ngModule: DragulaModule
    }
  }
}

export {DragulaDirective, DragulaModule, DragulaService};

@koraysels
Copy link

koraysels commented Jan 16, 2019

I found another solutions requiring no stubbing at all but relies on domino

in your server.ts:

const domino = require('domino');

const window = domino.createWindow('<h1>Hello world</h1>', 'http://example.com');
const document = window.document;
global['window'] = window;
global['document'] = document;
global['DOMTokenList'] = window.DOMTokenList;
global['Node'] = window.Node;
global['Text'] = window.Text;
global['HTMLElement'] = window.HTMLElement;
global['navigator'] = window.navigator;

Object.defineProperty(window.document.body.style, 'transform', {
  value: () => {
    return {
      enumerable: true,
      configurable: true,
    };
  },
});
global['CSS'] = null;
// global['XMLHttpRequest'] = require('xmlhttprequest').XMLHttpRequest;
global['Prism'] = null;

const fakeStorage: Storage = {
  length: 0,
  clear: () => {
  },
  getItem: (_key: string) => null,
  key: (_index: number) => null,
  removeItem: (_key: string) => {
  },
  setItem: (_key: string, _data: string) => {
  }
};

(global as any)['localStorage'] = fakeStorage;

@qubiack
Copy link

qubiack commented Feb 26, 2019

@koraysels
do you have any idea, why your solutions doesn't work in my situation? I add domino and use your config, but it doesn't change anything. When I try to start a server, I got an error:

${PROJECT}/node_modules/crossvent/node_modules/custom-event/index.js:24
'function' === typeof document.createEvent ? function CustomEvent (type, params) {
                      ^

ReferenceError: document is not defined
    at Object.<anonymous> (${PROJECT}/node_modules/crossvent/node_modules/custom-event/index.js:24:23)
    at Module._compile (module.js:652:30)
    at Object.Module._extensions..js (module.js:663:10)
    at Module.load (module.js:565:32)
    at tryModuleLoad (module.js:505:12)
    at Function.Module._load (module.js:497:3)
    at Module.require (module.js:596:17)
    at require (internal/module.js:11:18)
    at Object.<anonymous> (${PROJECT}/node_modules/crossvent/src/crossvent.js:3:19)
    at Module._compile (module.js:652:30)

My main.server.ts file below:

// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';

const domino = require('domino');

const window = domino.createWindow('<h1>Hello world</h1>', 'http://example.com');
const document = window.document;
global['window'] = window;
global['document'] = document;
global['DOMTokenList'] = window.DOMTokenList;
global['Node'] = window.Node;
global['Text'] = window.Text;
global['HTMLElement'] = window.HTMLElement;
global['navigator'] = window.navigator;

Object.defineProperty(window.document.body.style, 'transform', {
    value: () => {
        return {
            enumerable: true,
            configurable: true,
        };
    },
});
global['CSS'] = null;
// global['XMLHttpRequest'] = require('xmlhttprequest').XMLHttpRequest;
global['Prism'] = null;

const fakeStorage: Storage = {
    length: 0,
    clear: () => {
    },
    getItem: (_key: string) => null,
    key: (_index: number) => null,
    removeItem: (_key: string) => {
    },
    setItem: (_key: string, _data: string) => {
    }
};

(global as any)['localStorage'] = fakeStorage;

import { createServer } from 'http';
import { join } from 'path';

import { enableProdMode } from '@angular/core';
import { MODULE_MAP } from '@nguniversal/module-map-ngfactory-loader';
import { NgSetupOptions } from '@nguniversal/express-engine';

import { createApi } from './api';

export { AppServerModule } from './app/app.server.module';

export const PORT = process.env.PORT || 4000;
export const BROWSER_DIST_PATH = join(__dirname, '..', 'browser');

export const getNgRenderMiddlewareOptions: () => NgSetupOptions = () => ({
  bootstrap: exports.AppServerModuleNgFactory,
  providers: [
    // Import module map for lazy loading
    {
      provide: MODULE_MAP,
      useFactory: () => exports.LAZY_MODULE_MAP,
      deps: [],
    }
  ]
});

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

let requestListener = createApi(BROWSER_DIST_PATH, getNgRenderMiddlewareOptions());

// Start up the Node server
const server = createServer((req, res) => {
  requestListener(req, res);
});

server.listen(PORT, () => {
  console.log(`Server listening -- http://localhost:${PORT}`);
});

// HMR on server side
if (module.hot) {
  const hmr = () => {
    const { AppServerModuleNgFactory } = require('./app/app.server.module.ngfactory');

    exports.AppServerModuleNgFactory = AppServerModuleNgFactory;

    requestListener = require('./api').createApi(BROWSER_DIST_PATH, getNgRenderMiddlewareOptions());
  };

  module.hot.accept('./api', hmr);
  module.hot.accept('./app/app.server.module.ngfactory', hmr);
}

export default server;

@koraysels
Copy link

koraysels commented Feb 26, 2019

the code from your main.server.ts file should just be :

export { AppServerModule } from './app/app.server.module';

the other stuff belongs in the server.ts file in the root of your project.
I don't know about the hot reloading stuff on the server because I don't use that.

@qubiack
Copy link

qubiack commented Feb 26, 2019

In our porject main.server.ts is exactly the same as server.ts in other project and it's the first file used by angualar.json in build process:

"server": {
          "builder": "@angular-devkit/build-angular:server",
          "options": {
            "outputPath": "dist/app/server",
            "main": "src/main.server.ts",

But this doesn't change anything ;/

I use Angular 7.1.0

@neverlose-lv
Copy link

#567 (comment)

How can this be achieved in the latest version of Angular (17.2)? It does not have separate tsconfig for the SSR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests