Reputation: 49804
I am using universal-starter as backbone.
When my client starts, it read a token about user info from localStorage.
@Injectable()
export class UserService {
foo() {}
bar() {}
loadCurrentUser() {
const token = localStorage.getItem('token');
// do other things
};
}
Everything works well, however I got this in the server side (terminal) because of server rendering:
EXCEPTION: ReferenceError: localStorage is not defined
I got the idea from ng-conf-2016-universal-patterns that using Dependency Injection to solve this. But that demo is really old.
Say I have these two files now:
main.broswer.ts
export function ngApp() {
return bootstrap(App, [
// ...
UserService
]);
}
main.node.ts
export function ngApp(req, res) {
const config: ExpressEngineConfig = {
// ...
providers: [
// ...
UserService
]
};
res.render('index', config);
}
Right now they use both same UserService. Can someone give some codes to explain how to use different Dependency Injection to solve this?
If there is another better way rather than Dependency Injection, that will be cool too.
UPDATE 1 I am using Angular 2 RC4, I tried @Martin's way. But even I import it, it still gives me error in the terminal below:
Terminal (npm start)
/my-project/node_modules/@angular/core/src/di/reflective_provider.js:240 throw new reflective_exceptions_1.NoAnnotationError(typeOrFunc, params); ^ Error: Cannot resolve all parameters for 'UserService'(Http, ?). Make sure that all the parameters are decorated with Inject or have valid type annotations and that 'UserService' is decorated with Injectable.
Terminal (npm run watch)
error TS2304: Cannot find name 'LocalStorage'.
I guess it is somehow duplicated with the LocalStorage
from angular2-universal
(although I am not using import { LocalStorage } from 'angular2-universal';
), but even I tried to change mine to LocalStorage2
, still not work.
And in the meanwhile, my IDE WebStorm also shows red:
BTW, I found a import { LocalStorage } from 'angular2-universal';
, but not sure how to use that.
UPDATE 2, I changed to (not sure whether there is a better way):
import { Injectable, Inject } from '@angular/core';
import { Http } from '@angular/http';
import { LocalStorage } from '../../local-storage';
@Injectable()
export class UserService {
constructor (
private _http: Http,
@Inject(LocalStorage) private localStorage) {} // <- this line is new
loadCurrentUser() {
const token = this.localStorage.getItem('token'); // here I change from `localStorage` to `this.localStorage`
// …
};
}
This solves the issue in UPADAT 1, but now I got error in the terminal:
EXCEPTION: TypeError: this.localStorage.getItem is not a function
Upvotes: 48
Views: 77259
Reputation: 439
In my case what helped was simply checking if the document is defined before checking the localStorage.
if (typeof document !== 'undefined') {
localStorage.getItem("item")
}
Upvotes: 0
Reputation: 3745
Angular Universal should understand whether the function is for Browser or Server
Use the simple option like
Method 1
import { PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
constructor(@Inject(PLATFORM_ID) private platformId: Object) {
// constructor code
}
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
// Client only code. Local storage code
}
if (isPlatformServer(this.platformId)) {
// Server only code. Local storage code
}
}
Method 2
import { PLATFORM_ID} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Component({
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NavigationComponent implements OnInit {
private isBrowser: boolean = false;
constructor(
@Inject(PLATFORM_ID) private platformId: Object
) {
this.isBrowser = isPlatformBrowser(platformId);
}
ngOnInit(): void {
if (this.isBrowser) {
var retrievedLocale = localStorage?.getItem('preferredLocale');
}
}
changeLocale(category: any): void {
if (this.isBrowser) {
window.localStorage.setItem('preferredLocale', locale.code);
}
}
}
Upvotes: 2
Reputation: 91
You can use PLATFORM_ID token to check whether the current platform is browser or server.
import { Component, OnInit, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Component({
selector: "app-root",
templateUrl: "./app.component.html"
})
export class AppComponent implements OnInit {
constructor(@Inject(PLATFORM_ID) private platformId: Object) { }
ngOnInit() {
// Client only code.
if (isPlatformBrowser(this.platformId)) {
let item = {key1: 'value1', key2: 'valu2' };
localStorage.setItem( itemIndex, JSON.stringify(item) );
}
}
}
Upvotes: 7
Reputation: 11
Solved "getItem undefined" issue by below lines :
if(window.localStorage){
return window.localStorage.getItem('user');
}
Upvotes: 1
Reputation: 616
These steps resolved my issue:
Step 1: Run this command:
npm i localstorage-polyfill --save
Step 2: Add these two lines in server.ts file:
import 'localstorage-polyfill'
global['localStorage'] = localStorage;
Once you are done, run build command (eg: npm run build:serverless
)
All set now. Start the server again and you can see the issue is resolved.
Note: Use localStorage not window.localStorage, eg: localStorage.setItem(keyname, value)
Upvotes: 57
Reputation: 9517
As wierd as this approach might seem, it is working, and I had to do none of the plumbing the other answers are suggesting.
Step 1
Install localstorage-polyfill
: https://github.com/capaj/localstorage-polyfill
Step 2
Assuming you followed this step: https://github.com/angular/angular-cli/wiki/stories-universal-rendering, you should have a file called, server.js
in your project root folder.
In this server.js
add this:
import 'localstorage-polyfill'
global['localStorage'] = localStorage;
Step 3
Rebuild your project, npm run build:ssr
, and all should work fine now.
Does the above approach work? Yes, as far as I can tell.
Is it the best? Maybe not
Any performance issues? Not that I know of. Enlighten me.
However, as it stands now, this is the dumbest, most cleanest approach to getting my localStorage
to pass
Upvotes: 6
Reputation: 200
LocalStorage not implemented by the server side. We need to choose to write depend on platform code or use a cookie for save and read data.
ngx-cookie
is best solution(that i know) for work with cookie on server and browser.
You can write extend Storage class with cookie work:
universal.storage.ts
import { Injectable } from '@angular/core';
import { CookieService } from 'ngx-cookie';
@Injectable()
export class UniversalStorage implements Storage {
[index: number]: string;
[key: string]: any;
length: number;
cookies: any;
constructor(private cookieService: CookieService) {}
public clear(): void {
this.cookieService.removeAll();
}
public getItem(key: string): string {
return this.cookieService.get(key);
}
public key(index: number): string {
return this.cookieService.getAll().propertyIsEnumerable[index];
}
public removeItem(key: string): void {
this.cookieService.remove(key);
}
public setItem(key: string, data: string): void {
this.cookieService.put(key, data);
}
}
Upvotes: 1
Reputation: 9564
In Angular 4 (and 5) you can easily deal with such issue with a simple function in the following way:
app.module.ts
@NgModule({
providers: [
{ provide: 'LOCALSTORAGE', useFactory: getLocalStorage }
]
})
export class AppModule {
}
export function getLocalStorage() {
return (typeof window !== "undefined") ? window.localStorage : null;
}
If you have a server/client split file AppModule, place it in the app.module.shared.ts
file - the function won't break your code - unless you need to enforce completely different behaviours for the server and client builds; if that’s the case, it could be wiser to implement a custom class factory instead, just like it has been shown in other answers.
Anyway, once you're done with the provider implementation, you can inject the LOCALSTORAGE
generic in any Angular component and check for the platform type with the Angular-native isPlatformBrowser
function before using it:
import { PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
@Injectable()
export class SomeComponent {
constructor(
@Inject(PLATFORM_ID) private platformId: any,
@Inject('LOCALSTORAGE') private localStorage: any) {
// do something
}
NgOnInit() {
if (isPlatformBrowser(this.platformId)) {
// localStorage will be available: we can use it.
}
if (isPlatformServer(this.platformId)) {
// localStorage will be null.
}
}
}
It’s worth noting that, since the getLocalStorage()
function will return null
if the window object isn’t available, you could just check for this.localStorage
nullability and entirely skip the platform type check. However, I strongly recommend the above approach as that function implementation (and return value) might be subject to change in the future; conversely, the isPlatformBrowser
/ isPlatformServer
return values are something that can be trusted by design.
For more info, check out this blog post that I wrote on the topic.
Upvotes: 9
Reputation: 16302
Update for newer versions of Angular
OpaqueToken
was superseded by InjectionToken
which works much in the same way -- except it has a generic interface InjectionToken<T>
which makes for better type checking and inference.
Orginal Answer
Two things:
What you need to do is inject an adapter for localStorage that will work for both the browser and NodeJS. This will also give you testable code.
in local-storage.ts:
import { OpaqueToken } from '@angular/core';
export const LocalStorage = new OpaqueToken('localStorage');
In your main.browser.ts we will inject the actual localStorage object from your browser:
import {LocalStorage} from './local-storage.ts';
export function ngApp() {
return bootstrap(App, [
// ...
UserService,
{ provide: LocalStorage, useValue: window.localStorage}
]);
And then in main.node.ts we will use an empty object:
...
providers: [
// ...
UserService,
{provide: LocalStorage, useValue: {getItem() {} }}
]
...
Then your service injects this:
import { LocalStorage } from '../local-storage';
export class UserService {
constructor(@Inject(LocalStorage) private localStorage: LocalStorage) {}
loadCurrentUser() {
const token = this.localStorage.getItem('token');
...
};
}
Upvotes: 32
Reputation: 73357
This is how we have it in our project, as per from this github comment:
import { OnInit, PLATFORM_ID, Inject } from '@angular/core';
import { isPlatformServer, isPlatformBrowser } from '@angular/common';
export class SomeClass implements OnInit {
constructor(@Inject(PLATFORM_ID) private platformId: Object) { }
ngOnInit() {
if (isPlatformServer(this.platformId)) {
// do server side stuff
}
if (isPlatformBrowser(this.platformId)) {
localStorage.setItem('myCats', 'Lissie & Lucky')
}
}
}
Upvotes: 3
Reputation: 15634
I am having a similar issue with Angular 4 + Universal following steps here to configure a SPA that can render at client side or server side.
I am using oidc-client because I need my SPA to act as an OpenId Connect/Oauth2 client for my Identity Server.
The thing is that I was having the typical problem where localStorage or sessionStorage are not defined in server side (they only exist when there's a window object, therefore it wouldn't make sense for nodeJs to have these objects).
I have unsuccessfully tried the approach to mock the localStorage or sessionStorage and use the real one when in browser and an empty one in server.
But I came to the conclusion that for my needs I don't really need localStorage or sessionStorage to do anything in server side. If executed in NodeJs, simply skip the part where sessionStorage or localStorage is used, and the execution will then happen at client-side.
This would suffice:
console.log('Window is: ' + typeof window);
this.userManager = typeof window !== 'undefined'? new oidc.UserManager(config) : null; //just don't do anything unless there is a window object
In client-side rendering it prints:
Window is: object
In nodeJs it prints:
Window is: undefined
The beauty of this is that Angular Universal will simply ignore the execution/rendering at server side when there is no window object, BUT that execution will be working fine when Angular Universal sends the page with javascript to the browser, therefore even if I am running my app in NodeJs eventually my browser prints the following:
Window is: object
I know this is not a proper answer for those who really need to access localStorage or sessionStorage in server side, but for most of the cases we use Angular Universal simply to render whatever is possible to render in server side, and for sending the things that can't be rendered to the browser to work normally.
Upvotes: 5
Reputation: 1
I have also ran in to the same issue. You can write inside 'isBrowser' check by importing below statement.
import { isBrowser } from 'angular2-universal';
Upvotes: -2
Reputation: 39
I don't think this is a good solution, but I was stucked with the same problem using aspnetcore-spa generator and solved it this way:
@Injectable()
export class UserService {
foo() {}
bar() {}
loadCurrentUser() {
if (typeof window !== 'undefined') {
const token = localStorage.getItem('token');
}
// do other things
};
}
This condition prevents client code from running on the server-side where 'window' object doesn't exist.
Upvotes: 3
Reputation: 49804
Thanks for @Martin's great help. But there are several places below need to be updated to get it work:
constructor
in user.service.tsuseValue
in main.node.ts, main.browser.tsThis is how my codes look like now.
I would love to accept @Martin's answer when he updated.
BTW, I found a
import { LocalStorage } from 'angular2-universal';
, but not sure how to use that.
user.service.ts
import { Injectable, Inject } from '@angular/core';
import { LocalStorage } from '../local-storage';
@Injectable()
export class UserService {
constructor (
@Inject(LocalStorage) private localStorage) {}
loadCurrentUser() {
const token = localStorage.getItem('token');
// do other things
};
}
local-storage.ts
import { OpaqueToken } from '@angular/core';
export const LocalStorage = new OpaqueToken('localStorage');
main.broswer.ts
import { LocalStorage } from './local-storage';
export function ngApp() {
return bootstrap(App, [
// ...
{ provide: LocalStorage, useValue: window.localStorage},
UserService
]);
}
main.node.ts
import { LocalStorage } from './local-storage';
export function ngApp(req, res) {
const config: ExpressEngineConfig = {
// ...
providers: [
// ...
{ provide: LocalStorage, useValue: { getItem() {} }},
UserService
]
};
res.render('index', config);
}
Upvotes: 2
Reputation: 22352
I have no sufficient knowledge of preparing angular apps to run serverside. But in the similar scenario for react & nodejs, what needs to be done is to let server know what localStorage
is. For example:
//Stub for localStorage
(global as any).localStorage = {
getItem: function (key) {
return this[key];
},
setItem: function (key, value) {
this[key] = value;
}
};
Hope this can be of any help to you.
Upvotes: 1