diff --git a/.angular-cli.json b/.angular-cli.json index 4660e8f421..f2fb33440f 100644 --- a/.angular-cli.json +++ b/.angular-cli.json @@ -6,7 +6,7 @@ "apps": [ { "root": "demo/src", - "outDir": "demo/dist", + "outDir": "demo/dist/browser", "assets": [ "assets", "favicon.ico" @@ -29,7 +29,37 @@ "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", - "prod": "environments/environment.prod.ts" + "prod": "environments/environment.prod.ts", + "server": "environments/environment.server.ts" + } + }, + { + "platform" : "server", + "root": "demo/src", + "outDir": "demo/dist/server", + "assets": [ + "assets", + "favicon.ico" + ], + "index": "index.html", + "main": "main.server.ts", + "test": "../../scripts/test.ts", + "tsconfig": "tsconfig.server.json", + "testTsconfig": "../../src/tsconfig.spec.json", + "prefix": "", + "mobile": false, + "serviceWorker": false, + "styles": [ + "../../src/datepicker/bs-datepicker.scss", + "assets/css/style.scss", + "assets/css/prettify-angulario.css" + ], + "scripts": [], + "environmentSource": "environments/environment.ts", + "environments": { + "dev": "environments/environment.ts", + "prod": "environments/environment.prod.ts", + "server": "environments/environment.server.ts" } } ], diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..b8b71f6e7d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM node + +RUN apt-get update + +RUN mkdir /home/ngx-bootstrap + +WORKDIR /home/ngx-bootstrap + +COPY ./ ./ + +RUN npm i + +RUN npm run build + +RUN npm run link + +RUN npm run build:dynamic + +EXPOSE 3000 + +CMD ["node", "demo/dist/server.js"] + diff --git a/README.md b/README.md index 3e440f8a4d..ba14851ccb 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,9 @@ First time - `npm run build.watch` in first terminal - `ng serve` in second + If you want to run the demo with Angular Universal: + - `npm run demo.serve-universal` + # Usage & Demo diff --git a/demo/src/app/app.module.ts b/demo/src/app/app.module.ts index c491fe7376..d0ece1748d 100644 --- a/demo/src/app/app.module.ts +++ b/demo/src/app/app.module.ts @@ -17,6 +17,7 @@ import { GettingStartedComponent } from './common/getting-started/getting-starte import { ThemeStorage } from './theme/theme-storage'; import { StyleManager } from './theme/style-manager'; import { DocsModule } from './docs'; +import { environment } from '../environments/environment'; @NgModule({ declarations: [ @@ -27,12 +28,12 @@ import { DocsModule } from './docs'; ], imports: [ DocsModule, - BrowserModule, FormsModule, HttpModule, - RouterModule.forRoot(routes, { useHash: true }), + RouterModule.forRoot(routes, {useHash: environment.useHash}), Ng2PageScrollModule.forRoot(), - BsDropdownModule.forRoot() + BsDropdownModule.forRoot(), + BrowserModule.withServerTransition({appId: 'ngx-bootstrap'}) ], providers: [ ThemeStorage, diff --git a/demo/src/app/app.server.module.ts b/demo/src/app/app.server.module.ts new file mode 100644 index 0000000000..f17b89ea66 --- /dev/null +++ b/demo/src/app/app.server.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { AppComponent } from './app.component'; +import { AppModule } from './app.module'; +import { ServerModule } from '@angular/platform-server'; +import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader'; + +@NgModule({ + imports: [ + AppModule, + ServerModule, + ModuleMapLoaderModule + ], + bootstrap: [AppComponent] +}) +export class AppServerModule {} diff --git a/demo/src/app/common/landing/landing.component.html b/demo/src/app/common/landing/landing.component.html index 0ac0747de3..daa626373c 100644 --- a/demo/src/app/common/landing/landing.component.html +++ b/demo/src/app/common/landing/landing.component.html @@ -7,7 +7,7 @@
Bootstrap components, powered by Angular
{{currentVersion}}
diff --git a/demo/src/app/common/landing/landing.component.ts b/demo/src/app/common/landing/landing.component.ts index a650455cd3..ebf4b46b61 100644 --- a/demo/src/app/common/landing/landing.component.ts +++ b/demo/src/app/common/landing/landing.component.ts @@ -27,12 +27,14 @@ export class LandingComponent implements AfterViewInit { } ngAfterViewInit(): any { - this.http - .get('assets/json/current-version.json') - .map(res => res.json()) - .subscribe((data: any) => { - this.currentVersion = data.version; - }); + if (typeof window !== 'undefined') { + this.http + .get('assets/json/current-version.json') + .map(res => res.json()) + .subscribe((data: any) => { + this.currentVersion = data.version; + }); + } } installTheme(theme: 'bs3' | 'bs4') { diff --git a/demo/src/app/common/top-menu/top-menu.component.ts b/demo/src/app/common/top-menu/top-menu.component.ts index 9563b1d8d5..25f8948b5e 100644 --- a/demo/src/app/common/top-menu/top-menu.component.ts +++ b/demo/src/app/common/top-menu/top-menu.component.ts @@ -1,6 +1,6 @@ import { AfterViewInit, Component } from '@angular/core'; import { Http } from '@angular/http'; -import { NavigationEnd, Router, UrlSerializer } from '@angular/router'; +import { NavigationEnd, Router } from '@angular/router'; import 'rxjs/add/operator/map'; @Component({ @@ -22,8 +22,27 @@ export class TopMenuComponent implements AfterViewInit { ngAfterViewInit(): any { // todo: remove this sh** - this.isLocalhost = location.hostname === 'localhost'; - this.needPrefix = location.pathname !== '/'; + if (typeof window !== 'undefined') { + this.isLocalhost = location.hostname === 'localhost'; + this.needPrefix = location.pathname !== '/'; + this.appUrl = + location.protocol + + '//' + + location.hostname + + (this.isLocalhost ? ':' + location.port + '/' : '/'); + this.http + .get('assets/json/versions.json') + .map(res => res.json()) + .subscribe((data: any) => { + this.previousDocs = data; + }); + this.http + .get('assets/json/current-version.json') + .map(res => res.json()) + .subscribe((data: any) => { + this.currentVersion = data.version; + }); + } const getUrl = (router: Router) => { const indexOfHash = router.routerState.snapshot.url.indexOf('#'); @@ -32,29 +51,12 @@ export class TopMenuComponent implements AfterViewInit { let _prev = getUrl(this.router); this.router.events.subscribe((event: any) => { const _cur = getUrl(this.router); - this.appHash = location.hash === '#/' ? '' : location.hash; + if (typeof window !== 'undefined') { + this.appHash = location.hash === '#/' ? '' : location.hash; + } if (event instanceof NavigationEnd && _cur !== _prev) { _prev = _cur; } }); - - this.http - .get('assets/json/versions.json') - .map(res => res.json()) - .subscribe((data: any) => { - this.previousDocs = data; - }); - this.http - .get('assets/json/current-version.json') - .map(res => res.json()) - .subscribe((data: any) => { - this.currentVersion = data.version; - }); - - this.appUrl = - location.protocol + - '//' + - location.hostname + - (this.isLocalhost ? ':' + location.port + '/' : '/'); } } diff --git a/demo/src/app/docs/api-docs/analytics/analytics.ts b/demo/src/app/docs/api-docs/analytics/analytics.ts index f62f5a0d57..3daa8c39cf 100644 --- a/demo/src/app/docs/api-docs/analytics/analytics.ts +++ b/demo/src/app/docs/api-docs/analytics/analytics.ts @@ -24,7 +24,7 @@ export class Analytics { constructor(_location: Location, _router: Router) { this._location = _location; this._router = _router; - this._enabled = window.location.href.indexOf('bootstrap') >= 0; + this._enabled = (typeof window != 'undefined') && window.location.href.indexOf('bootstrap') >= 0; } /** diff --git a/demo/src/assets/json/current-version.json b/demo/src/assets/json/current-version.json index 2947579fdb..06ced1bb21 100644 --- a/demo/src/assets/json/current-version.json +++ b/demo/src/assets/json/current-version.json @@ -1 +1 @@ -{"version":"2.0.0-beta.6"} \ No newline at end of file +{"version":"2.0.0-beta.7"} \ No newline at end of file diff --git a/demo/src/environments/environment.prod.ts b/demo/src/environments/environment.prod.ts index 3612073bc3..d7cfe8bbf4 100644 --- a/demo/src/environments/environment.prod.ts +++ b/demo/src/environments/environment.prod.ts @@ -1,3 +1,4 @@ export const environment = { - production: true + production: true, + useHash: true }; diff --git a/demo/src/environments/environment.server.ts b/demo/src/environments/environment.server.ts new file mode 100644 index 0000000000..881207f33b --- /dev/null +++ b/demo/src/environments/environment.server.ts @@ -0,0 +1,4 @@ +export const environment = { + production: true, + useHash: false +}; diff --git a/demo/src/environments/environment.ts b/demo/src/environments/environment.ts index b7f639aeca..373f74d51b 100644 --- a/demo/src/environments/environment.ts +++ b/demo/src/environments/environment.ts @@ -4,5 +4,6 @@ // The list of which env maps to which file can be found in `.angular-cli.json`. export const environment = { - production: false + production: false, + useHash: true }; diff --git a/demo/src/index.html b/demo/src/index.html index 804743b4cb..27b535bdec 100644 --- a/demo/src/index.html +++ b/demo/src/index.html @@ -34,6 +34,7 @@ diff --git a/demo/src/main.server.ts b/demo/src/main.server.ts new file mode 100644 index 0000000000..d7c01cde7b --- /dev/null +++ b/demo/src/main.server.ts @@ -0,0 +1 @@ +export { AppServerModule } from './app/app.server.module'; diff --git a/demo/src/tsconfig.server.json b/demo/src/tsconfig.server.json new file mode 100644 index 0000000000..deef5ca3de --- /dev/null +++ b/demo/src/tsconfig.server.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "declaration": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "noEmitHelpers" :true, + "lib": ["es6", "dom"], + "types": [ + "jasmine", + "webpack" + ], + "mapRoot": "./", + "module": "commonjs", + "moduleResolution": "node", + "outDir": "../temp/out-tsc", + "sourceMap": true, + "target": "es5", + "typeRoots": [ + "../node_modules/@types" + ] + }, + "angularCompilerOptions": { + "entryModule": "app/app.server.module#AppServerModule" + } +} diff --git a/package.json b/package.json index 615855b731..28b2a9d34d 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "demo.deploy-gh-pages": "gh-pages -d gh-pages", "demo.build": "run-s build link demo.gen-docs demo.ng-build demo.set-version", "demo.serve": "run-s demo.build lite-server", + "demo.serve-universal": "run-s build link build:dynamic serve:dynamic", "demo.deploy": "run-s demo.build demo.deploy-gh-pages", "link": "ngm link -p src --here", "lint-pretty": "prettier --config .prettierrc --write -l \"{demo/src,src}/**/*.ts\"", @@ -37,7 +38,11 @@ "pree2e": "webdriver-manager update", "e2e": "protractor", "e2e-cross": "SAUCE=true npm run e2e", - "view-stats": "webpack-bundle-analyzer demo/dist/stats.json" + "view-stats": "webpack-bundle-analyzer demo/dist/stats.json", + "build:dynamic": "npm run build:client-and-server-bundles && npm run webpack:server", + "serve:dynamic": "node demo/dist/server.js", + "build:client-and-server-bundles": "ng build -bh / --prod --env=server && ng build -bh / --prod --env=server --app 1 --output-hashing=false", + "webpack:server": "webpack --config ./scripts/universal/webpack.server.config.js --progress --colors" }, "main": "bundles/ngx-bootstrap.umd.js", "module": "index.js", @@ -70,6 +75,7 @@ "rxjs": ">=5.4.3" }, "devDependencies": { + "@angular/animations": "4.3.6", "@angular/cli": "1.5.0", "@angular/common": "4.3.6", "@angular/compiler": "4.3.6", @@ -80,9 +86,12 @@ "@angular/language-service": "4.3.6", "@angular/platform-browser": "4.3.6", "@angular/platform-browser-dynamic": "4.3.6", + "@angular/platform-server": "4.3.6", "@angular/router": "4.3.6", "@angular/service-worker": "1.0.0-beta.16", "@angular/tsc-wrapped": "4.3.6", + "@nguniversal/express-engine": "1.0.0-beta.3", + "@nguniversal/module-map-ngfactory-loader": "1.0.0-beta.3", "@types/jasmine": "2.5.54", "@types/marked": "0.3.0", "@types/node": "8.0.28", @@ -91,6 +100,7 @@ "classlist-polyfill": "1.2.0", "codecov": "2.3.1", "codelyzer": "3.2.0", + "compression": "1.7.1", "conventional-changelog-cli": "1.3.4", "conventional-github-releaser": "1.1.12", "core-js": "2.5.1", @@ -126,6 +136,7 @@ "require-dir": "0.3.2", "rxjs": "5.4.3", "ts-helpers": "^1.1.1", + "ts-loader": "3.0.5", "tslint": "5.7.0", "tslint-config-valorsoft": "2.1.1", "typedoc": "0.8.0", diff --git a/scripts/archive.js b/scripts/archive.js index 05636f0a27..0a99c72ec2 100644 --- a/scripts/archive.js +++ b/scripts/archive.js @@ -7,7 +7,7 @@ const versionsFilePath = 'assets/json/versions.json'; const currentVersionFilePath = 'assets/json/current-version.json'; const demoSrcDir = 'demo/src'; -const demoDistDir = 'demo/dist'; +const demoDistDir = 'demo/dist/browser'; const hostname = 'ngx-bootstrap'; let prevVersion; diff --git a/scripts/universal/prerender.ts b/scripts/universal/prerender.ts new file mode 100644 index 0000000000..0c088a4013 --- /dev/null +++ b/scripts/universal/prerender.ts @@ -0,0 +1,45 @@ +// Load zone.js for the server. +import 'zone.js/dist/zone-node'; +import 'reflect-metadata'; +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; +import { join } from 'path'; + +import { enableProdMode } from '@angular/core'; +// Faster server renders w/ Prod mode (dev mode never needed) +enableProdMode(); + +// Express Engine +import { ngExpressEngine } from '@nguniversal/express-engine'; +// Import module map for lazy loading +import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader'; +import { renderModuleFactory } from '@angular/platform-server'; +import { ROUTES } from './static.paths'; + +// * NOTE :: leave this as require() since this file is built Dynamically from webpack +const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('../../demo/dist/server/main.bundle'); + +const BROWSER_FOLDER = join(process.cwd(), 'browser'); + +// Load the index.html file containing referances to your application bundle. +const index = readFileSync(join('browser', 'index.html'), 'utf8'); + +let previousRender = Promise.resolve(); + +// Iterate each route path +ROUTES.forEach(route => { + const fullPath = join(BROWSER_FOLDER, route); + + // Make sure the directory structure is there + if(!existsSync(fullPath)){ + mkdirSync(fullPath); + } + + // Writes rendered HTML to index.html, replacing the file if it already exists. + previousRender = previousRender.then(_ => renderModuleFactory(AppServerModuleNgFactory, { + document: index, + url: route, + extraProviders: [ + provideModuleMap(LAZY_MODULE_MAP) + ] + })).then(html => writeFileSync(join(fullPath, 'index.html'), html)); +}); diff --git a/scripts/universal/server.ts b/scripts/universal/server.ts new file mode 100644 index 0000000000..ae1b549df3 --- /dev/null +++ b/scripts/universal/server.ts @@ -0,0 +1,59 @@ +// These are important and needed before anything else +import 'zone.js/dist/zone-node'; +import 'reflect-metadata'; + +import { renderModuleFactory } from '@angular/platform-server'; +import { enableProdMode } from '@angular/core'; + +import * as express from 'express'; +import * as compression from 'compression'; +import { join } from 'path'; +import { readFileSync } from 'fs'; + +// Faster server renders w/ Prod mode (dev mode never needed) +enableProdMode(); + +// Express server +const app = express(); +app.use(compression()); + +const PORT = process.env.PORT || 3000; +const DIST_FOLDER = join(process.cwd(), 'demo/dist'); + +// Our index.html we'll use as our template +const template = readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString(); + +// * NOTE :: leave this as require() since this file is built Dynamically from webpack +const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('../../demo/dist/server/main.bundle'); + +const { provideModuleMap } = require('@nguniversal/module-map-ngfactory-loader'); + +app.engine('html', (_, options, callback) => { + renderModuleFactory(AppServerModuleNgFactory, { + // Our index.html + document: template, + url: options.req.url, + // DI so that we can get lazy-loading to work differently (since we need it to just instantly render it) + extraProviders: [ + provideModuleMap(LAZY_MODULE_MAP) + ] + }).then(html => { + callback(null, html); + }); +}); + +app.set('view engine', 'html'); +app.set('views', join(DIST_FOLDER, 'browser')); + +// Server static files from /browser +app.get('*.*', express.static(join(DIST_FOLDER, 'browser'))); + +// All regular routes use the Universal engine +app.get('*', (req, res) => { + res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req }); +}); + +// Start up the Node server +app.listen(PORT, () => { + console.log(`Node server listening on http://localhost:${PORT}`); +}); diff --git a/scripts/universal/static.paths.ts b/scripts/universal/static.paths.ts new file mode 100644 index 0000000000..7b08d79446 --- /dev/null +++ b/scripts/universal/static.paths.ts @@ -0,0 +1,21 @@ +export const ROUTES = [ + '/', + '/getting-started', + '/accordion', + '/alerts', + '/buttons', + '/carousel', + '/collapse', + '/datepicker', + '/dropdowns', + '/modals', + '/pagination', + '/popover', + '/progressbar', + '/rating', + '/sortable', + '/tabs', + '/timepicker', + '/tooltip', + '/typeahead' +]; diff --git a/scripts/universal/webpack.server.config.js b/scripts/universal/webpack.server.config.js new file mode 100644 index 0000000000..d819bbdcf9 --- /dev/null +++ b/scripts/universal/webpack.server.config.js @@ -0,0 +1,32 @@ +const path = require('path'); +const webpack = require('webpack'); + +module.exports = { + entry: { server: './scripts/universal/server.ts', prerender: './scripts/universal/prerender.ts' }, + resolve: { extensions: ['.ts', '.js'] }, + target: 'node', + // this makes sure we include node_modules and other 3rd party libraries + externals: [/(node_modules|main\..*\.js)/], + output: { + path: path.join(__dirname, '../../demo/dist'), + filename: '[name].js' + }, + module: { + rules: [ + { test: /\.ts$/, exclude: /\.ts$/, loader: 'ts-loader' }, + ] + }, + plugins: [ + // Temporary Fix for issue: https://github.com/angular/angular/issues/11580 + // for "WARNING Critical dependency: the request of a dependency is an expression" + new webpack.ContextReplacementPlugin( + /(.+)?angular(\\|\/)core(.+)?/, + path.join(__dirname, 'src'), // location of your src + {} // a map of your routes + ), + new webpack.ContextReplacementPlugin( + /(.+)?express(\\|\/)(.+)?/, + path.join(__dirname, 'src'), + ) + ] +} diff --git a/src/pagination/pager.component.ts b/src/pagination/pager.component.ts index d1569234d8..2aa8ca09dc 100644 --- a/src/pagination/pager.component.ts +++ b/src/pagination/pager.component.ts @@ -143,7 +143,9 @@ export class PagerComponent implements ControlValueAccessor, OnInit { } ngOnInit(): void { - this.classMap = this.elementRef.nativeElement.getAttribute('class') || ''; + if (typeof window !== 'undefined') { + this.classMap = this.elementRef.nativeElement.getAttribute('class') || ''; + } // watch for maxSize this.maxSize = typeof this.maxSize !== 'undefined' ? this.maxSize : this.config.maxSize; diff --git a/src/pagination/pagination.component.ts b/src/pagination/pagination.component.ts index dd82c9f35a..5f93288978 100644 --- a/src/pagination/pagination.component.ts +++ b/src/pagination/pagination.component.ts @@ -148,7 +148,9 @@ export class PaginationComponent implements ControlValueAccessor, OnInit { } ngOnInit(): void { - this.classMap = this.elementRef.nativeElement.getAttribute('class') || ''; + if (typeof window !== 'undefined') { + this.classMap = this.elementRef.nativeElement.getAttribute('class') || ''; + } // watch for maxSize this.maxSize = typeof this.maxSize !== 'undefined' ? this.maxSize : this.config.maxSize; diff --git a/src/popover/popover.directive.ts b/src/popover/popover.directive.ts index 07dfd8e1ae..34e8727fce 100644 --- a/src/popover/popover.directive.ts +++ b/src/popover/popover.directive.ts @@ -92,13 +92,15 @@ export class PopoverDirective implements OnInit, OnDestroy { this.onHidden = this._popover.onHidden; // fix: no focus on button on Mac OS #1795 - _elementRef.nativeElement.addEventListener('click', function () { - try { - _elementRef.nativeElement.focus(); - } catch (err) { - return; - } - }); + if (typeof window !== 'undefined') { + _elementRef.nativeElement.addEventListener('click', function () { + try { + _elementRef.nativeElement.focus(); + } catch (err) { + return; + } + }); + } } /** diff --git a/src/tabs/tabset.component.ts b/src/tabs/tabset.component.ts index ab5dff2a77..99758d4a92 100644 --- a/src/tabs/tabset.component.ts +++ b/src/tabs/tabset.component.ts @@ -1,4 +1,4 @@ -import { Component, HostBinding, Input, OnDestroy } from '@angular/core'; +import { Component, HostBinding, Input, OnDestroy, Renderer2 } from '@angular/core'; import { TabDirective } from './tab.directive'; import { TabsetConfig } from './tabset.config'; @@ -49,7 +49,7 @@ export class TabsetComponent implements OnDestroy { protected _justified: boolean; protected _type: string; - constructor(config: TabsetConfig) { + constructor(config: TabsetConfig, private renderer: Renderer2) { Object.assign(this, config); } @@ -80,7 +80,8 @@ export class TabsetComponent implements OnDestroy { } this.tabs.splice(index, 1); if (tab.elementRef.nativeElement.parentNode) { - tab.elementRef.nativeElement.parentNode.removeChild( + this.renderer.removeChild( + tab.elementRef.nativeElement.parentNode, tab.elementRef.nativeElement ); } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..2f97dc9ca2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "declaration": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "noEmitHelpers" :true, + "lib": ["es6", "dom"], + "types": [ + "jasmine", + "webpack" + ], + "mapRoot": "./", + "module": "es6", + "moduleResolution": "node", + "outDir": "../temp/out-tsc", + "sourceMap": true, + "target": "es5", + "typeRoots": [ + "../node_modules/@types" + ] + } +}