├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── Views ├── _ViewStart.cshtml ├── _ViewImports.cshtml ├── Shared │ ├── Error.cshtml │ └── _Layout.cshtml └── Home │ └── Index.cshtml ├── ClientApp ├── typings.d.ts ├── app │ ├── models │ │ └── User.ts │ ├── components │ │ ├── user-detail │ │ │ ├── user-detail.component.scss │ │ │ ├── user-detail.component.html │ │ │ └── user-detail.component.ts │ │ └── navmenu │ │ │ ├── navmenu.component.ts │ │ │ ├── navmenu.component.html │ │ │ └── navmenu.component.scss │ ├── _variables.scss │ ├── app.component.html │ ├── containers │ │ ├── counter │ │ │ ├── counter.component.html │ │ │ ├── counter.component.ts │ │ │ └── counter.component.spec.ts │ │ ├── not-found │ │ │ ├── not-found.component.ts │ │ │ └── not-found.component.html │ │ ├── lazy │ │ │ ├── lazy.component.ts │ │ │ └── lazy.module.ts │ │ ├── home │ │ │ ├── home.component.ts │ │ │ └── home.component.html │ │ ├── ngx-bootstrap-demo │ │ │ ├── ngx-bootstrap.component.ts │ │ │ └── ngx-bootstrap.component.html │ │ └── users │ │ │ ├── users.component.html │ │ │ ├── users.component.scss │ │ │ └── users.component.ts │ ├── app.module.server.ts │ ├── _styles.scss │ ├── shared │ │ ├── user.service.ts │ │ └── link.service.ts │ ├── app.component.scss │ ├── app.module.browser.ts │ ├── app.component.ts │ └── app.module.ts ├── polyfills │ ├── server.polyfills.ts │ ├── browser.polyfills.ts │ └── polyfills.ts ├── tsconfig.app.json ├── tsconfig.spec.json ├── boot.browser.ts ├── test │ ├── boot-tests.js │ ├── karma.conf.js │ └── webpack.config.test.js ├── boot.server.ts └── boot.server.PRODUCTION.ts ├── wwwroot ├── favicon.ico ├── images │ ├── icon144x144.png │ ├── icon192x192.png │ ├── icon256x256.png │ ├── icon36x36.png │ ├── icon384x384.png │ ├── icon48x48.png │ ├── icon512x512.png │ ├── icon72x72.png │ └── icon96x96.png ├── assets │ └── i18n │ │ ├── en.json │ │ └── no.json └── manifest.json ├── docs ├── angular2-seo.png └── architecture.png ├── .env ├── docker-compose.yml ├── .dockerignore ├── appsettings.json ├── docker-compose.override.yml ├── Server ├── Data │ ├── LoggingEFStartup.cs │ ├── SpaDbContext.cs │ ├── CoreEFStartup.cs │ └── SimpleContentEFStartup.cs ├── Models │ ├── IRequest.cs │ ├── TransferData.cs │ └── User.cs ├── Helpers │ └── HttpRequestExtensions.cs ├── Controllers │ └── HomeController.cs └── RestAPI │ └── UsersController.cs ├── tsconfig.json ├── web.config ├── webpack.additions.js ├── docker-compose.dcproj ├── Dockerfile ├── LICENSE ├── Program.cs ├── Asp2017.sln ├── Asp2017.csproj ├── angular.json ├── package.json ├── tslint.json ├── .gitignore ├── Startup.cs ├── webpack.config.vendor.js ├── webpack.config.js ├── .editorconfig └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vsicons.presets.angular": true 3 | } -------------------------------------------------------------------------------- /Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /ClientApp/typings.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * 4 | * Application typings we need 5 | * 6 | */ 7 | -------------------------------------------------------------------------------- /ClientApp/app/models/User.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | id: number; 3 | name: string; 4 | } 5 | -------------------------------------------------------------------------------- /wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrilonIO/aspnetcore-angular-universal/HEAD/wwwroot/favicon.ico -------------------------------------------------------------------------------- /docs/angular2-seo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrilonIO/aspnetcore-angular-universal/HEAD/docs/angular2-seo.png -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrilonIO/aspnetcore-angular-universal/HEAD/docs/architecture.png -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | #Use this variable to set your Docker Repository to make it easier to push later. 2 | #DOCKER_REGISTRY=your repo name -------------------------------------------------------------------------------- /wwwroot/images/icon144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrilonIO/aspnetcore-angular-universal/HEAD/wwwroot/images/icon144x144.png -------------------------------------------------------------------------------- /wwwroot/images/icon192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrilonIO/aspnetcore-angular-universal/HEAD/wwwroot/images/icon192x192.png -------------------------------------------------------------------------------- /wwwroot/images/icon256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrilonIO/aspnetcore-angular-universal/HEAD/wwwroot/images/icon256x256.png -------------------------------------------------------------------------------- /wwwroot/images/icon36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrilonIO/aspnetcore-angular-universal/HEAD/wwwroot/images/icon36x36.png -------------------------------------------------------------------------------- /wwwroot/images/icon384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrilonIO/aspnetcore-angular-universal/HEAD/wwwroot/images/icon384x384.png -------------------------------------------------------------------------------- /wwwroot/images/icon48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrilonIO/aspnetcore-angular-universal/HEAD/wwwroot/images/icon48x48.png -------------------------------------------------------------------------------- /wwwroot/images/icon512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrilonIO/aspnetcore-angular-universal/HEAD/wwwroot/images/icon512x512.png -------------------------------------------------------------------------------- /wwwroot/images/icon72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrilonIO/aspnetcore-angular-universal/HEAD/wwwroot/images/icon72x72.png -------------------------------------------------------------------------------- /wwwroot/images/icon96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrilonIO/aspnetcore-angular-universal/HEAD/wwwroot/images/icon96x96.png -------------------------------------------------------------------------------- /ClientApp/app/components/user-detail/user-detail.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables'; 2 | :host-context() { 3 | flex: 1; 4 | } 5 | -------------------------------------------------------------------------------- /ClientApp/polyfills/server.polyfills.ts: -------------------------------------------------------------------------------- 1 | // Note: * The order is IMPORTANT! * 2 | import './polyfills.ts'; 3 | 4 | import 'zone.js'; 5 | -------------------------------------------------------------------------------- /Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using AspCoreServer 2 | @addTagHelper "*, Microsoft.AspNetCore.SpaServices" 3 | @addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | asp2017: 5 | image: ${DOCKER_REGISTRY}asp2017 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .env 3 | .git 4 | .gitignore 5 | .vs 6 | .vscode 7 | docker-compose.yml 8 | docker-compose.*.yml 9 | */bin 10 | */obj 11 | node_modules 12 | docs -------------------------------------------------------------------------------- /Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Error"; 3 | } 4 | 5 |

Error.

6 |

An error occurred while processing your request.

7 | -------------------------------------------------------------------------------- /ClientApp/app/_variables.scss: -------------------------------------------------------------------------------- 1 | @import 'styles'; 2 | $header-height: 50px; 3 | $menu-max-width: 25%; 4 | $navbar-default-bg: #312312; 5 | $light-orange: #ff8c00; 6 | $navbar-default-color: $light-orange; 7 | -------------------------------------------------------------------------------- /ClientApp/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | asp2017: 5 | environment: 6 | - ASPNETCORE_ENVIRONMENT=Development 7 | #- ASPNETCORE_ENVIRONMENT=Production 8 | ports: 9 | - "80" 10 | -------------------------------------------------------------------------------- /ClientApp/polyfills/browser.polyfills.ts: -------------------------------------------------------------------------------- 1 | // Note: * The order is IMPORTANT! * 2 | 3 | import './polyfills.ts'; 4 | 5 | import 'reflect-metadata'; 6 | import 'zone.js/dist/zone'; 7 | 8 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 9 | -------------------------------------------------------------------------------- /ClientApp/app/containers/counter/counter.component.html: -------------------------------------------------------------------------------- 1 |

Counter

2 | 3 |

This is a simple example of an Angular 2 component.

4 | 5 |

Current count: 6 | {{ currentCount }} 7 |

8 | 9 | 10 | -------------------------------------------------------------------------------- /wwwroot/assets/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "HOME": "Home", 3 | "HOME_FEATURE_LIST_TITLE": "What does this Starter offer?", 4 | "HOME_ISSUES_TITLE": "Having issues?", 5 | "COUNTER": "Counter", 6 | "SWITCH_LANGUAGE": "Switch language", 7 | "ENGLISH": "English", 8 | "NORWEGIAN": "Norwegian" 9 | } 10 | -------------------------------------------------------------------------------- /wwwroot/assets/i18n/no.json: -------------------------------------------------------------------------------- 1 | { 2 | "HOME": "Hjem", 3 | "HOME_FEATURE_LIST_TITLE": "Hva tilbyr dette prosjektet?", 4 | "HOME_ISSUES_TITLE": "Oppever du problemer?", 5 | "COUNTER": "Teller", 6 | "SWITCH_LANGUAGE": "Bytt språk", 7 | "ENGLISH": "Engelsk", 8 | "NORWEGIAN": "Norsk" 9 | } 10 | -------------------------------------------------------------------------------- /ClientApp/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "baseUrl": "", 7 | "sourceMap": true, 8 | "types": ["node"] 9 | }, 10 | "exclude": [ 11 | "test.ts", 12 | "**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /ClientApp/app/containers/not-found/not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-not-found', 5 | templateUrl: './not-found.component.html' 6 | }) 7 | export class NotFoundComponent implements OnInit { 8 | constructor() {} 9 | 10 | ngOnInit() {} 11 | } 12 | -------------------------------------------------------------------------------- /ClientApp/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": ["jasmine", "node"] 9 | }, 10 | "files": ["polyfills.ts"], 11 | "include": ["**/*.spec.ts", "**/*.d.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /ClientApp/app/containers/counter/counter.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-counter', 5 | templateUrl: './counter.component.html' 6 | }) 7 | export class CounterComponent { 8 | public currentCount = 0; 9 | 10 | public incrementCounter() { 11 | this.currentCount++; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Server/Data/LoggingEFStartup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace AspCoreServer.Data 5 | { 6 | public static class LoggingEFStartup 7 | { 8 | public static async Task InitializeDatabaseAsync(IServiceProvider services) 9 | { 10 | //Implent to your hearts' content 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Server/Models/IRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Asp2017.Server.Models { 7 | public class IRequest { 8 | public object cookies { get; set; } 9 | public object headers { get; set; } 10 | public object host { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ClientApp/app/containers/lazy/lazy.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-lazy-view', 5 | template: ` 6 |

Lazy-Loaded Component!

7 |
8 | Fun fact: This was lazy-loaded :) 9 | Check your Network tab! 10 |
11 | ` 12 | }) 13 | export class LazyComponent {} 14 | -------------------------------------------------------------------------------- /Server/Models/TransferData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Asp2017.Server.Models { 7 | public class TransferData { 8 | public dynamic request { get; set; } 9 | 10 | // Your data here ? 11 | public object thisCameFromDotNET { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ClientApp/app/containers/lazy/lazy.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { LazyComponent } from './lazy.component'; 4 | 5 | @NgModule({ 6 | declarations: [LazyComponent], 7 | imports: [ 8 | RouterModule.forChild([ 9 | { path: '', component: LazyComponent, pathMatch: 'full' } 10 | ]) 11 | ] 12 | }) 13 | export class LazyModule {} 14 | -------------------------------------------------------------------------------- /Server/Data/SpaDbContext.cs: -------------------------------------------------------------------------------- 1 | using AspCoreServer.Models; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace AspCoreServer.Data { 5 | public class SpaDbContext : DbContext { 6 | public SpaDbContext (DbContextOptions options) : base (options) { 7 | Database.EnsureCreated (); 8 | } 9 | 10 | //List of DB Models - Add your DB models here 11 | public DbSet User { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | 2 | @Html.Raw(ViewData["SpaHtml"]) 3 | 4 | 5 | 6 | 7 | 8 | @section scripts { 9 | 10 | 11 | } 12 | 13 | @Html.Raw(ViewData["ServiceWorker"]) 14 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "dotnet", 6 | "isShellCommand": true, 7 | "args": [], 8 | "tasks": [ 9 | { 10 | "taskName": "build", 11 | "args": ["Asp2017.sln"], 12 | "isBuildCommand": true, 13 | "showOutput": "silent", 14 | "problemMatcher": "$msCompile" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /Server/Data/CoreEFStartup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace AspCoreServer.Data 6 | { 7 | public static class CoreEFStartup 8 | { 9 | public static async Task InitializeDatabaseAsync(IServiceProvider services) 10 | { 11 | var context = services.GetRequiredService(); 12 | 13 | await context.Database.EnsureCreatedAsync(); 14 | } 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "es2015", 5 | "target": "es5", 6 | "alwaysStrict": true, 7 | "noImplicitAny": false, 8 | "sourceMap": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "skipDefaultLibCheck": true, 12 | "skipLibCheck": true, 13 | "allowUnreachableCode": false, 14 | "lib": [ 15 | "es2016", 16 | "dom" 17 | ], 18 | "types": [ "node" ] 19 | }, 20 | "include": [ 21 | "ClientApp" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /Server/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace AspCoreServer.Models { 5 | public class User { 6 | public int ID { get; set; } 7 | public string Name { get; set; } 8 | 9 | [DisplayFormat (DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] 10 | [DataType (DataType.Date)] 11 | public DateTime EntryTime { get; set; } 12 | 13 | //Setting Default value 14 | public User () { 15 | EntryTime = DateTime.Now; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ClientApp/app/components/navmenu/navmenu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-nav-menu', 5 | templateUrl: './navmenu.component.html', 6 | styleUrls: ['./navmenu.component.scss'] 7 | }) 8 | export class NavMenuComponent { 9 | collapse: string = 'collapse'; 10 | 11 | collapseNavbar(): void { 12 | if (this.collapse.length > 1) { 13 | this.collapse = ''; 14 | } else { 15 | this.collapse = 'collapse'; 16 | } 17 | } 18 | 19 | collapseMenu() { 20 | this.collapse = 'collapse'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ClientApp/boot.browser.ts: -------------------------------------------------------------------------------- 1 | import './polyfills/browser.polyfills'; 2 | import { enableProdMode } from '@angular/core'; 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | import { AppModule } from './app/app.module.browser'; 5 | 6 | // // Enable either Hot Module Reloading or production mode 7 | if (module['hot']) { 8 | module['hot'].accept(); 9 | module['hot'].dispose(() => { 10 | modulePromise.then(appModule => appModule.destroy()); 11 | }); 12 | } else { 13 | enableProdMode(); 14 | } 15 | 16 | const modulePromise = platformBrowserDynamic().bootstrapModule(AppModule); 17 | -------------------------------------------------------------------------------- /webpack.additions.js: -------------------------------------------------------------------------------- 1 | /* [ Webpack Additions ] 2 | * 3 | * This file contains ADD-ONS we are adding on-top of the traditional JavaScriptServices repo 4 | * We do this so that those already using JavaScriptServices can easily figure out how to combine this repo into it. 5 | */ 6 | 7 | // Shared rules[] we need to add 8 | const sharedModuleRules = [ 9 | // sass 10 | { 11 | test: /\.scss$/, 12 | loaders: ['to-string-loader', 'css-loader', 'sass-loader'] 13 | }, 14 | // font-awesome 15 | { test: /\.(woff2?|ttf|eot|svg)$/, loader: 'url-loader?limit=10000' } 16 | ]; 17 | 18 | module.exports = { 19 | sharedModuleRules 20 | }; 21 | -------------------------------------------------------------------------------- /ClientApp/app/containers/not-found/not-found.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Ahhhhhhhhhhh! This page doesn't exist

4 |

Not to worry. You can either head back to 5 | our homepage, or sit there and listen to a goat scream like a human.

6 |
7 |
8 | 10 |
11 | -------------------------------------------------------------------------------- /ClientApp/app/containers/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { TranslateService } from '@ngx-translate/core'; 3 | 4 | @Component({ 5 | selector: 'app-home', 6 | templateUrl: './home.component.html' 7 | }) 8 | export class HomeComponent implements OnInit { 9 | title: string = 10 | 'Angular 7.x Universal & ASP.NET Core 2.1 advanced starter-kit'; 11 | 12 | // Use "constructor"s only for dependency injection 13 | constructor(public translate: TranslateService) {} 14 | 15 | // Here you want to handle anything with @Input()'s @Output()'s 16 | // Data retrieval / etc - this is when the Component is "ready" and wired up 17 | ngOnInit() {} 18 | 19 | public setLanguage(lang) { 20 | this.translate.use(lang); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docker-compose.dcproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.1 5 | Linux 6 | 10b71bfc-c3ed-40b0-bb25-e38f04135e17 7 | LaunchBrowser 8 | {Scheme}://localhost:{ServicePort} 9 | asp2017 10 | 11 | 12 | 13 | docker-compose.yml 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-bootstrap', 5 | templateUrl: './ngx-bootstrap.component.html' 6 | }) 7 | export class NgxBootstrapComponent { 8 | public oneAtATime: boolean = true; 9 | public items = ['Item 1', 'Item 2', 'Item 3']; 10 | 11 | public status = { 12 | isFirstOpen: true, 13 | isFirstDisabled: false, 14 | open: false 15 | }; 16 | 17 | public groups = [ 18 | { 19 | title: 'Angular is neato gang!', 20 | content: 'ASP.NET Core is too :)' 21 | }, 22 | { 23 | title: 'Another One!', 24 | content: 'Some content going here' 25 | } 26 | ]; 27 | 28 | // Use "constructor"s only for dependency injection 29 | constructor() {} 30 | 31 | addItem(): void { 32 | this.items.push(`Items ${this.items.length + 1}`); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base 2 | WORKDIR /app 3 | RUN apt-get -qq update && apt-get install -y build-essential 4 | RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - 5 | RUN apt-get install -y nodejs 6 | RUN npm i -g --unsafe-perm node-sass && npm rebuild --unsafe-perm node-sass -f 7 | EXPOSE 80 8 | 9 | FROM microsoft/dotnet:2.1-sdk AS build 10 | WORKDIR /src 11 | COPY Asp2017.csproj . 12 | RUN apt-get -qq update && apt-get install build-essential -y && apt-get install -my wget gnupg && apt-get -qq -y install bzip2 13 | RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - 14 | RUN apt-get install -y nodejs 15 | RUN dotnet restore ./Asp2017.csproj 16 | COPY . . 17 | WORKDIR /src/ 18 | RUN dotnet build Asp2017.csproj -c Release -o /app 19 | 20 | FROM build AS publish 21 | RUN dotnet publish Asp2017.csproj -c Release -o /app 22 | 23 | FROM base AS final 24 | WORKDIR /app 25 | COPY --from=publish /app . 26 | ENTRYPOINT ["dotnet", "Asp2017.dll"] 27 | -------------------------------------------------------------------------------- /ClientApp/polyfills/polyfills.ts: -------------------------------------------------------------------------------- 1 | // Note: * The order is IMPORTANT! * 2 | 3 | /*************************************************************************************************** 4 | * BROWSER POLYFILLS 5 | */ 6 | 7 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 8 | import 'core-js/es6/symbol'; 9 | import 'core-js/es6/object'; 10 | import 'core-js/es6/function'; 11 | import 'core-js/es6/parse-int'; 12 | import 'core-js/es6/parse-float'; 13 | import 'core-js/es6/number'; 14 | import 'core-js/es6/math'; 15 | import 'core-js/es6/string'; 16 | import 'core-js/es6/date'; 17 | import 'core-js/es6/array'; 18 | import 'core-js/es6/regexp'; 19 | import 'core-js/es6/map'; 20 | import 'core-js/es6/set'; 21 | 22 | /** */ 23 | import 'reflect-metadata'; 24 | 25 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 26 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 27 | /** Evergreen browsers require these. **/ 28 | import 'core-js/es6/reflect'; 29 | import 'core-js/es7/reflect'; -------------------------------------------------------------------------------- /ClientApp/app/app.module.server.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { ServerModule } from '@angular/platform-server'; 4 | import { PrebootModule } from 'preboot'; 5 | import { AppComponent } from './app.component'; 6 | import { AppModuleShared } from './app.module'; 7 | 8 | import { TransferHttpCacheModule, StateTransferInitializerModule } from '@nguniversal/common'; 9 | 10 | @NgModule({ 11 | bootstrap: [AppComponent], 12 | imports: [ 13 | // Our Common AppModule 14 | AppModuleShared, 15 | 16 | ServerModule, 17 | PrebootModule.withConfig({ appRoot: 'app-root' }), 18 | NoopAnimationsModule, 19 | 20 | TransferHttpCacheModule, // still needs fixes for 5.0 21 | // Leave this commented out for now, as it breaks Server-renders 22 | // Looking into fixes for this! - @MarkPieszak 23 | // StateTransferInitializerModule // <-- broken for the time-being with ASP.NET 24 | ] 25 | }) 26 | export class AppModule { 27 | constructor() {} 28 | } 29 | -------------------------------------------------------------------------------- /Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @ViewData["Title"] 7 | 8 | 9 | @Html.Raw(ViewData["Meta"]) 10 | @Html.Raw(ViewData["Links"]) 11 | 12 | 13 | 14 | @Html.Raw(ViewData["Styles"]) 15 | 16 | 17 | 18 | @RenderBody() 19 | 20 | 21 | @Html.Raw(ViewData["TransferData"]) 22 | @Html.Raw(ViewData["Scripts"]) 23 | 24 | @RenderSection("scripts", required: false) 25 | 26 | 27 | -------------------------------------------------------------------------------- /ClientApp/app/components/user-detail/user-detail.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{user.name}} details:

3 |
4 |
5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | 25 | -------------------------------------------------------------------------------- /ClientApp/app/_styles.scss: -------------------------------------------------------------------------------- 1 | $body-bg: #f1f1f1; 2 | $body-color: #111; 3 | $theme-colors: ( 4 | 'primary': #216dad 5 | ); 6 | $theme-colors: ( 7 | 'accent': #669ecd 8 | ); 9 | 10 | @import '~bootstrap/scss/bootstrap'; 11 | .panel { 12 | box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 13 | 0 1px 5px 0 rgba(0, 0, 0, 0.12); 14 | height: 100%; 15 | flex: 1; 16 | background-color: rgba(255, 255, 255, 0.3); 17 | border-radius: 0.25rem; 18 | .title { 19 | background-color: #86afd0; 20 | color: #ffffff; 21 | text-align: center; 22 | border-top-left-radius: 0.25rem; 23 | border-top-right-radius: 0.25rem; 24 | } 25 | .body { 26 | display: flex; 27 | } 28 | } 29 | 30 | @include media-breakpoint-down(md) { 31 | .panel { 32 | .body { 33 | flex-direction: column; 34 | padding: 0px; 35 | } 36 | .title { 37 | font-size: 1.5rem; 38 | } 39 | } 40 | } 41 | 42 | @include media-breakpoint-up(md) { 43 | .panel { 44 | .body { 45 | flex-direction: row; 46 | padding: 1.5rem; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ClientApp/app/shared/user.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable, Injector } from '@angular/core'; 3 | import { ORIGIN_URL } from '@nguniversal/aspnetcore-engine/tokens'; 4 | import { IUser } from '../models/User'; 5 | 6 | @Injectable() 7 | export class UserService { 8 | private baseUrl: string; 9 | 10 | constructor(private http: HttpClient, private injector: Injector) { 11 | this.baseUrl = this.injector.get(ORIGIN_URL); 12 | } 13 | 14 | getUsers() { 15 | return this.http.get(`${this.baseUrl}/api/users`); 16 | } 17 | 18 | getUser(user: IUser) { 19 | return this.http.get(`${this.baseUrl}/api/users/` + user.id); 20 | } 21 | 22 | deleteUser(user: IUser) { 23 | return this.http.delete(`${this.baseUrl}/api/users/` + user.id); 24 | } 25 | 26 | updateUser(user: IUser) { 27 | return this.http.put(`${this.baseUrl}/api/users/` + user.id, user); 28 | } 29 | 30 | addUser(newUserName: string) { 31 | return this.http.post(`${this.baseUrl}/api/users`, { 32 | name: newUserName 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ClientApp/test/boot-tests.js: -------------------------------------------------------------------------------- 1 | Error.stackTraceLimit = Infinity; 2 | // Load required polyfills and testing libraries 3 | require('core-js'); // Added for Phantomjs 4 | require('zone.js'); 5 | require('zone.js/dist/long-stack-trace-zone'); 6 | require('zone.js/dist/proxy.js'); 7 | require('zone.js/dist/sync-test'); 8 | require('zone.js/dist/jasmine-patch'); 9 | require('zone.js/dist/async-test'); 10 | require('zone.js/dist/fake-async-test'); 11 | const testing = require('@angular/core/testing'); 12 | const testingBrowser = require('@angular/platform-browser-dynamic/testing'); 13 | 14 | // Prevent Karma from running prematurely 15 | __karma__.loaded = function() {}; 16 | 17 | // First, initialize the Angular testing environment 18 | testing 19 | .getTestBed() 20 | .initTestEnvironment( 21 | testingBrowser.BrowserDynamicTestingModule, 22 | testingBrowser.platformBrowserDynamicTesting() 23 | ); 24 | 25 | // Then we find all the tests 26 | const context = require.context('../', true, /\.spec\.ts$/); 27 | 28 | // And load the modules 29 | context.keys().map(context); 30 | 31 | // Finally, start Karma to run the tests 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-2017 Mark Pieszak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ClientApp/app/containers/counter/counter.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { CounterComponent } from './counter.component'; 3 | 4 | import {} from 'jasmine'; 5 | 6 | let fixture: ComponentFixture; 7 | 8 | describe('Counter component', () => { 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ declarations: [CounterComponent] }); 11 | fixture = TestBed.createComponent(CounterComponent); 12 | fixture.detectChanges(); 13 | }); 14 | 15 | it('should display a title', async(() => { 16 | const titleText = fixture.nativeElement.querySelector('h1').textContent; 17 | expect(titleText).toEqual('Counter'); 18 | })); 19 | 20 | it('should start with count 0, then increments by 1 when clicked', async(() => { 21 | const countElement = fixture.nativeElement.querySelector('strong'); 22 | expect(countElement.textContent).toEqual('0'); 23 | 24 | const incrementButton = fixture.nativeElement.querySelector('button'); 25 | incrementButton.click(); 26 | fixture.detectChanges(); 27 | expect(countElement.textContent).toEqual('1'); 28 | })); 29 | }); 30 | -------------------------------------------------------------------------------- /ClientApp/app/app.component.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | /* *** Overall APP Styling can go here *** 3 | -------------------------------------------- 4 | Note: This Component has ViewEncapsulation.None so the styles will bleed out 5 | 6 | */ 7 | 8 | body { 9 | line-height: 18px; 10 | padding-top: $header-height; 11 | } 12 | 13 | .body-content { 14 | margin: auto; 15 | } 16 | 17 | h1 { 18 | border-bottom: 3px theme-color('accent') solid; 19 | font-size: 24px; 20 | } 21 | 22 | h2 { 23 | font-size: 20px; 24 | } 25 | 26 | h1, 27 | h2, 28 | h3 { 29 | padding: 3px 0; 30 | } 31 | 32 | ul { 33 | padding: 10px 25px; 34 | } 35 | 36 | ul li { 37 | padding: 5px 0; 38 | } 39 | 40 | blockquote { 41 | margin: 25px 10px; 42 | padding: 10px 35px 10px 10px; 43 | border-left: 10px color('green') solid; 44 | background: $gray-100; 45 | } 46 | 47 | blockquote a, 48 | blockquote a:hover { 49 | color: $green; 50 | } 51 | 52 | @include media-breakpoint-up(lg) { 53 | body { 54 | padding-top: 30px; 55 | } 56 | .body-content { 57 | margin-left: $menu-max-width; 58 | } 59 | h1 { 60 | border-bottom: 5px #4189c7 solid; 61 | font-size: 36px; 62 | } 63 | h2 { 64 | font-size: 30px; 65 | } 66 | h1, 67 | h2, 68 | h3 { 69 | padding: 10px 0; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ClientApp/app/app.module.browser.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { ORIGIN_URL, REQUEST } from '@nguniversal/aspnetcore-engine/tokens'; 4 | import { PrebootModule } from 'preboot'; 5 | import { AppComponent } from './app.component'; 6 | import { AppModuleShared } from './app.module'; 7 | 8 | export function getOriginUrl() { 9 | return window.location.origin; 10 | } 11 | 12 | export function getRequest() { 13 | // the Request object only lives on the server 14 | return { cookie: document.cookie }; 15 | } 16 | 17 | @NgModule({ 18 | bootstrap: [AppComponent], 19 | imports: [ 20 | PrebootModule.withConfig({ appRoot: 'app-root' }), 21 | BrowserAnimationsModule, 22 | 23 | // Our Common AppModule 24 | AppModuleShared 25 | ], 26 | providers: [ 27 | { 28 | // We need this for our Http calls since they'll be using an ORIGIN_URL provided in main.server 29 | // (Also remember the Server requires Absolute URLs) 30 | provide: ORIGIN_URL, 31 | useFactory: getOriginUrl 32 | }, 33 | { 34 | // The server provides these in main.server 35 | provide: REQUEST, 36 | useFactory: getRequest 37 | } 38 | ] 39 | }) 40 | export class AppModule {} 41 | -------------------------------------------------------------------------------- /wwwroot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ASP.NET Core 2.1 & Angular 7(+) Advanced Starter", 3 | "short_name": "aspnetcore-angular2-universal", 4 | "description": 5 | "ASP.NET Core 2.1 & Angular 7(+) Advanced Starter - with Server-side prerendering (for Angular SEO)!", 6 | "icons": [ 7 | { 8 | "src": "/images/icon36x36.png", 9 | "sizes": "36x36", 10 | "type": "image/png" 11 | }, { 12 | "src": "/images/icon48x48.png", 13 | "sizes": "48x48", 14 | "type": "image/png" 15 | }, { 16 | "src": "/images/icon72x72.png", 17 | "sizes": "72x72", 18 | "type": "image/png" 19 | }, { 20 | "src": "/images/icon96x96.png", 21 | "sizes": "96x96", 22 | "type": "image/png" 23 | }, { 24 | "src": "/images/icon144x144.png", 25 | "sizes": "144x144", 26 | "type": "image/png" 27 | }, { 28 | "src": "/images/icon192x192.png", 29 | "sizes": "192x192", 30 | "type": "image/png" 31 | }, { 32 | "src": "/images/icon256x256.png", 33 | "sizes": "256x256", 34 | "type": "image/png" 35 | }, { 36 | "src": "/images/icon384x384.png", 37 | "sizes": "384x384", 38 | "type": "image/png" 39 | }, { 40 | "src": "/images/icon512x512.png", 41 | "sizes": "512x512", 42 | "type": "image/png" 43 | } 44 | ], 45 | "background_color": "#3E4EB8", 46 | "theme_color": "#2F3BA2", 47 | "display": "standalone", 48 | "start_url": "/" 49 | } 50 | -------------------------------------------------------------------------------- /Server/Data/SimpleContentEFStartup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AspCoreServer.Models; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace AspCoreServer.Data 8 | { 9 | public static class SimpleContentEFStartup 10 | { 11 | public static async Task InitializeDatabaseAsync(IServiceProvider services) 12 | { 13 | var context = services.GetRequiredService(); 14 | 15 | 16 | if (await context.User.AnyAsync()) 17 | { 18 | return; // DB has been seeded 19 | } 20 | var users = new User[] { 21 | new User () { Name = "Mark Pieszak" }, 22 | new User () { Name = "Abrar Jahin" }, 23 | new User () { Name = "hakonamatata" }, 24 | new User () { Name = "LiverpoolOwen" }, 25 | new User () { Name = "Ketrex" }, 26 | new User () { Name = "markwhitfeld" }, 27 | new User () { Name = "daveo1001" }, 28 | new User () { Name = "paonath" }, 29 | new User () { Name = "nalex095" }, 30 | new User () { Name = "ORuban" }, 31 | new User () { Name = "Gaulomatic" }, 32 | new User () { Name = "GRIMMR3AP3R" } 33 | }; 34 | await context.User.AddRangeAsync(users); 35 | 36 | await context.SaveChangesAsync(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ClientApp/boot.server.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/dist/zone-node'; 2 | import './polyfills/server.polyfills'; 3 | 4 | import { enableProdMode } from '@angular/core'; 5 | import { createTransferScript, IEngineOptions, ngAspnetCoreEngine } from '@nguniversal/aspnetcore-engine'; 6 | import { createServerRenderer } from 'aspnet-prerendering'; 7 | 8 | // Grab the (Node) server-specific NgModule 9 | import { AppModule } from './app/app.module.server'; 10 | 11 | enableProdMode(); 12 | 13 | export default createServerRenderer((params) => { 14 | 15 | // Platform-server provider configuration 16 | const setupOptions: IEngineOptions = { 17 | appSelector: '', 18 | ngModule: AppModule, 19 | request: params, 20 | providers: [ 21 | // Optional - Any other Server providers you want to pass 22 | // (remember you'll have to provide them for the Browser as well) 23 | ] 24 | }; 25 | 26 | return ngAspnetCoreEngine(setupOptions).then(response => { 27 | 28 | // Apply your transferData to response.globals 29 | response.globals.transferData = createTransferScript({ 30 | someData: 'Transfer this to the client on the window.TRANSFER_CACHE {} object', 31 | fromDotnet: params.data.thisCameFromDotNET // example of data coming from dotnet, in HomeController 32 | }); 33 | 34 | return ({ 35 | html: response.html, // our serialized 36 | globals: response.globals // all of our styles/scripts/meta-tags/link-tags for aspnet to serve up 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using AspCoreServer; 2 | using AspCoreServer.Data; 3 | using Microsoft.AspNetCore; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Logging; 7 | using System; 8 | using System.IO; 9 | using System.Threading.Tasks; 10 | 11 | public class Program 12 | { 13 | public static async Task Main(string[] args) 14 | { 15 | var host = BuildWebHost(args); 16 | using (var scope = host.Services.CreateScope()) 17 | { 18 | var services = scope.ServiceProvider; 19 | 20 | try 21 | { 22 | await EnsureDataStorageIsReady(services); 23 | 24 | } catch (Exception ex) 25 | { 26 | var logger = services.GetRequiredService>(); 27 | logger.LogError(ex, "An error occurred while seeding the database."); 28 | } 29 | } 30 | 31 | host.Run(); 32 | } 33 | public static IWebHost BuildWebHost(string[] args) => 34 | WebHost.CreateDefaultBuilder(args) 35 | .UseKestrel() 36 | .UseContentRoot(Directory.GetCurrentDirectory()) 37 | .UseIISIntegration() 38 | .UseStartup() 39 | .Build(); 40 | 41 | private static async Task EnsureDataStorageIsReady(IServiceProvider services) 42 | { 43 | await CoreEFStartup.InitializeDatabaseAsync(services); 44 | await SimpleContentEFStartup.InitializeDatabaseAsync(services); 45 | await LoggingEFStartup.InitializeDatabaseAsync(services); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ClientApp/app/components/user-detail/user-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | OnChanges, 6 | OnInit, 7 | Output, 8 | SimpleChanges 9 | } from '@angular/core'; 10 | import { FormControl, FormGroup } from '@angular/forms'; 11 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; 12 | import { IUser } from '../../models/User'; 13 | import { UserService } from '../../shared/user.service'; 14 | 15 | @Component({ 16 | selector: 'app-user-detail', 17 | styleUrls: ['./user-detail.component.scss'], 18 | templateUrl: './user-detail.component.html' 19 | }) 20 | export class UserDetailComponent implements OnInit, OnChanges { 21 | @Input() user: IUser; 22 | @Output() userUpdate: EventEmitter = new EventEmitter(); 23 | userForm = new FormGroup({ 24 | id: new FormControl(), 25 | name: new FormControl() 26 | }); 27 | constructor(private userService: UserService) {} 28 | 29 | ngOnInit() { 30 | this.userForm.valueChanges 31 | .pipe( 32 | debounceTime(400), 33 | distinctUntilChanged() 34 | ) 35 | .subscribe(user => (this.user = user)); 36 | } 37 | ngOnChanges(changes: SimpleChanges) { 38 | this.userForm.patchValue(this.user); 39 | } 40 | 41 | updateUser() { 42 | this.userService.updateUser(this.userForm.value).subscribe( 43 | result => { 44 | console.log('Put user result: ', result); 45 | }, 46 | error => { 47 | console.log(`There was an issue. ${error._body}.`); 48 | } 49 | ); 50 | this.userUpdate.emit(this.user); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ClientApp/app/containers/users/users.component.html: -------------------------------------------------------------------------------- 1 |

This is a RestAPI Example (hitting WebAPI in our case)

2 | 3 |
4 | Let's get some fake users from Rest: 5 |
You can find the Web API Routes in 6 | {{ "/Server/RestAPI/ ... "}} 7 |
8 |
9 |

Users

10 |
11 |
12 |

13 | Loading... 14 |

15 |
    16 |
  • 17 | {{user.id}} 18 |

    {{user.name}}

    19 | 22 |
  • 23 |
24 |
25 |
26 |
27 |
28 | 29 |
30 | 31 |
32 |
33 |
34 | 35 |
36 |
37 | 38 |
39 | -------------------------------------------------------------------------------- /ClientApp/boot.server.PRODUCTION.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/dist/zone-node'; 2 | import './polyfills/server.polyfills'; 3 | import { enableProdMode } from '@angular/core'; 4 | import { createServerRenderer } from 'aspnet-prerendering'; 5 | 6 | // Grab the (Node) server-specific NgModule 7 | const { AppModuleNgFactory } = require('./app/app.module.server.ngfactory'); // <-- ignore this - this is Production only 8 | import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from '@nguniversal/aspnetcore-engine'; 9 | 10 | enableProdMode(); 11 | 12 | export default createServerRenderer(params => { 13 | // Platform-server provider configuration 14 | const setupOptions: IEngineOptions = { 15 | appSelector: '', 16 | ngModule: AppModuleNgFactory, 17 | request: params, 18 | providers: [ 19 | // Optional - Any other Server providers you want to pass 20 | // (remember you'll have to provide them for the Browser as well) 21 | ] 22 | }; 23 | 24 | return ngAspnetCoreEngine(setupOptions).then(response => { 25 | // Apply your transferData to response.globals 26 | response.globals.transferData = createTransferScript({ 27 | someData: 28 | 'Transfer this to the client on the window.TRANSFER_CACHE {} object', 29 | fromDotnet: params.data.thisCameFromDotNET // example of data coming from dotnet, in HomeController 30 | }); 31 | 32 | return { 33 | html: response.html, // our serialized 34 | globals: response.globals // all of our styles/scripts/meta-tags/link-tags for aspnet to serve up 35 | }; 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /Asp2017.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27703.2018 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Asp2017", "Asp2017.csproj", "{BC28E9F7-E6EC-447D-AABD-17683BEAD625}" 7 | EndProject 8 | Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{10B71BFC-C3ED-40B0-BB25-E38F04135E17}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {BC28E9F7-E6EC-447D-AABD-17683BEAD625}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {BC28E9F7-E6EC-447D-AABD-17683BEAD625}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {BC28E9F7-E6EC-447D-AABD-17683BEAD625}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {BC28E9F7-E6EC-447D-AABD-17683BEAD625}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {10B71BFC-C3ED-40B0-BB25-E38F04135E17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {10B71BFC-C3ED-40B0-BB25-E38F04135E17}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {10B71BFC-C3ED-40B0-BB25-E38F04135E17}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {10B71BFC-C3ED-40B0-BB25-E38F04135E17}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {DE341460-9041-458F-99D5-43FC7572CFA6} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /ClientApp/test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | basePath: '.', 7 | frameworks: ['jasmine'], 8 | exclude: [], 9 | files: ['../../wwwroot/dist/vendor.js', './boot-tests.js'], 10 | preprocessors: { 11 | './boot-tests.js': ['coverage', 'webpack', 'sourcemap'] 12 | }, 13 | client: { 14 | captureConsole: false 15 | }, 16 | coverageReporter: { 17 | type: 'in-memory' 18 | }, 19 | remapCoverageReporter: { 20 | 'text-summary': null, 21 | json: './coverage/coverage.json', 22 | html: './coverage/html' 23 | }, 24 | reporters: ['mocha', 'coverage', 'remap-coverage'], 25 | port: 9876, 26 | colors: true, 27 | logLevel: config.LOG_WARN, 28 | autoWatch: false, 29 | browsers: ['Chrome'], 30 | mime: { 31 | 'application/javascript': ['ts', 'tsx'] 32 | }, 33 | singleRun: true, 34 | webpack: require('./webpack.config.test.js')({ 35 | env: 'test' 36 | }), 37 | webpackMiddleware: { 38 | noInfo: true, 39 | stats: { 40 | chunks: false 41 | } 42 | }, 43 | // you can define custom flags 44 | customLaunchers: { 45 | PhantomJS_custom: { 46 | base: 'PhantomJS', 47 | options: { 48 | windowName: 'test-window', 49 | settings: { 50 | webSecurityEnabled: false 51 | } 52 | }, 53 | flags: ['--load-images=true'] 54 | // debug: true 55 | } 56 | }, 57 | phantomjsLauncher: { 58 | // Have phantomjs exit if a ResourceError is encountered (useful if karma exits without killing phantom) 59 | exitOnResourceError: true 60 | } 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html: -------------------------------------------------------------------------------- 1 |

Ngx-bootstrap Demo:

2 | 3 |
4 | Here we're using Bootstrap via 5 | ngx-bootstrap, which can even be rendered on the server! 6 |
7 |
8 | 9 |
10 |
11 |
12 | 13 |

Bootstrap Accordion demo:

14 | 15 |

16 | 19 | 22 |

23 | 24 |
25 | 28 |
29 | 30 | 31 | 32 | 33 | This content is straight in the template. 34 | 35 | 36 | {{ group?.content }} 37 | 38 | 39 |

The body of the accordion group grows to fit the contents

40 | 41 |
{{item}}
42 |
43 | 44 |
45 | I can have markup, too! 46 | 47 |
48 | This is just some content to illustrate fancy headings. 49 |
50 |
51 | -------------------------------------------------------------------------------- /ClientApp/app/components/navmenu/navmenu.component.html: -------------------------------------------------------------------------------- 1 | 42 | -------------------------------------------------------------------------------- /ClientApp/app/containers/users/users.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables'; 2 | .users-wrapper { 3 | max-height: 65vh; 4 | overflow-y: auto; 5 | width: 100%; 6 | margin-bottom: 15px; 7 | } 8 | 9 | .card-wrapper { 10 | display: flex; 11 | flex-direction: column; 12 | flex: 1; 13 | padding-right: 15px; 14 | padding-left: 15px; 15 | } 16 | 17 | .users { 18 | list-style-type: none; 19 | display: flex; 20 | flex-direction: column; 21 | margin: 0px; 22 | padding: 0px 2px; 23 | } 24 | 25 | h1 { 26 | font-size: 1.75rem; 27 | } 28 | 29 | .user { 30 | @extend .btn; 31 | border: 0px; 32 | background-color: #d9d9d9; 33 | display: flex; 34 | flex-direction: row; 35 | align-items: center; 36 | padding: 0px; 37 | &:hover { 38 | color: #607d8b; 39 | background-color: #e6e6e6; 40 | left: 0.1em; 41 | } 42 | &.selected { 43 | background-color: #7eaacd !important; 44 | color: white; 45 | } 46 | } 47 | 48 | .users li > * { 49 | display: flex; 50 | flex-direction: column; 51 | align-items: center; 52 | justify-content: center; 53 | } 54 | 55 | .users li p { 56 | flex: 1; 57 | margin: 0px; 58 | } 59 | 60 | .users li.selected:hover { 61 | background-color: #bbd8dc !important; 62 | color: white; 63 | } 64 | 65 | .user-id { 66 | @extend .input-group-text; 67 | background-color: #607d8b; 68 | color: white; 69 | width: 2.5rem; 70 | border: 0px; 71 | border-top-right-radius: 0px; 72 | border-bottom-right-radius: 0px; 73 | } 74 | 75 | .delete-user { 76 | @extend .btn, .badge, .badge-secondary; 77 | height: 25px; 78 | width: 25px; 79 | margin-right: 10px; 80 | } 81 | 82 | @include media-breakpoint-down(sm) { 83 | .body { 84 | flex-direction: column-reverse !important; 85 | } 86 | } 87 | 88 | @include media-breakpoint-up(md) { 89 | .users-wrapper { 90 | max-height: 65vh; 91 | overflow-y: auto; 92 | margin-right: 1.25rem; 93 | margin-bottom: 0px; 94 | width: 100%; 95 | } 96 | .body { 97 | flex-direction: column; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Server/Helpers/HttpRequestExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Asp2017.Server.Models; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Http.Features; 9 | using Microsoft.AspNetCore.NodeServices; 10 | using Microsoft.AspNetCore.SpaServices.Prerendering; 11 | using Microsoft.Extensions.DependencyInjection; 12 | 13 | namespace Asp2017.Server.Helpers { 14 | public static class HttpRequestExtensions { 15 | public static IRequest AbstractRequestInfo(this HttpRequest request) => new IRequest() 16 | { 17 | cookies = request.Cookies, 18 | headers = request.Headers, 19 | host = request.Host 20 | }; 21 | 22 | public static async Task BuildPrerender(this HttpRequest request) => 23 | // Prerender / Serialize application (with Universal) 24 | await Prerenderer.RenderToString( 25 | "/", 26 | request.HttpContext.RequestServices.GetRequiredService(), 27 | new System.Threading.CancellationTokenSource().Token, 28 | new JavaScriptModuleExport(request.HttpContext.RequestServices.GetRequiredService().ContentRootPath + "/ClientApp/dist/main-server"), 29 | $"{request.Scheme}://{request.Host}{request.HttpContext.Features.Get().RawTarget}", 30 | request.HttpContext.Features.Get().RawTarget, 31 | // ** TransferData concept ** 32 | // Here we can pass any Custom Data we want ! 33 | // By default we're passing down Cookies, Headers, Host from the Request object here 34 | new TransferData 35 | { 36 | request = request.AbstractRequestInfo(), 37 | thisCameFromDotNET = "Hi Angular it's asp.net :)" 38 | }, // Our simplified Request object & any other CustommData you want to send! 39 | 30000, 40 | request.PathBase.ToString() 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Server/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using Asp2017.Server.Helpers; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Mvc; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace AspCoreServer.Controllers 8 | { 9 | public class HomeController : Controller { 10 | protected readonly IHostingEnvironment HostingEnvironment; 11 | public HomeController(IHostingEnvironment hostingEnv) => this.HostingEnvironment = hostingEnv; 12 | 13 | [HttpGet] 14 | public async Task Index () { 15 | var prerenderResult = await this.Request.BuildPrerender (); 16 | 17 | this.ViewData["SpaHtml"] = prerenderResult.Html; // our from Angular 18 | this.ViewData["Title"] = prerenderResult.Globals["title"]; // set our from Angular 19 | this.ViewData["Styles"] = prerenderResult.Globals["styles"]; // put styles in the correct place 20 | this.ViewData["Scripts"] = prerenderResult.Globals["scripts"]; // scripts (that were in our header) 21 | this.ViewData["Meta"] = prerenderResult.Globals["meta"]; // set our <meta> SEO tags 22 | this.ViewData["Links"] = prerenderResult.Globals["links"]; // set our <link rel="canonical"> etc SEO tags 23 | this.ViewData["TransferData"] = prerenderResult.Globals["transferData"]; // our transfer data set to window.TRANSFER_CACHE = {}; 24 | if (!this.HostingEnvironment.IsDevelopment ()) { 25 | this.ViewData["ServiceWorker"] = "<script>'serviceWorker'in navigator&&navigator.serviceWorker.register('/serviceworker')</script>"; 26 | } 27 | 28 | return View (); 29 | } 30 | 31 | [HttpGet] 32 | [Route("sitemap.xml")] 33 | public IActionResult SitemapXml() => Content($@"<?xml version=""1.0"" encoding=""utf-8""?> 34 | <urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9""> 35 | <url> 36 | <loc>http://localhost:4251/home</loc> 37 | <lastmod>{ DateTime.Now.ToString("yyyy-MM-dd")}</lastmod> 38 | </url> 39 | <url> 40 | <loc>http://localhost:4251/counter</loc> 41 | <lastmod>{DateTime.Now.ToString("yyyy-MM-dd")}</lastmod> 42 | </url> 43 | </urlset>", "text/xml"); 44 | 45 | public IActionResult Error() => View(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ClientApp/app/components/navmenu/navmenu.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables'; 2 | // Mobile first styling. 3 | /* Apply for small displays */ 4 | 5 | .navbar { 6 | position: fixed; 7 | background-color: theme-color('primary'); 8 | top: 0; 9 | right: 0; 10 | left: 0; 11 | z-index: 1030; 12 | } 13 | 14 | .navbar, 15 | a, 16 | button { 17 | color: $body-bg; 18 | } 19 | 20 | .nav { 21 | padding: 0px; 22 | background-color: white; 23 | } 24 | 25 | .nav-item a { 26 | /* If a menu item's text is too long, truncate it */ 27 | white-space: nowrap; 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | padding-right: 5px; 31 | } 32 | 33 | .nav-link { 34 | display: flex; 35 | svg { 36 | margin-right: 10px; 37 | width: 20px; 38 | height: 20px; 39 | } 40 | } 41 | 42 | .navbar-brand { 43 | font-size: 1.15rem; 44 | } 45 | 46 | li a { 47 | border: 1px solid transparent; 48 | &:hover { 49 | border: 1px solid theme-color-level(primary, 2); 50 | } 51 | } 52 | 53 | li.link-active a, 54 | li.link-active a:hover, 55 | li.link-active a:focus { 56 | background-color: theme-color('accent'); 57 | color: $body-bg; 58 | } 59 | 60 | @include media-breakpoint-up(lg) { 61 | .navbar { 62 | width: 275px; 63 | min-height: 1px; 64 | padding-right: 15px; 65 | padding-left: 15px; 66 | background-color: $body-bg; 67 | flex: 0 0 $menu-max-width; 68 | min-width: $menu-max-width; 69 | position: fixed; 70 | display: flex; 71 | flex-direction: column; 72 | top: 0; 73 | bottom: 0; 74 | left: 0; 75 | right: 0; 76 | z-index: 100; 77 | padding: 0px; 78 | align-items: flex-start; 79 | box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1); 80 | } 81 | .navbar, 82 | a, 83 | button { 84 | color: theme-color('primary'); 85 | } 86 | .navbar-brand { 87 | padding: 15px 15px; 88 | font-size: 100%; 89 | line-height: 20px; 90 | height: 50px; 91 | } 92 | .navbar-collapse { 93 | width: 100%; 94 | flex: 1; 95 | align-items: flex-start; 96 | } 97 | .nav { 98 | width: 100%; 99 | border-top: 1px solid #444; 100 | background-color: transparent; 101 | } 102 | .nav-item { 103 | float: none; 104 | font-size: 15px; 105 | margin: 6px; 106 | } 107 | .nav-item a { 108 | padding: 10px 16px; 109 | border-radius: 4px; 110 | white-space: nowrap; 111 | overflow: hidden; 112 | text-overflow: ellipsis; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /ClientApp/app/containers/users/users.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | animate, 3 | state, 4 | style, 5 | transition, 6 | trigger 7 | } from '@angular/animations'; 8 | import { Component, OnInit } from '@angular/core'; 9 | import { IUser } from '../../models/User'; 10 | import { UserService } from '../../shared/user.service'; 11 | 12 | @Component({ 13 | selector: 'app-users', 14 | templateUrl: './users.component.html', 15 | styleUrls: ['./users.component.scss'], 16 | animations: [ 17 | // Animation example 18 | // Triggered in the ngFor with [@flyInOut] 19 | trigger('flyInOut', [ 20 | state('in', style({ transform: 'translateY(0)' })), 21 | transition('void => *', [ 22 | style({ transform: 'translateY(-100%)' }), 23 | animate(1000) 24 | ]), 25 | transition('* => void', [ 26 | animate(1000, style({ transform: 'translateY(100%)' })) 27 | ]) 28 | ]) 29 | ] 30 | }) 31 | export class UsersComponent implements OnInit { 32 | users: IUser[]; 33 | selectedUser: IUser; 34 | 35 | // Use "constructor"s only for dependency injection 36 | constructor(private userService: UserService) {} 37 | 38 | // Here you want to handle anything with @Input()'s @Output()'s 39 | // Data retrieval / etc - this is when the Component is "ready" and wired up 40 | ngOnInit() { 41 | this.userService.getUsers().subscribe(result => { 42 | console.log('HttpClient [GET] /api/users/allresult', result); 43 | this.users = result; 44 | }); 45 | } 46 | 47 | onSelect(user: IUser): void { 48 | this.selectedUser = user; 49 | } 50 | 51 | deleteUser(user) { 52 | this.clearUser(); 53 | this.userService.deleteUser(user).subscribe( 54 | result => { 55 | console.log('Delete user result: ', result); 56 | let position = this.users.indexOf(user); 57 | this.users.splice(position, 1); 58 | }, 59 | error => { 60 | console.log(`There was an issue. ${error._body}.`); 61 | } 62 | ); 63 | } 64 | 65 | onUserUpdate(user: IUser) { 66 | this.users[this.users.findIndex(u => u.id == user.id)] = user; 67 | } 68 | 69 | addUser(newUserName) { 70 | this.userService.addUser(newUserName).subscribe( 71 | result => { 72 | console.log('Post user result: ', result); 73 | this.users.push(result); 74 | this.selectedUser = result; 75 | }, 76 | error => { 77 | console.log(`There was an issue. ${error._body}.`); 78 | } 79 | ); 80 | } 81 | 82 | clearUser() { 83 | if (this.selectedUser) { 84 | this.selectedUser = null; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ClientApp/app/shared/link.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * -- LinkService -- [Temporary] 3 | * @MarkPieszak 4 | * 5 | * Similar to Meta service but made to handle <link> creation for SEO purposes 6 | * Soon there will be an overall HeadService within Angular that handles Meta/Link everything 7 | */ 8 | 9 | import { isPlatformServer } from '@angular/common'; 10 | import { 11 | Inject, 12 | Injectable, 13 | PLATFORM_ID, 14 | RendererFactory2, 15 | ViewEncapsulation 16 | } from '@angular/core'; 17 | import { DOCUMENT } from '@angular/platform-browser'; 18 | 19 | @Injectable() 20 | export class LinkService { 21 | private isServer: boolean = isPlatformServer(this.platform_id); 22 | 23 | constructor( 24 | private rendererFactory: RendererFactory2, 25 | @Inject(DOCUMENT) private document, 26 | @Inject(PLATFORM_ID) private platform_id 27 | ) {} 28 | 29 | /** 30 | * Inject the State into the bottom of the <head> 31 | */ 32 | addTag(tag: LinkDefinition, forceCreation?: boolean) { 33 | try { 34 | const renderer = this.rendererFactory.createRenderer(this.document, { 35 | id: '-1', 36 | encapsulation: ViewEncapsulation.None, 37 | styles: [], 38 | data: {} 39 | }); 40 | 41 | const link = renderer.createElement('link'); 42 | const head = this.document.head; 43 | const selector = this._parseSelector(tag); 44 | 45 | if (head === null) { 46 | throw new Error('<head> not found within DOCUMENT.'); 47 | } 48 | 49 | Object.keys(tag).forEach((prop: string) => { 50 | return renderer.setAttribute(link, prop, tag[prop]); 51 | }); 52 | 53 | // [TODO]: get them to update the existing one (if it exists) ? 54 | renderer.appendChild(head, link); 55 | } catch (e) { 56 | console.error('Error within linkService : ', e); 57 | } 58 | } 59 | 60 | // updateTag(tag: LinkDefinition, selector?: string) { 61 | // if (!tag) return null; 62 | // selector = selector || this._parseSelector(tag); 63 | // const meta = this.getTag(selector); 64 | // if (meta) { 65 | // return this._setMetaElementAttributes(tag, meta); 66 | // } 67 | // return this._getOrCreateElement(tag, true); 68 | // } 69 | 70 | // getTag(attrSelector: string): HTMLMetaElement { 71 | // if (!attrSelector) return null; 72 | // return this._dom.querySelector(this._doc, `meta[${attrSelector}]`); 73 | // } 74 | 75 | private _parseSelector(tag: LinkDefinition): string { 76 | // Possibly re-work this 77 | const attr: string = tag.rel ? 'rel' : 'hreflang'; 78 | return `${attr}="${tag[attr]}"`; 79 | } 80 | } 81 | 82 | export declare type LinkDefinition = { 83 | charset?: string; 84 | crossorigin?: string; 85 | href?: string; 86 | hreflang?: string; 87 | media?: string; 88 | rel?: string; 89 | rev?: string; 90 | sizes?: string; 91 | target?: string; 92 | type?: string; 93 | } & { 94 | [prop: string]: string; 95 | }; 96 | -------------------------------------------------------------------------------- /ClientApp/app/containers/home/home.component.html: -------------------------------------------------------------------------------- 1 | <h1>{{ title }}</h1> 2 | 3 | <blockquote> 4 | <strong>Enjoy the latest features from .NET Core & Angular 7.x!</strong> 5 | <br> For more info check the repo here: 6 | <a href="https://github.com/MarkPieszak/aspnetcore-angular-universal" target="_blank">AspNetCore-Angular-Universal repo</a> 7 | <br> 8 | <br> 9 | </blockquote> 10 | 11 | <div class="row"> 12 | <div class="col-lg-6"> 13 | <h2>{{ 'HOME_FEATURE_LIST_TITLE' | translate }} </h2> 14 | <ul> 15 | <li>ASP.NET Core 2.1 :: ( Visual Studio 2017 )</li> 16 | <li> 17 | Angular 7.* front-end UI framework 18 | <ul> 19 | <li>Angular **platform-server** (aka: Universal) - server-side rendering for SEO, deep-linking, and incredible performance.</li> 20 | <!--<li>HMR State Management - Don't lose your applications state during HMR!</li>--> 21 | <li>AoT (Ahead-of-time) production compilation for even faster Prod builds.</li> 22 | </ul> 23 | </li> 24 | <li> 25 | The latest TypeScript 2.* features 26 | </li> 27 | <li> 28 | Webpack 29 | <ul> 30 | <li>Hot Module Reloading/Replacement for an amazing development experience.</li> 31 | <li>Tree-shaking</li> 32 | </ul> 33 | </li> 34 | 35 | <li>Bootstrap (ngx-bootstrap) : Bootstrap capable of being rendered even on the server.</li> 36 | <li>Unit testing via karma & jasmine.</li> 37 | </ul> 38 | 39 | </div> 40 | 41 | <div class="col-lg-6"> 42 | <h2>{{ 'HOME_ISSUES_TITLE' | translate }}</h2> 43 | 44 | <ul> 45 | <li> 46 | <strong>Issues with this Starter?</strong> 47 | <br>Please post an issue here: 48 | <a href="https://github.com/MarkPieszak/aspnetcore-angular2-universal" target="_blank">AspNetCore-Angular2-Universal repo</a> 49 | <br> 50 | <br> 51 | </li> 52 | <!--<li><strong>Issues with <u>Universal</u> itself?</strong> <br>Please post an issue here: <a href="https://github.com/angular/universal">Angular Universal repo</a></li>--> 53 | </ul> 54 | 55 | <h2><a href="https://trilon.io" target="_blank">Trilon Consulting - Trilon.io</a></h2> 56 | 57 | <strong>Consulting | Development | Training | Workshops</strong> 58 | <br><br> 59 | Get your Team or Application up to speed by working with some of the leading industry experts in JavaScript, Node / NestJS, & ASP.NET!<br> 60 | <br> 61 | <strong>Follow us on Twitter!</strong><br><br> 62 | <a href="http://www.twitter.com/trilon_io" target="_blank">@trilon_io</a> | 63 | <a href="http://www.twitter.com/MarkPieszak" target="_blank">@MarkPieszak</a> 64 | <br> 65 | <br> 66 | 67 | </div> 68 | 69 | </div> 70 | 71 | 72 | <div class="row"> 73 | <div class="col-lg-12"> 74 | 75 | <h2> {{ 'SWITCH_LANGUAGE' | translate }}</h2> 76 | 77 | <button class="btn btn-outline-secondary mr-2" (click)="setLanguage('en')"> 78 | <span class="flag-icon flag-icon-us"></span> {{ 'ENGLISH' | translate }} 79 | </button> 80 | 81 | <button class="btn btn-outline-secondary" (click)="setLanguage('no')"> 82 | <span class="flag-icon flag-icon-no"></span> {{ 'NORWEGIAN' | translate }} 83 | </button> 84 | 85 | </div> 86 | </div> 87 | -------------------------------------------------------------------------------- /ClientApp/test/webpack.config.test.js: -------------------------------------------------------------------------------- 1 | const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin'); 2 | const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); 3 | 4 | const webpack = require('webpack'); 5 | var path = require('path'); 6 | var rootPath = path.join.bind(path, path.resolve(__dirname, '../../')); 7 | 8 | module.exports = function(options) { 9 | return { 10 | devtool: 'inline-source-map', 11 | resolve: { 12 | extensions: ['.ts', '.js'], 13 | modules: [rootPath('ClientApp'), 'node_modules'] 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | enforce: 'pre', 19 | test: /\.js$/, 20 | loader: 'source-map-loader', 21 | exclude: [ 22 | rootPath('node_modules/rxjs'), 23 | rootPath('node_modules/@angular') 24 | ] 25 | }, 26 | { 27 | test: /\.ts$/, 28 | use: [ 29 | { 30 | loader: 'awesome-typescript-loader', 31 | query: { 32 | sourceMap: false, 33 | inlineSourceMap: true, 34 | compilerOptions: { 35 | removeComments: true 36 | } 37 | } 38 | }, 39 | 'angular2-template-loader' 40 | ], 41 | exclude: [/\.e2e\.ts$/] 42 | }, 43 | { 44 | test: /\.css$/, 45 | loader: ['to-string-loader', 'css-loader'] 46 | }, 47 | { 48 | test: /\.scss$/, 49 | loader: ['raw-loader', 'sass-loader'] 50 | }, 51 | { 52 | test: /\.html$/, 53 | loader: 'raw-loader' 54 | }, 55 | { 56 | enforce: 'post', 57 | test: /\.(js|ts)$/, 58 | loader: 'istanbul-instrumenter-loader', 59 | options: { 60 | esModules: true 61 | }, 62 | include: rootPath('ClientApp'), 63 | exclude: [/ClientApp\\test/, /\.(e2e|spec)\.ts$/, /node_modules/] 64 | } 65 | ] 66 | }, 67 | plugins: [ 68 | new webpack.DllReferencePlugin({ 69 | context: __dirname, 70 | manifest: require(rootPath('wwwroot', 'dist', 'vendor-manifest.json')) 71 | }), 72 | new ContextReplacementPlugin( 73 | /** 74 | * The (\\|\/) piece accounts for path separators in *nix and Windows 75 | */ 76 | /angular(\\|\/)core(\\|\/)@angular/, 77 | rootPath('ClientApp'), // location of your src 78 | { 79 | /** 80 | * your Angular Async Route paths relative to this root directory 81 | */ 82 | } 83 | ), 84 | new LoaderOptionsPlugin({ 85 | debug: false, 86 | options: { 87 | /** 88 | * legacy options go here 89 | */ 90 | } 91 | }) 92 | ], 93 | performance: { 94 | hints: false 95 | }, 96 | 97 | /** 98 | * Include polyfills or mocks for various node stuff 99 | * Description: Node configuration 100 | * 101 | * See: https://webpack.github.io/docs/configuration.html#node 102 | */ 103 | node: { 104 | global: true, 105 | process: false, 106 | crypto: 'empty', 107 | module: false, 108 | clearImmediate: false, 109 | setImmediate: false 110 | } 111 | }; 112 | }; 113 | -------------------------------------------------------------------------------- /Asp2017.csproj: -------------------------------------------------------------------------------- 1 | <Project Sdk="Microsoft.NET.Sdk.Web"> 2 | <PropertyGroup> 3 | <TargetFramework>netcoreapp2.1</TargetFramework> 4 | <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> 5 | <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion> 6 | <IsPackable>false</IsPackable> 7 | <DockerTargetOS>Linux</DockerTargetOS> 8 | <DockerComposeProjectPath>docker-compose.dcproj</DockerComposeProjectPath> 9 | </PropertyGroup> 10 | <ItemGroup> 11 | <!-- New Meta Package has SpaServices in It --> 12 | <PackageReference Include="Microsoft.AspNetCore.All" /> 13 | <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="0.1.1646902" /> 14 | <PackageReference Include="NETStandard.Library" Version="2.0.3" /> 15 | <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.1" /> 16 | <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.1" /> 17 | <PackageReference Include="Swashbuckle.AspNetCore" Version="2.4.0" /> 18 | <PackageReference Include="WebEssentials.AspNetCore.PWA" Version="1.0.33" /> 19 | </ItemGroup> 20 | 21 | <ItemGroup> 22 | <!-- Files not to show in IDE --> 23 | <None Remove="yarn.lock" /> 24 | <Content Remove="wwwroot\dist\**" /> 25 | <None Remove="ClientApp\dist\**" /> 26 | <Content Remove="coverage\**" /> 27 | 28 | <!-- Files not to publish (note that the 'dist' subfolders are re-added below) --> 29 | <Content Remove="ClientApp\**" /> 30 | </ItemGroup> 31 | <Target Name="DebugRunWebpack" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('wwwroot\dist') "> 32 | <!-- Ensure Node.js is installed --> 33 | <Exec Command="node --version" ContinueOnError="true"> 34 | <Output TaskParameter="ExitCode" PropertyName="ErrorCode" /> 35 | </Exec> 36 | <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." /> 37 | 38 | <!-- In development, the dist files won't exist on the first run or when cloning to 39 | a different machine, so rebuild them if not already present. --> 40 | <Message Importance="high" Text="Performing first-run Webpack build..." /> 41 | <Exec Command="npm install" /> 42 | <Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js" /> 43 | <Exec Command="node node_modules/webpack/bin/webpack.js" /> 44 | </Target> 45 | <Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish"> 46 | <!-- As part of publishing, ensure the JS resources are freshly built in production mode --> 47 | <Exec Command="npm install" /> 48 | <Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" /> 49 | <Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" /> 50 | 51 | <!-- Include the newly-built files in the publish output --> 52 | <ItemGroup> 53 | <DistFiles Include="wwwroot\dist\**; ClientApp\dist\**" /> 54 | <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)"> 55 | <RelativePath>%(DistFiles.Identity)</RelativePath> 56 | <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> 57 | </ResolvedFileToPublish> 58 | </ItemGroup> 59 | </Target> 60 | <Target Name="CleanDist" AfterTargets="Clean"> 61 | <ItemGroup> 62 | <FilesToDelete Include="ClientApp\dist\**; wwwroot\dist\**" /> 63 | </ItemGroup> 64 | <Delete Files="@(FilesToDelete)" /> 65 | <RemoveDir Directories="ClientApp\dist; wwwroot\dist" /> 66 | </Target> 67 | </Project> 68 | -------------------------------------------------------------------------------- /ClientApp/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Injector, 4 | OnDestroy, 5 | OnInit, 6 | ViewEncapsulation 7 | } from '@angular/core'; 8 | import { Meta, Title } from '@angular/platform-browser'; 9 | import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; 10 | import { REQUEST } from '@nguniversal/aspnetcore-engine/tokens'; 11 | // i18n support 12 | import { TranslateService } from '@ngx-translate/core'; 13 | import { Subscription } from 'rxjs'; 14 | import { filter, map, mergeMap } from 'rxjs/operators'; 15 | import { LinkService } from './shared/link.service'; 16 | 17 | @Component({ 18 | selector: 'app-root', 19 | templateUrl: './app.component.html', 20 | styleUrls: ['./app.component.scss'], 21 | encapsulation: ViewEncapsulation.None 22 | }) 23 | export class AppComponent implements OnInit, OnDestroy { 24 | // This will go at the END of your title for example "Home - Angular Universal..." <-- after the dash (-) 25 | private endPageTitle: string = 'Angular Universal and ASP.NET Core Starter'; 26 | // If no Title is provided, we'll use a default one before the dash(-) 27 | private defaultPageTitle: string = 'My App'; 28 | 29 | private routerSub$: Subscription; 30 | private request; 31 | 32 | constructor( 33 | private router: Router, 34 | private activatedRoute: ActivatedRoute, 35 | private title: Title, 36 | private meta: Meta, 37 | private linkService: LinkService, 38 | public translate: TranslateService, 39 | private injector: Injector 40 | ) { 41 | // this language will be used as a fallback when a translation isn't found in the current language 42 | translate.setDefaultLang('en'); 43 | 44 | // the lang to use, if the lang isn't available, it will use the current loader to get them 45 | translate.use('en'); 46 | 47 | this.request = this.injector.get(REQUEST); 48 | 49 | console.log(`What's our REQUEST Object look like?`); 50 | console.log( 51 | `The Request object only really exists on the Server, but on the Browser we can at least see Cookies` 52 | ); 53 | console.log(this.request); 54 | } 55 | 56 | ngOnInit() { 57 | // Change "Title" on every navigationEnd event 58 | // Titles come from the data.title property on all Routes (see app.routes.ts) 59 | this._changeTitleOnNavigation(); 60 | } 61 | 62 | ngOnDestroy() { 63 | // Subscription clean-up 64 | this.routerSub$.unsubscribe(); 65 | } 66 | 67 | private _changeTitleOnNavigation() { 68 | this.routerSub$ = this.router.events 69 | .pipe( 70 | filter(event => event instanceof NavigationEnd), 71 | map(() => this.activatedRoute), 72 | map(route => { 73 | while (route.firstChild) route = route.firstChild; 74 | return route; 75 | }), 76 | filter(route => route.outlet === 'primary'), 77 | mergeMap(route => route.data) 78 | ) 79 | .subscribe(event => { 80 | this._setMetaAndLinks(event); 81 | }); 82 | } 83 | 84 | private _setMetaAndLinks(event) { 85 | // Set Title if available, otherwise leave the default Title 86 | const title = event['title'] 87 | ? `${event['title']} - ${this.endPageTitle}` 88 | : `${this.defaultPageTitle} - ${this.endPageTitle}`; 89 | 90 | this.title.setTitle(title); 91 | 92 | const metaData = event['meta'] || []; 93 | const linksData = event['links'] || []; 94 | 95 | for (let i = 0; i < metaData.length; i++) { 96 | this.meta.updateTag(metaData[i]); 97 | } 98 | 99 | for (let i = 0; i < linksData.length; i++) { 100 | this.linkService.addTag(linksData[i]); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "compounds": [{ 4 | "name": "[Development] Debug Server & Client", 5 | "configurations": ["[Development] Launch Server (no browser)", "[Development] Debug TypeScript"] 6 | }], 7 | "configurations": [{ 8 | 9 | "name": "[Development] Debug TypeScript", 10 | "type": "chrome", 11 | "request": "launch", 12 | "url": "http://localhost:5000", 13 | "webRoot": "${workspaceRoot}/wwwroot", 14 | "sourceMapPathOverrides": { 15 | "webpack:///./*": "${workspaceRoot}\\*" 16 | } 17 | }, 18 | { 19 | "name": "[Development] Launch Server (no browser)", 20 | "type": "coreclr", 21 | "request": "launch", 22 | "preLaunchTask": "build", 23 | "program": "${workspaceRoot}/bin/Debug/netcoreapp2.1/Asp2017.dll", 24 | "args": [], 25 | "cwd": "${workspaceRoot}", 26 | "stopAtEntry": false, 27 | "internalConsoleOptions": "openOnSessionStart", 28 | "launchBrowser": { 29 | "enabled": false, 30 | "args": "${auto-detect-url}", 31 | "windows": { 32 | "command": "cmd.exe", 33 | "args": "/C start ${auto-detect-url}" 34 | }, 35 | "osx": { 36 | "command": "open" 37 | }, 38 | "linux": { 39 | "command": "xdg-open" 40 | } 41 | }, 42 | "env": { 43 | "ASPNETCORE_ENVIRONMENT": "Development" 44 | }, 45 | "sourceFileMap": { 46 | "/Views": "${workspaceRoot}/Views" 47 | } 48 | }, 49 | { 50 | "name": "[Development] Launch Web", 51 | "type": "coreclr", 52 | "request": "launch", 53 | "preLaunchTask": "build", 54 | "program": "${workspaceRoot}/bin/Debug/netcoreapp2.1/Asp2017.dll", 55 | "args": [], 56 | "cwd": "${workspaceRoot}", 57 | "stopAtEntry": false, 58 | "internalConsoleOptions": "openOnSessionStart", 59 | "launchBrowser": { 60 | "enabled": true, 61 | "args": "${auto-detect-url}", 62 | "windows": { 63 | "command": "cmd.exe", 64 | "args": "/C start ${auto-detect-url}" 65 | }, 66 | "osx": { 67 | "command": "open" 68 | }, 69 | "linux": { 70 | "command": "xdg-open" 71 | } 72 | }, 73 | "env": { 74 | "ASPNETCORE_ENVIRONMENT": "Development" 75 | }, 76 | "sourceFileMap": { 77 | "/Views": "${workspaceRoot}/Views" 78 | } 79 | }, 80 | { 81 | "name": "[Production] Launch Web", 82 | "type": "coreclr", 83 | "request": "launch", 84 | "preLaunchTask": "build", 85 | "program": "${workspaceRoot}/bin/Debug/netcoreapp2.1/Asp2017.dll", 86 | "args": [], 87 | "cwd": "${workspaceRoot}", 88 | "stopAtEntry": false, 89 | "internalConsoleOptions": "openOnSessionStart", 90 | "launchBrowser": { 91 | "enabled": true, 92 | "args": "${auto-detect-url}", 93 | "windows": { 94 | "command": "cmd.exe", 95 | "args": "/C start ${auto-detect-url}" 96 | }, 97 | "osx": { 98 | "command": "open" 99 | }, 100 | "linux": { 101 | "command": "xdg-open" 102 | } 103 | }, 104 | "env": { 105 | "ASPNETCORE_ENVIRONMENT": "Production" 106 | }, 107 | "sourceFileMap": { 108 | "/Views": "${workspaceRoot}/src/AspCoreServer/Views" 109 | } 110 | }, 111 | { 112 | "name": ".NET Core Attach", 113 | "type": "coreclr", 114 | "request": "attach", 115 | "processId": "${command:pickProcess}" 116 | } 117 | ] 118 | } 119 | -------------------------------------------------------------------------------- /Server/RestAPI/UsersController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using AspCoreServer.Data; 5 | using AspCoreServer.Models; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | namespace AspCoreServer.Controllers { 10 | [Route ("api/[controller]")] 11 | public class UsersController : Controller { 12 | private readonly SpaDbContext context; 13 | 14 | public UsersController(SpaDbContext context) => this.context = context; 15 | 16 | [HttpGet] 17 | public async Task<IActionResult> Get (int currentPageNo = 1, int pageSize = 20) { 18 | var users = await this.context.User 19 | .OrderByDescending (u => u.EntryTime) 20 | .Skip ((currentPageNo - 1) * pageSize) 21 | .Take (pageSize) 22 | .ToArrayAsync (); 23 | 24 | if (!users.Any ()) { 25 | return NotFound ("Users not Found"); 26 | } else { 27 | return Ok (users); 28 | } 29 | } 30 | 31 | [HttpGet ("{id}")] 32 | public async Task<IActionResult> Get (int id) { 33 | var user = await this.context.User 34 | .Where (u => u.ID == id) 35 | .AsNoTracking () 36 | .SingleOrDefaultAsync (m => m.ID == id); 37 | 38 | if (user == null) { 39 | return NotFound ("User not Found"); 40 | } else { 41 | return Ok (user); 42 | } 43 | } 44 | 45 | [HttpPost] 46 | public async Task<IActionResult> Post ([FromBody] User user) { 47 | if (!string.IsNullOrEmpty (user.Name)) { 48 | this.context.Add (user); 49 | await this.context.SaveChangesAsync (); 50 | return CreatedAtAction ("Post", user); 51 | } else { 52 | return BadRequest ("User's name was not given"); 53 | } 54 | } 55 | 56 | [HttpPut ("{id}")] 57 | public async Task<IActionResult> Put (int id, [FromBody] User userUpdateValue) { 58 | try { 59 | userUpdateValue.EntryTime = DateTime.Now; 60 | 61 | var userToEdit = await context.User 62 | .AsNoTracking () 63 | .SingleOrDefaultAsync (m => m.ID == id); 64 | 65 | if (userToEdit == null) { 66 | return NotFound ("Could not update user as it was not Found"); 67 | } else { 68 | this.context.Update (userUpdateValue); 69 | await this.context.SaveChangesAsync (); 70 | return Json ("Updated user - " + userUpdateValue.Name); 71 | } 72 | } catch (DbUpdateException) { 73 | //Log the error (uncomment ex variable name and write a log.) 74 | this.ModelState.AddModelError ("", "Unable to save changes. " + 75 | "Try again, and if the problem persists, " + 76 | "see your system administrator."); 77 | return NotFound ("User not Found"); 78 | } 79 | } 80 | 81 | [HttpDelete ("{id}")] 82 | public async Task<IActionResult> Delete (int id) { 83 | var userToRemove = await this.context.User 84 | .AsNoTracking () 85 | .SingleOrDefaultAsync (m => m.ID == id); 86 | if (userToRemove == null) { 87 | return NotFound ("Could not delete user as it was not Found"); 88 | } else { 89 | this.context.User.Remove (userToRemove); 90 | await this.context.SaveChangesAsync (); 91 | return Json ("Deleted user - " + userToRemove.Name); 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "AspnetCore-Angular-Universal": { 7 | "root": "", 8 | "sourceRoot": "ClientApp", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "wwwroot/dist", 17 | "main": "ClientApp/boot-browser.ts", 18 | "polyfills": "ClientApp/polyfills/polyfills.ts", 19 | "tsConfig": "ClientApp/tsconfig.app.json", 20 | "assets": [], 21 | "styles": [], 22 | "scripts": [] 23 | }, 24 | "configurations": { 25 | "production": { 26 | "fileReplacements": [ 27 | { 28 | "replace": "ClientApp/environments/environment.ts", 29 | "with": "ClientApp/environments/environment.prod.ts" 30 | } 31 | ], 32 | "optimization": true, 33 | "outputHashing": "all", 34 | "sourceMap": false, 35 | "extractCss": true, 36 | "namedChunks": false, 37 | "aot": true, 38 | "extractLicenses": true, 39 | "vendorChunk": false, 40 | "buildOptimizer": true 41 | } 42 | } 43 | }, 44 | "serve": { 45 | "builder": "@angular-devkit/build-angular:dev-server", 46 | "options": { 47 | "browserTarget": "AspnetCore-Angular-Universal:build" 48 | }, 49 | "configurations": { 50 | "production": { 51 | "browserTarget": "AspnetCore-Angular-Universal:build:production" 52 | } 53 | } 54 | }, 55 | "extract-i18n": { 56 | "builder": "@angular-devkit/build-angular:extract-i18n", 57 | "options": { 58 | "browserTarget": "AspnetCore-Angular-Universal:build" 59 | } 60 | }, 61 | "test": { 62 | "builder": "@angular-devkit/build-angular:karma", 63 | "options": { 64 | "main": "ClientApp/test.ts", 65 | "polyfills": "ClientApp/polyfills.ts", 66 | "tsConfig": "ClientApp/tsconfig.spec.json", 67 | "karmaConfig": "ClientApp/karma.conf.js", 68 | "styles": ["ClientApp/styles.css"], 69 | "scripts": [], 70 | "assets": ["ClientApp/favicon.ico", "ClientApp/assets"] 71 | } 72 | }, 73 | "lint": { 74 | "builder": "@angular-devkit/build-angular:tslint", 75 | "options": { 76 | "tsConfig": [ 77 | "ClientApp/tsconfig.app.json", 78 | "ClientApp/tsconfig.spec.json" 79 | ], 80 | "exclude": ["**/node_modules/**"] 81 | } 82 | } 83 | } 84 | }, 85 | "AspnetCore-Angular-Universal-e2e": { 86 | "root": "e2e/", 87 | "projectType": "application", 88 | "architect": { 89 | "e2e": { 90 | "builder": "@angular-devkit/build-angular:protractor", 91 | "options": { 92 | "protractorConfig": "e2e/protractor.conf.js", 93 | "devServerTarget": "AspnetCore-Angular-Universal:serve" 94 | } 95 | }, 96 | "lint": { 97 | "builder": "@angular-devkit/build-angular:tslint", 98 | "options": { 99 | "tsConfig": "e2e/tsconfig.e2e.json", 100 | "exclude": ["**/node_modules/**"] 101 | } 102 | } 103 | } 104 | } 105 | }, 106 | "defaultProject": "AspnetCore-Angular-Universal" 107 | } 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular7-aspnetcore-universal", 3 | "author": { 4 | "name": "Mark Pieszak | Trilon Consulting", 5 | "email": "hello@trilon.io", 6 | "url": "https://trilon.io" 7 | }, 8 | "version": "1.0.0-rc4", 9 | "scripts": { 10 | "clean:install": "npm run clean && rimraf ./node_modules ./bin ./obj ./package-lock.json && dotnet restore Asp2017.csproj && npm i", 11 | "lint": "tslint -p tsconfig.json", 12 | "test": "npm run build:vendor && karma start ClientApp/test/karma.conf.js", 13 | "test:watch": "npm run test -- --auto-watch --no-single-run", 14 | "test:ci": "npm run test -- --browsers PhantomJS_custom", 15 | "test:ci:watch": "npm run test:ci -- --auto-watch --no-single-run", 16 | "test:coverage": "npm run test -- --coverage", 17 | "build:dev": "npm run build:vendor && npm run build:webpack", 18 | "build:webpack": "webpack --progress --color", 19 | "build:prod": "npm run clean && npm run build:vendor -- --env.prod && npm run build:webpack -- --env.prod", 20 | "build:p": "npm run build:webpack -- --env.prod", 21 | "build:vendor": "webpack --config webpack.config.vendor.js --progress --color", 22 | "clean": "rimraf wwwroot/dist clientapp/dist" 23 | }, 24 | "dependencies": { 25 | "@angular/animations": "~7.2.0", 26 | "@angular/common": "~7.2.0", 27 | "@angular/compiler": "~7.2.0", 28 | "@angular/core": "~7.2.0", 29 | "@angular/forms": "~7.2.0", 30 | "@angular/http": "~7.2.0", 31 | "@angular/platform-browser": "~7.2.0", 32 | "@angular/platform-browser-dynamic": "~7.2.0", 33 | "@angular/platform-server": "~7.2.0", 34 | "@angular/router": "~7.2.0", 35 | "@nguniversal/aspnetcore-engine": "^7.1.0", 36 | "@nguniversal/common": "^7.1.0", 37 | "@ngx-translate/core": "^11.0.1", 38 | "@ngx-translate/http-loader": "^4.0.0", 39 | "@types/node": "^11.9.5", 40 | "angular2-router-loader": "^0.3.5", 41 | "angular2-template-loader": "^0.6.2", 42 | "aspnet-prerendering": "^3.0.1", 43 | "aspnet-webpack": "^3.0.0", 44 | "awesome-typescript-loader": "^5.2.1", 45 | "bootstrap": "^4.3.1", 46 | "core-js": "^2.6.5", 47 | "css": "^2.2.4", 48 | "css-loader": "^2.1.0", 49 | "event-source-polyfill": "^1.0.5", 50 | "expose-loader": "^0.7.5", 51 | "file-loader": "^3.0.1", 52 | "html-loader": "^0.5.5", 53 | "isomorphic-fetch": "^2.2.1", 54 | "jquery": "^3.3.1", 55 | "json-loader": "^0.5.7", 56 | "moment": "^2.24.0", 57 | "ngx-bootstrap": "^3.2.0", 58 | "node-sass": "^4.11.0", 59 | "preboot": "^7.0.0", 60 | "raw-loader": "^1.0.0", 61 | "rimraf": "^2.6.3", 62 | "rxjs": "6.2.2", 63 | "sass-loader": "^7.1.0", 64 | "style-loader": "^0.23.1", 65 | "to-string-loader": "^1.1.5", 66 | "url-loader": "^1.1.2", 67 | "webpack": "^4.29.5", 68 | "webpack-hot-middleware": "^2.24.3", 69 | "webpack-merge": "^4.2.1", 70 | "zone.js": "^0.8.29" 71 | }, 72 | "devDependencies": { 73 | "@angular-devkit/build-angular": "~0.13.3", 74 | "@angular/cli": "~7.3.3", 75 | "@angular/compiler-cli": "~7.2.0", 76 | "@ngtools/webpack": "~7.3.3", 77 | "@types/jasmine": "~2.8.8", 78 | "codelyzer": "~4.5.0", 79 | "istanbul-instrumenter-loader": "^3.0.1", 80 | "jasmine-core": "^3.3.0", 81 | "jasmine-spec-reporter": "^4.2.1", 82 | "karma": "~4.0.0", 83 | "karma-chrome-launcher": "~2.2.0", 84 | "karma-coverage": "~1.1.2", 85 | "karma-jasmine": "~2.0.1", 86 | "karma-mocha-reporter": "^2.2.5", 87 | "karma-phantomjs-launcher": "^1.0.4", 88 | "karma-remap-coverage": "^0.1.5", 89 | "karma-sourcemap-loader": "^0.3.7", 90 | "karma-webpack": "^3.0.5", 91 | "mini-css-extract-plugin": "^0.5.0", 92 | "terser-webpack-plugin": "^1.2.3", 93 | "tslint": "~5.11.0", 94 | "typescript": "~3.2.2", 95 | "uglifyjs-webpack-plugin": "^2.1.2", 96 | "webpack-bundle-analyzer": "^3.0.4", 97 | "webpack-cli": "^3.2.3" 98 | }, 99 | "license": "MIT", 100 | "repository": { 101 | "type": "github", 102 | "url": "https://github.com/MarkPieszak/aspnetcore-angular2-universal" 103 | }, 104 | "readme": "https://github.com/MarkPieszak/aspnetcore-angular2-universal/blob/master/README.md" 105 | } 106 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warn", 3 | "rules": { 4 | "align": false, 5 | "ban": false, 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "component-class-suffix": true, 12 | "component-selector": [ 13 | true, 14 | "element", 15 | "app", 16 | "kebab-case" 17 | ], 18 | "curly": false, 19 | "directive-class-suffix": true, 20 | "directive-selector": [ 21 | true, 22 | "attribute", 23 | "app", 24 | "camelCase" 25 | ], 26 | "eofline": true, 27 | "forin": true, 28 | "import-destructuring-spacing": true, 29 | "indent": [ 30 | true, 31 | "spaces" 32 | ], 33 | "interface-name": false, 34 | "jsdoc-format": true, 35 | "label-position": true, 36 | "max-line-length": [ 37 | true, 38 | 200 39 | ], 40 | "member-access": false, 41 | "member-ordering": [ 42 | true, 43 | "public-before-private", 44 | "static-before-instance", 45 | "variables-before-functions" 46 | ], 47 | "no-any": false, 48 | "no-arg": true, 49 | "no-attribute-parameter-decorator": true, 50 | "no-bitwise": true, 51 | "no-conditional-assignment": true, 52 | "no-consecutive-blank-lines": false, 53 | "no-console": [ 54 | true, 55 | "debug", 56 | "info", 57 | "time", 58 | "timeEnd", 59 | "trace" 60 | ], 61 | "no-construct": true, 62 | "no-constructor-vars": false, 63 | "no-debugger": true, 64 | "no-duplicate-variable": true, 65 | "no-empty": false, 66 | "no-eval": true, 67 | "no-forward-ref": true, 68 | "no-inferrable-types": false, 69 | "no-input-rename": true, 70 | "no-internal-module": true, 71 | "no-null-keyword": true, 72 | "no-output-rename": true, 73 | "no-require-imports": false, 74 | "no-shadowed-variable": false, 75 | "no-string-literal": false, 76 | "no-switch-case-fall-through": true, 77 | "no-trailing-whitespace": false, 78 | "no-unused-expression": true, 79 | "no-unused-variable": true, 80 | "no-use-before-declare": true, 81 | "no-var-keyword": true, 82 | "no-var-requires": false, 83 | "object-literal-sort-keys": false, 84 | "one-line": [ 85 | true, 86 | "check-open-brace", 87 | "check-catch", 88 | "check-else", 89 | "check-finally", 90 | "check-whitespace" 91 | ], 92 | "pipe-naming": [ 93 | true, 94 | "camelCase", 95 | "app" 96 | ], 97 | "quotemark": [ 98 | true, 99 | "single", 100 | "avoid-escape" 101 | ], 102 | "radix": true, 103 | "semicolon": [ 104 | true, 105 | "always" 106 | ], 107 | "switch-default": true, 108 | "trailing-comma": [ 109 | true, 110 | { 111 | "multiline": "never", 112 | "singleline": "never" 113 | } 114 | ], 115 | "triple-equals": [ 116 | true, 117 | "allow-null-check" 118 | ], 119 | "typedef": false, 120 | "typedef-whitespace": [ 121 | true, 122 | { 123 | "call-signature": "nospace", 124 | "index-signature": "nospace", 125 | "parameter": "nospace", 126 | "property-declaration": "nospace", 127 | "variable-declaration": "nospace" 128 | }, 129 | { 130 | "call-signature": "space", 131 | "index-signature": "space", 132 | "parameter": "space", 133 | "property-declaration": "space", 134 | "variable-declaration": "space" 135 | } 136 | ], 137 | "use-host-property-decorator": true, 138 | "use-input-property-decorator": true, 139 | "use-life-cycle-interface": true, 140 | "use-output-property-decorator": true, 141 | "use-pipe-transform-interface": true, 142 | "variable-name": [ 143 | true, 144 | "check-format", 145 | "allow-leading-underscore", 146 | "ban-keywords" 147 | ], 148 | "whitespace": [ 149 | true, 150 | "check-branch", 151 | "check-decl", 152 | "check-operator", 153 | "check-separator", 154 | "check-type" 155 | ] 156 | }, 157 | "rulesDirectory": [ 158 | "node_modules/codelyzer" 159 | ] 160 | } 161 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /Properties/launchSettings.json 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # yarn 16 | yarn.lock 17 | 18 | npm-debug.log.* 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | build/ 28 | bld/ 29 | bin/ 30 | Bin/ 31 | obj/ 32 | Obj/ 33 | 34 | # Visual Studio 2015 cache/options directory 35 | .vs/ 36 | 37 | /wwwroot/dist/ 38 | /ClientApp/dist/ 39 | 40 | # MSTest test Results 41 | [Tt]est[Rr]esult*/ 42 | [Bb]uild[Ll]og.* 43 | 44 | # NUNIT 45 | *.VisualState.xml 46 | TestResult.xml 47 | 48 | # Build Results of an ATL Project 49 | [Dd]ebugPS/ 50 | [Rr]eleasePS/ 51 | dlldata.c 52 | 53 | # DNX 54 | project.lock.json 55 | artifacts/ 56 | 57 | *_i.c 58 | *_p.c 59 | *_i.h 60 | *.ilk 61 | *.meta 62 | *.obj 63 | *.pch 64 | *.pdb 65 | *.pgc 66 | *.pgd 67 | *.rsp 68 | *.sbr 69 | *.tlb 70 | *.tli 71 | *.tlh 72 | *.tmp 73 | *.tmp_proj 74 | *.log 75 | *.vspscc 76 | *.vssscc 77 | .builds 78 | *.pidb 79 | *.svclog 80 | *.scc 81 | 82 | # Chutzpah Test files 83 | _Chutzpah* 84 | 85 | # Visual C++ cache files 86 | ipch/ 87 | *.aps 88 | *.ncb 89 | *.opendb 90 | *.opensdf 91 | *.sdf 92 | *.cachefile 93 | 94 | # Visual Studio profiler 95 | *.psess 96 | *.vsp 97 | *.vspx 98 | *.sap 99 | 100 | # TFS 2012 Local Workspace 101 | $tf/ 102 | 103 | # Guidance Automation Toolkit 104 | *.gpState 105 | 106 | # ReSharper is a .NET coding add-in 107 | _ReSharper*/ 108 | *.[Rr]e[Ss]harper 109 | *.DotSettings.user 110 | 111 | # JustCode is a .NET coding add-in 112 | .JustCode 113 | 114 | # TeamCity is a build add-in 115 | _TeamCity* 116 | 117 | # DotCover is a Code Coverage Tool 118 | *.dotCover 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # Typedocs generated content 136 | doc/ 137 | 138 | # DocProject is a documentation generator add-in 139 | DocProject/buildhelp/ 140 | DocProject/Help/*.HxT 141 | DocProject/Help/*.HxC 142 | DocProject/Help/*.hhc 143 | DocProject/Help/*.hhk 144 | DocProject/Help/*.hhp 145 | DocProject/Help/Html2 146 | DocProject/Help/html 147 | 148 | # Click-Once directory 149 | publish/ 150 | 151 | # Publish Web Output 152 | *.[Pp]ublish.xml 153 | *.azurePubxml 154 | # TODO: Comment the next line if you want to checkin your web deploy settings 155 | # but database connection strings (with potential passwords) will be unencrypted 156 | *.pubxml 157 | *.publishproj 158 | 159 | # NuGet Packages 160 | *.nupkg 161 | # The packages folder can be ignored because of Package Restore 162 | **/packages/* 163 | # except build/, which is used as an MSBuild target. 164 | !**/packages/build/ 165 | # Uncomment if necessary however generally it will be regenerated when needed 166 | #!**/packages/repositories.config 167 | 168 | # Microsoft Azure Build Output 169 | csx/ 170 | *.build.csdef 171 | 172 | # Microsoft Azure Emulator 173 | ecf/ 174 | rcf/ 175 | 176 | # Microsoft Azure ApplicationInsights config file 177 | ApplicationInsights.config 178 | 179 | # Windows Store app package directory 180 | AppPackages/ 181 | BundleArtifacts/ 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | *.db 191 | ClientBin/ 192 | ~$* 193 | *~ 194 | *.dbmdl 195 | *.dbproj.schemaview 196 | *.pfx 197 | *.publishsettings 198 | orleans.codegen.cs 199 | 200 | # Workaround for https://github.com/aspnet/JavaScriptServices/issues/235 201 | /node_modules 202 | node_modules 203 | node_modules/**/* 204 | node_modules/_placeholder.txt 205 | 206 | # RIA/Silverlight projects 207 | Generated_Code/ 208 | 209 | # Backup & report files from converting an old project file 210 | # to a newer Visual Studio version. Backup files are not needed, 211 | # because we have git ;-) 212 | _UpgradeReport_Files/ 213 | Backup*/ 214 | UpgradeLog*.XML 215 | UpgradeLog*.htm 216 | 217 | # SQL Server files 218 | *.mdf 219 | *.ldf 220 | 221 | # Business Intelligence projects 222 | *.rdl.data 223 | *.bim.layout 224 | *.bim_*.settings 225 | 226 | # Microsoft Fakes 227 | FakesAssemblies/ 228 | 229 | # GhostDoc plugin setting file 230 | *.GhostDoc.xml 231 | 232 | # Node.js Tools for Visual Studio 233 | .ntvs_analysis.dat 234 | 235 | # Visual Studio 6 build log 236 | *.plg 237 | 238 | # Visual Studio 6 workspace options file 239 | *.opt 240 | 241 | # Visual Studio LightSwitch build output 242 | **/*.HTMLClient/GeneratedArtifacts 243 | **/*.DesktopClient/GeneratedArtifacts 244 | **/*.DesktopClient/ModelManifest.xml 245 | **/*.Server/GeneratedArtifacts 246 | **/*.Server/ModelManifest.xml 247 | _Pvt_Extensions 248 | 249 | # Paket dependency manager 250 | .paket/paket.exe 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # Ignore AwesomeTypescript cache 256 | .awcache 257 | 258 | # VSCode files 259 | .vscode/chrome 260 | 261 | # Jest Code Coverage report 262 | coverage/ 263 | 264 | .DS_Store 265 | package-lock.json 266 | -------------------------------------------------------------------------------- /Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using AspCoreServer.Data; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.SpaServices.Webpack; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Net.Http.Headers; 13 | using Swashbuckle.AspNetCore.Swagger; 14 | using WebEssentials.AspNetCore.Pwa; 15 | 16 | namespace AspCoreServer { 17 | public class Startup { 18 | public Startup (IHostingEnvironment env) { 19 | var builder = new ConfigurationBuilder () 20 | .SetBasePath (env.ContentRootPath) 21 | .AddJsonFile ("appsettings.json", optional : true, reloadOnChange : true) 22 | .AddJsonFile ($"appsettings.{env.EnvironmentName}.json", optional : true) 23 | .AddEnvironmentVariables (); 24 | this.Configuration = builder.Build (); 25 | } 26 | 27 | public IConfigurationRoot Configuration { get; } 28 | 29 | // This method gets called by the runtime. Use this method to add services to the container. 30 | public void ConfigureServices (IServiceCollection services) { 31 | // Add framework services. 32 | services.AddMvc (); 33 | services.AddNodeServices (); 34 | services.AddHttpContextAccessor (); 35 | services.AddProgressiveWebApp (new PwaOptions { Strategy = ServiceWorkerStrategy.CacheFirst, RegisterServiceWorker = true, RegisterWebmanifest = true }, "manifest.json"); 36 | 37 | var connectionStringBuilder = new Microsoft.Data.Sqlite.SqliteConnectionStringBuilder { DataSource = "spa.db" }; 38 | var connectionString = connectionStringBuilder.ToString(); 39 | 40 | services.AddDbContext<SpaDbContext>(options => 41 | options.UseSqlite(connectionString)); 42 | 43 | // Register the Swagger generator, defining one or more Swagger documents 44 | services.AddSwaggerGen (c => { 45 | c.SwaggerDoc ("v1", new Info { Title = "Angular 7.0 Universal & ASP.NET Core advanced starter-kit web API", Version = "v1" }); 46 | }); 47 | } 48 | 49 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 50 | public void Configure (IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, SpaDbContext context) { 51 | loggerFactory.AddConsole (this.Configuration.GetSection ("Logging")); 52 | loggerFactory.AddDebug (); 53 | 54 | // app.UseStaticFiles(); 55 | 56 | app.UseStaticFiles (new StaticFileOptions () { 57 | OnPrepareResponse = c => { 58 | //Do not add cache to json files. We need to have new versions when we add new translations. 59 | c.Context.Response.GetTypedHeaders ().CacheControl = !c.Context.Request.Path.Value.Contains (".json") 60 | ? new CacheControlHeaderValue () { 61 | MaxAge = TimeSpan.FromDays (30) // Cache everything except json for 30 days 62 | } 63 | : new CacheControlHeaderValue () { 64 | MaxAge = TimeSpan.FromMinutes (15) // Cache json for 15 minutes 65 | }; 66 | } 67 | }); 68 | 69 | if (env.IsDevelopment ()) { 70 | app.UseDeveloperExceptionPage (); 71 | app.UseWebpackDevMiddleware (new WebpackDevMiddlewareOptions { 72 | HotModuleReplacement = true, 73 | HotModuleReplacementEndpoint = "/dist/" 74 | }); 75 | app.UseSwagger (); 76 | app.UseSwaggerUI (c => { 77 | c.SwaggerEndpoint ("/swagger/v1/swagger.json", "My API V1"); 78 | }); 79 | 80 | // Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint. 81 | 82 | app.MapWhen (x => !x.Request.Path.Value.StartsWith ("/swagger", StringComparison.OrdinalIgnoreCase), builder => { 83 | builder.UseMvc (routes => { 84 | routes.MapSpaFallbackRoute ( 85 | name: "spa-fallback", 86 | defaults : new { controller = "Home", action = "Index" }); 87 | }); 88 | }); 89 | } else { 90 | app.UseMvc (routes => { 91 | routes.MapRoute ( 92 | name: "default", 93 | template: "{controller=Home}/{action=Index}/{id?}"); 94 | 95 | routes.MapRoute ( 96 | "Sitemap", 97 | "sitemap.xml", 98 | new { controller = "Home", action = "SitemapXml" }); 99 | 100 | routes.MapSpaFallbackRoute ( 101 | name: "spa-fallback", 102 | defaults : new { controller = "Home", action = "Index" }); 103 | 104 | }); 105 | app.UseExceptionHandler ("/Home/Error"); 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /webpack.config.vendor.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const merge = require('webpack-merge'); 5 | const TerserPlugin = require('terser-webpack-plugin'); 6 | const treeShakableModules = [ 7 | '@angular/animations', 8 | '@angular/common', 9 | '@angular/compiler', 10 | '@angular/core', 11 | '@angular/forms', 12 | '@angular/http', 13 | '@angular/platform-browser', 14 | '@angular/platform-browser-dynamic', 15 | '@angular/router', 16 | 'ngx-bootstrap', 17 | 'zone.js', 18 | ]; 19 | const nonTreeShakableModules = [ 20 | // 'bootstrap', 21 | // 'bootstrap/dist/css/bootstrap.css', 22 | 'core-js', 23 | // 'es6-promise', 24 | // 'es6-shim', 25 | 'event-source-polyfill', 26 | // 'jquery', 27 | ]; 28 | 29 | const allModules = treeShakableModules.concat(nonTreeShakableModules); 30 | 31 | module.exports = (env) => { 32 | console.log(`env = ${JSON.stringify(env)}`) 33 | const extractCSS = new MiniCssExtractPlugin({ 34 | // Options similar to the same options in webpackOptions.output 35 | // both options are optional 36 | filename: "[name].css", 37 | chunkFilename: "[id].css" 38 | }); 39 | const isDevBuild = !(env && env.prod); 40 | const sharedConfig = { 41 | mode: isDevBuild ? "development" : "production", 42 | stats: { 43 | modules: false 44 | }, 45 | resolve: { 46 | extensions: ['.js'] 47 | }, 48 | module: { 49 | rules: [{ 50 | test: /\.(png|woff|woff2|eot|ttf|svg)(\?|$)/, 51 | use: 'url-loader?limit=100000' 52 | }] 53 | }, 54 | output: { 55 | publicPath: 'dist/', 56 | filename: '[name].js', 57 | library: '[name]_[hash]' 58 | }, 59 | plugins: [ 60 | // new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' }), // Maps these identifiers to the jQuery package (because Bootstrap expects it to be a global variable) 61 | new webpack.ContextReplacementPlugin(/\@angular\b.*\b(bundles|linker)/, path.join(__dirname, './ClientApp')), // Workaround for https://github.com/angular/angular/issues/11580 62 | new webpack.ContextReplacementPlugin(/(.+)?angular(\\|\/)core(.+)?/, path.join(__dirname, './ClientApp')), // Workaround for https://github.com/angular/angular/issues/14898 63 | new webpack.IgnorePlugin(/^vertx$/) // Workaround for https://github.com/stefanpenner/es6-promise/issues/100 64 | ] 65 | }; 66 | 67 | const clientBundleConfig = merge(sharedConfig, { 68 | entry: { 69 | // To keep development builds fast, include all vendor dependencies in the vendor bundle. 70 | // But for production builds, leave the tree-shakable ones out so the AOT compiler can produce a smaller bundle. 71 | vendor: isDevBuild ? allModules : nonTreeShakableModules 72 | }, 73 | output: { 74 | path: path.join(__dirname, 'wwwroot', 'dist') 75 | }, 76 | module: { 77 | rules: [{ 78 | test: /\.css(\?|$)/, 79 | use: [ 80 | MiniCssExtractPlugin.loader, 81 | isDevBuild ? 'css-loader' : 'css-loader?minimize' 82 | ] 83 | }] 84 | }, 85 | plugins: [ 86 | new MiniCssExtractPlugin({ 87 | filename: 'vendor.css', 88 | }), 89 | new webpack.DllPlugin({ 90 | path: path.join(__dirname, 'wwwroot', 'dist', '[name]-manifest.json'), 91 | name: '[name]_[hash]' 92 | }) 93 | ].concat(isDevBuild ? [] : [ 94 | 95 | ]), 96 | optimization: { 97 | minimizer: [].concat(isDevBuild ? [] : [ 98 | // we specify a custom TerserPlugin here to get source maps in production 99 | new TerserPlugin({ 100 | cache: true, 101 | parallel: true, 102 | sourceMap: true, 103 | terserOptions: { 104 | compress: false, 105 | ecma: 6, 106 | mangle: true, 107 | keep_classnames: true, 108 | keep_fnames: true, 109 | }, 110 | }) 111 | ]) 112 | } 113 | }); 114 | 115 | const serverBundleConfig = merge(sharedConfig, { 116 | target: 'node', 117 | resolve: { 118 | mainFields: ['main'] 119 | }, 120 | entry: { 121 | vendor: allModules.concat(['aspnet-prerendering']) 122 | }, 123 | output: { 124 | path: path.join(__dirname, 'ClientApp', 'dist'), 125 | libraryTarget: 'commonjs2', 126 | }, 127 | module: { 128 | rules: [{ 129 | test: /\.css(\?|$)/, 130 | use: [ 131 | MiniCssExtractPlugin.loader, 132 | isDevBuild ? 'css-loader' : 'css-loader?minimize' 133 | ] 134 | }] 135 | }, 136 | plugins: [ 137 | new webpack.DllPlugin({ 138 | path: path.join(__dirname, 'ClientApp', 'dist', '[name]-manifest.json'), 139 | name: '[name]_[hash]' 140 | }) 141 | ].concat(isDevBuild ? [] : []), 142 | optimization: { 143 | minimizer: [].concat(isDevBuild ? [] : [ 144 | // we specify a custom TerserPlugin here to get source maps in production 145 | new TerserPlugin({ 146 | cache: true, 147 | parallel: true, 148 | sourceMap: true, 149 | terserOptions: { 150 | compress: false, 151 | ecma: 6, 152 | mangle: true, 153 | keep_classnames: true, 154 | keep_fnames: true, 155 | }, 156 | }) 157 | ]) 158 | } 159 | }); 160 | 161 | return [clientBundleConfig, serverBundleConfig]; 162 | } 163 | -------------------------------------------------------------------------------- /ClientApp/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { HttpClient, HttpClientModule } from '@angular/common/http'; 3 | import { NgModule } from '@angular/core'; 4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 5 | import { 6 | BrowserModule, 7 | BrowserTransferStateModule 8 | } from '@angular/platform-browser'; 9 | import { PreloadAllModules, RouterModule } from '@angular/router'; 10 | import { ORIGIN_URL } from '@nguniversal/aspnetcore-engine/tokens'; 11 | import { TransferHttpCacheModule } from '@nguniversal/common'; 12 | // i18n support 13 | import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; 14 | import { TranslateHttpLoader } from '@ngx-translate/http-loader'; 15 | import { AccordionModule } from 'ngx-bootstrap'; 16 | import { AppComponent } from './app.component'; 17 | import { NavMenuComponent } from './components/navmenu/navmenu.component'; 18 | import { UserDetailComponent } from './components/user-detail/user-detail.component'; 19 | import { CounterComponent } from './containers/counter/counter.component'; 20 | import { HomeComponent } from './containers/home/home.component'; 21 | import { NgxBootstrapComponent } from './containers/ngx-bootstrap-demo/ngx-bootstrap.component'; 22 | import { NotFoundComponent } from './containers/not-found/not-found.component'; 23 | import { UsersComponent } from './containers/users/users.component'; 24 | import { LinkService } from './shared/link.service'; 25 | import { UserService } from './shared/user.service'; 26 | 27 | export function createTranslateLoader(http: HttpClient, baseHref) { 28 | // Temporary Azure hack 29 | if (baseHref === null && typeof window !== 'undefined') { 30 | baseHref = window.location.origin; 31 | } 32 | // i18n files are in `wwwroot/assets/` 33 | return new TranslateHttpLoader(http, `${baseHref}/assets/i18n/`, '.json'); 34 | } 35 | 36 | @NgModule({ 37 | declarations: [ 38 | AppComponent, 39 | NavMenuComponent, 40 | CounterComponent, 41 | UsersComponent, 42 | UserDetailComponent, 43 | HomeComponent, 44 | NotFoundComponent, 45 | NgxBootstrapComponent 46 | ], 47 | imports: [ 48 | CommonModule, 49 | BrowserModule.withServerTransition({ 50 | appId: 'my-app-id' // make sure this matches with your Server NgModule 51 | }), 52 | HttpClientModule, 53 | TransferHttpCacheModule, 54 | BrowserTransferStateModule, 55 | FormsModule, 56 | ReactiveFormsModule, 57 | AccordionModule.forRoot(), // You could also split this up if you don't want the Entire Module imported 58 | 59 | // i18n support 60 | TranslateModule.forRoot({ 61 | loader: { 62 | provide: TranslateLoader, 63 | useFactory: createTranslateLoader, 64 | deps: [HttpClient, [ORIGIN_URL]] 65 | } 66 | }), 67 | 68 | // App Routing 69 | RouterModule.forRoot( 70 | [ 71 | { 72 | path: '', 73 | redirectTo: 'home', 74 | pathMatch: 'full' 75 | }, 76 | { 77 | path: 'home', 78 | component: HomeComponent, 79 | 80 | // *** SEO Magic *** 81 | // We're using "data" in our Routes to pass in our <title> <meta> <link> tag information 82 | // Note: This is only happening for ROOT level Routes, you'd have to add some additional logic if you wanted this for Child level routing 83 | // When you change Routes it will automatically append these to your document for you on the Server-side 84 | // - check out app.component.ts to see how it's doing this 85 | data: { 86 | title: 'Homepage', 87 | meta: [ 88 | { 89 | name: 'description', 90 | content: 'This is an example Description Meta tag!' 91 | } 92 | ], 93 | links: [ 94 | { rel: 'canonical', href: 'http://blogs.example.com/blah/nice' }, 95 | { 96 | rel: 'alternate', 97 | hreflang: 'es', 98 | href: 'http://es.example.com/' 99 | } 100 | ] 101 | } 102 | }, 103 | { 104 | path: 'counter', 105 | component: CounterComponent, 106 | data: { 107 | title: 'Counter', 108 | meta: [ 109 | { 110 | name: 'description', 111 | content: 'This is an Counter page Description!' 112 | } 113 | ], 114 | links: [ 115 | { 116 | rel: 'canonical', 117 | href: 'http://blogs.example.com/counter/something' 118 | }, 119 | { 120 | rel: 'alternate', 121 | hreflang: 'es', 122 | href: 'http://es.example.com/counter' 123 | } 124 | ] 125 | } 126 | }, 127 | { 128 | path: 'users', 129 | component: UsersComponent, 130 | data: { 131 | title: 'Users REST example', 132 | meta: [ 133 | { 134 | name: 'description', 135 | content: 'This is User REST API example page Description!' 136 | } 137 | ], 138 | links: [ 139 | { 140 | rel: 'canonical', 141 | href: 'http://blogs.example.com/chat/something' 142 | }, 143 | { 144 | rel: 'alternate', 145 | hreflang: 'es', 146 | href: 'http://es.example.com/users' 147 | } 148 | ] 149 | } 150 | }, 151 | { 152 | path: 'ngx-bootstrap', 153 | component: NgxBootstrapComponent, 154 | data: { 155 | title: 'Ngx-bootstrap demo!!', 156 | meta: [ 157 | { 158 | name: 'description', 159 | content: 'This is an Demo Bootstrap page Description!' 160 | } 161 | ], 162 | links: [ 163 | { 164 | rel: 'canonical', 165 | href: 'http://blogs.example.com/bootstrap/something' 166 | }, 167 | { 168 | rel: 'alternate', 169 | hreflang: 'es', 170 | href: 'http://es.example.com/bootstrap-demo' 171 | } 172 | ] 173 | } 174 | }, 175 | 176 | { 177 | path: 'lazy', 178 | loadChildren: './containers/lazy/lazy.module#LazyModule' 179 | }, 180 | 181 | { 182 | path: '**', 183 | component: NotFoundComponent, 184 | data: { 185 | title: '404 - Not found', 186 | meta: [{ name: 'description', content: '404 - Error' }], 187 | links: [ 188 | { 189 | rel: 'canonical', 190 | href: 'http://blogs.example.com/bootstrap/something' 191 | }, 192 | { 193 | rel: 'alternate', 194 | hreflang: 'es', 195 | href: 'http://es.example.com/bootstrap-demo' 196 | } 197 | ] 198 | } 199 | } 200 | ], 201 | { 202 | // Router options 203 | useHash: false, 204 | preloadingStrategy: PreloadAllModules, 205 | initialNavigation: 'enabled' 206 | } 207 | ) 208 | ], 209 | providers: [LinkService, UserService, TranslateModule], 210 | bootstrap: [AppComponent] 211 | }) 212 | export class AppModuleShared {} 213 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Webpack (JavaScriptServices) with a few changes & updates 3 | * - This is to keep us inline with JSServices, and help those using that template to add things from this one 4 | * 5 | * Things updated or changed: 6 | * module -> rules [] 7 | * .ts$ test : Added 'angular2-router-loader' for lazy-loading in development 8 | * added ...sharedModuleRules (for scss & font-awesome loaders) 9 | */ 10 | 11 | const path = require('path'); 12 | const webpack = require('webpack'); 13 | const merge = require('webpack-merge'); 14 | const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin; 15 | const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin; 16 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') 17 | .BundleAnalyzerPlugin; 18 | const TerserPlugin = require('terser-webpack-plugin'); 19 | 20 | const { sharedModuleRules } = require('./webpack.additions'); 21 | 22 | module.exports = env => { 23 | // Configuration in common to both client-side and server-side bundles 24 | const isDevBuild = !(env && env.prod); 25 | const sharedConfig = { 26 | mode: isDevBuild ? 'development' : 'production', 27 | stats: { 28 | modules: false 29 | }, 30 | context: __dirname, 31 | resolve: { 32 | extensions: ['.js', '.ts'] 33 | }, 34 | output: { 35 | filename: '[name].js', 36 | publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix 37 | }, 38 | module: { 39 | rules: [ 40 | { 41 | test: /^(?!.*\.spec\.ts$).*\.ts$/, 42 | use: isDevBuild 43 | ? [ 44 | 'awesome-typescript-loader?silent=true', 45 | 'angular2-template-loader', 46 | 'angular2-router-loader' 47 | ] 48 | : '@ngtools/webpack' 49 | }, 50 | { 51 | test: /\.html$/, 52 | use: 'html-loader?minimize=false' 53 | }, 54 | { 55 | test: /\.css$/, 56 | use: [ 57 | 'to-string-loader', 58 | isDevBuild ? 'css-loader' : 'css-loader?minimize' 59 | ] 60 | }, 61 | { 62 | test: /\.(png|jpg|jpeg|gif|svg)$/, 63 | use: 'url-loader?limit=25000' 64 | }, 65 | ...sharedModuleRules 66 | ] 67 | }, 68 | plugins: [new CheckerPlugin()] 69 | }; 70 | 71 | // Configuration for client-side bundle suitable for running in browsers 72 | const clientBundleOutputDir = './wwwroot/dist'; 73 | const clientBundleConfig = merge(sharedConfig, { 74 | entry: { 75 | 'main-client': './ClientApp/boot.browser.ts' 76 | }, 77 | output: { 78 | path: path.join(__dirname, clientBundleOutputDir) 79 | }, 80 | plugins: [ 81 | new webpack.DllReferencePlugin({ 82 | context: __dirname, 83 | manifest: require('./wwwroot/dist/vendor-manifest.json') 84 | }) 85 | ].concat( 86 | isDevBuild 87 | ? [ 88 | // Plugins that apply in development builds only 89 | new webpack.SourceMapDevToolPlugin({ 90 | filename: '[file].map', // Remove this line if you prefer inline source maps 91 | moduleFilenameTemplate: path.relative( 92 | clientBundleOutputDir, 93 | '[resourcePath]' 94 | ) // Point sourcemap entries to the original file locations on disk 95 | }) 96 | ] 97 | : [ 98 | // new BundleAnalyzerPlugin(), 99 | // Plugins that apply in production builds only 100 | new AngularCompilerPlugin({ 101 | mainPath: path.join(__dirname, 'ClientApp/boot.browser.ts'), 102 | tsConfigPath: './ClientApp/tsconfig.app.json', 103 | entryModule: path.join( 104 | __dirname, 105 | 'ClientApp/app/app.module.browser#AppModule' 106 | ), 107 | exclude: ['./**/*.server.ts'], 108 | sourceMap: isDevBuild 109 | }) 110 | ] 111 | ), 112 | devtool: isDevBuild ? 'cheap-eval-source-map' : false, 113 | node: { 114 | fs: 'empty' 115 | }, 116 | optimization: { 117 | minimizer: [].concat( 118 | isDevBuild 119 | ? [] 120 | : [ 121 | // we specify a custom TerserPlugin here to get source maps in production 122 | new TerserPlugin({ 123 | sourceMap: true, 124 | terserOptions: { 125 | compress: true, 126 | ecma: 6, 127 | mangle: true, 128 | keep_classnames: true, 129 | keep_fnames: true, 130 | }, 131 | }), 132 | ] 133 | ) 134 | } 135 | }); 136 | 137 | // Configuration for server-side (prerendering) bundle suitable for running in Node 138 | const serverBundleConfig = merge(sharedConfig, { 139 | // resolve: { mainFields: ['main'] }, 140 | entry: { 141 | 'main-server': isDevBuild 142 | ? './ClientApp/boot.server.ts' 143 | : './ClientApp/boot.server.PRODUCTION.ts' 144 | }, 145 | plugins: [ 146 | new webpack.DllReferencePlugin({ 147 | context: __dirname, 148 | manifest: require('./ClientApp/dist/vendor-manifest.json'), 149 | sourceType: 'commonjs2', 150 | name: './vendor' 151 | }) 152 | ].concat( 153 | isDevBuild 154 | ? [ 155 | new webpack.ContextReplacementPlugin( 156 | // fixes WARNING Critical dependency: the request of a dependency is an expression 157 | /(.+)?angular(\\|\/)core(.+)?/, 158 | path.join(__dirname, 'src'), // location of your src 159 | {} // a map of your routes 160 | ), 161 | new webpack.ContextReplacementPlugin( 162 | // fixes WARNING Critical dependency: the request of a dependency is an expression 163 | /(.+)?express(\\|\/)(.+)?/, 164 | path.join(__dirname, 'src'), 165 | {} 166 | ) 167 | ] 168 | : [ 169 | // Plugins that apply in production builds only 170 | new AngularCompilerPlugin({ 171 | mainPath: path.join( 172 | __dirname, 173 | 'ClientApp/boot.server.PRODUCTION.ts' 174 | ), 175 | tsConfigPath: './ClientApp/tsconfig.app.json', 176 | entryModule: path.join( 177 | __dirname, 178 | 'ClientApp/app/app.module.server#AppModule' 179 | ), 180 | exclude: ['./**/*.browser.ts'], 181 | sourceMap: isDevBuild 182 | }) 183 | ] 184 | ), 185 | output: { 186 | libraryTarget: 'commonjs', 187 | path: path.join(__dirname, './ClientApp/dist') 188 | }, 189 | target: 'node', 190 | // switch to "inline-source-map" if you want to debug the TS during SSR 191 | devtool: isDevBuild ? 'cheap-eval-source-map' : false, 192 | optimization: { 193 | minimizer: [].concat( 194 | isDevBuild 195 | ? [] 196 | : [ 197 | // we specify a custom TerserPlugin here to get source maps in production 198 | new TerserPlugin({ 199 | cache: true, 200 | parallel: true, 201 | sourceMap: true, 202 | terserOptions: { 203 | compress: false, 204 | ecma: 6, 205 | mangle: true, 206 | keep_classnames: true, 207 | keep_fnames: true, 208 | }, 209 | }) 210 | ] 211 | ) 212 | } 213 | }); 214 | 215 | return [clientBundleConfig, serverBundleConfig]; 216 | }; 217 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ############################### 2 | # Core EditorConfig Options # 3 | ############################### 4 | root = true 5 | 6 | # All files 7 | [*] 8 | indent_style = space 9 | 10 | # Code files 11 | [*.{cs,csx,vb,vbx}] 12 | indent_size = 4 13 | insert_final_newline = true 14 | charset = utf-8-bom 15 | 16 | [*.md] 17 | insert_final_newline = false 18 | trim_trailing_whitespace = false 19 | 20 | ############################### 21 | # .NET Coding Conventions # 22 | ############################### 23 | 24 | # Solution Files 25 | [*.sln] 26 | indent_style = tab 27 | 28 | # XML Project Files 29 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 30 | indent_size = 2 31 | 32 | # Configuration Files 33 | [*.{json,xml,yml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] 34 | indent_size = 2 35 | 36 | # Dotnet Code Style Settings 37 | # See https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 38 | # See http://kent-boogaart.com/blog/editorconfig-reference-for-c-developers 39 | [*.{cs,csx,cake,vb}] 40 | dotnet_sort_system_directives_first = true:warning 41 | dotnet_style_coalesce_expression = true:warning 42 | dotnet_style_collection_initializer = true:warning 43 | dotnet_style_explicit_tuple_names = true:warning 44 | dotnet_style_null_propagation = true:warning 45 | dotnet_style_object_initializer = true:warning 46 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning 47 | dotnet_style_predefined_type_for_member_access = true:warning 48 | dotnet_style_qualification_for_event = true:warning 49 | dotnet_style_qualification_for_field = true:warning 50 | dotnet_style_qualification_for_method = true:warning 51 | dotnet_style_qualification_for_property = true:warning 52 | 53 | # Naming Symbols 54 | # constant_fields - Define constant fields 55 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 56 | dotnet_naming_symbols.constant_fields.required_modifiers = const 57 | # non_private_readonly_fields - Define public, internal and protected readonly fields 58 | dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, internal, protected 59 | dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field 60 | dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly 61 | # static_readonly_fields - Define static and readonly fields 62 | dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field 63 | dotnet_naming_symbols.static_readonly_fields.required_modifiers = static, readonly 64 | # private_readonly_fields - Define private readonly fields 65 | dotnet_naming_symbols.private_readonly_fields.applicable_accessibilities = private 66 | dotnet_naming_symbols.private_readonly_fields.applicable_kinds = field 67 | dotnet_naming_symbols.private_readonly_fields.required_modifiers = readonly 68 | # public_internal_fields - Define public and internal fields 69 | dotnet_naming_symbols.public_internal_fields.applicable_accessibilities = public, internal 70 | dotnet_naming_symbols.public_internal_fields.applicable_kinds = field 71 | # private_protected_fields - Define private and protected fields 72 | dotnet_naming_symbols.private_protected_fields.applicable_accessibilities = private, protected 73 | dotnet_naming_symbols.private_protected_fields.applicable_kinds = field 74 | # public_symbols - Define any public symbol 75 | dotnet_naming_symbols.public_symbols.applicable_accessibilities = public, internal, protected, protected_internal 76 | dotnet_naming_symbols.public_symbols.applicable_kinds = method, property, event, delegate 77 | # parameters - Defines any parameter 78 | dotnet_naming_symbols.parameters.applicable_kinds = parameter 79 | # non_interface_types - Defines class, struct, enum and delegate types 80 | dotnet_naming_symbols.non_interface_types.applicable_kinds = class, struct, enum, delegate 81 | # interface_types - Defines interfaces 82 | dotnet_naming_symbols.interface_types.applicable_kinds = interface 83 | 84 | # Naming Styles 85 | # camel_case - Define the camelCase style 86 | dotnet_naming_style.camel_case.capitalization = camel_case 87 | # pascal_case - Define the Pascal_case style 88 | dotnet_naming_style.pascal_case.capitalization = pascal_case 89 | # first_upper - The first character must start with an upper-case character 90 | dotnet_naming_style.first_upper.capitalization = first_word_upper 91 | # prefix_interface_interface_with_i - Interfaces must be PascalCase and the first character of an interface must be an 'I' 92 | dotnet_naming_style.prefix_interface_interface_with_i.capitalization = pascal_case 93 | dotnet_naming_style.prefix_interface_interface_with_i.required_prefix = I 94 | 95 | # Naming Rules 96 | # Constant fields must be PascalCase 97 | dotnet_naming_rule.constant_fields_must_be_pascal_case.severity = warning 98 | dotnet_naming_rule.constant_fields_must_be_pascal_case.symbols = constant_fields 99 | dotnet_naming_rule.constant_fields_must_be_pascal_case.style = pascal_case 100 | # Public, internal and protected readonly fields must be PascalCase 101 | dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.severity = warning 102 | dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.symbols = non_private_readonly_fields 103 | dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.style = pascal_case 104 | # Static readonly fields must be PascalCase 105 | dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.severity = warning 106 | dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.symbols = static_readonly_fields 107 | dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.style = pascal_case 108 | # Private readonly fields must be camelCase 109 | dotnet_naming_rule.private_readonly_fields_must_be_camel_case.severity = warning 110 | dotnet_naming_rule.private_readonly_fields_must_be_camel_case.symbols = private_readonly_fields 111 | dotnet_naming_rule.private_readonly_fields_must_be_camel_case.style = camel_case 112 | # Public and internal fields must be PascalCase 113 | dotnet_naming_rule.public_internal_fields_must_be_pascal_case.severity = warning 114 | dotnet_naming_rule.public_internal_fields_must_be_pascal_case.symbols = public_internal_fields 115 | dotnet_naming_rule.public_internal_fields_must_be_pascal_case.style = pascal_case 116 | # Private and protected fields must be camelCase 117 | dotnet_naming_rule.private_protected_fields_must_be_camel_case.severity = warning 118 | dotnet_naming_rule.private_protected_fields_must_be_camel_case.symbols = private_protected_fields 119 | dotnet_naming_rule.private_protected_fields_must_be_camel_case.style = camel_case 120 | # Public members must be capitalized 121 | dotnet_naming_rule.public_members_must_be_capitalized.severity = warning 122 | dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols 123 | dotnet_naming_rule.public_members_must_be_capitalized.style = first_upper 124 | # Parameters must be camelCase 125 | dotnet_naming_rule.parameters_must_be_camel_case.severity = warning 126 | dotnet_naming_rule.parameters_must_be_camel_case.symbols = parameters 127 | dotnet_naming_rule.parameters_must_be_camel_case.style = camel_case 128 | # Class, struct, enum and delegates must be PascalCase 129 | dotnet_naming_rule.non_interface_types_must_be_pascal_case.severity = warning 130 | dotnet_naming_rule.non_interface_types_must_be_pascal_case.symbols = non_interface_types 131 | dotnet_naming_rule.non_interface_types_must_be_pascal_case.style = pascal_case 132 | # Interfaces must be PascalCase and start with an 'I' 133 | dotnet_naming_rule.interface_types_must_be_prefixed_with_i.severity = warning 134 | dotnet_naming_rule.interface_types_must_be_prefixed_with_i.symbols = interface_types 135 | dotnet_naming_rule.interface_types_must_be_prefixed_with_i.style = prefix_interface_interface_with_i 136 | 137 | # C# Code Style Settings 138 | # See https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 139 | # See http://kent-boogaart.com/blog/editorconfig-reference-for-c-developers 140 | [*.{cs,csx,cake}] 141 | # Indentation Options 142 | csharp_indent_block_contents = true:warning 143 | csharp_indent_braces = false:warning 144 | csharp_indent_case_contents = true:warning 145 | csharp_indent_labels = no_change:warning 146 | csharp_indent_switch_labels = true:warning 147 | # Style Options 148 | csharp_style_conditional_delegate_call = true:warning 149 | csharp_style_expression_bodied_accessors = true:warning 150 | csharp_style_expression_bodied_constructors = true:warning 151 | csharp_style_expression_bodied_indexers = true:warning 152 | csharp_style_expression_bodied_methods = true:warning 153 | csharp_style_expression_bodied_operators = true:warning 154 | csharp_style_expression_bodied_properties = true:warning 155 | csharp_style_inlined_variable_declaration = true:warning 156 | csharp_style_pattern_matching_over_as_with_null_check = true:warning 157 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning 158 | csharp_style_throw_expression = true:warning 159 | csharp_style_var_elsewhere = true:warning 160 | csharp_style_var_for_built_in_types = true:warning 161 | csharp_style_var_when_type_is_apparent = true:warning 162 | # New Line Options 163 | csharp_new_line_before_catch = true:warning 164 | csharp_new_line_before_else = true:warning 165 | csharp_new_line_before_finally = true:warning 166 | csharp_new_line_before_members_in_anonymous_types = true:warning 167 | csharp_new_line_before_members_in_object_initializers = true:warning 168 | # BUG: Warning level cannot be set https://github.com/dotnet/roslyn/issues/18010 169 | csharp_new_line_before_open_brace = all 170 | csharp_new_line_between_query_expression_clauses = true:warning 171 | # Spacing Options 172 | csharp_space_after_cast = false:warning 173 | csharp_space_after_colon_in_inheritance_clause = true:warning 174 | csharp_space_after_comma = true:warning 175 | csharp_space_after_dot = false:warning 176 | csharp_space_after_keywords_in_control_flow_statements = true:warning 177 | csharp_space_after_semicolon_in_for_statement = true:warning 178 | csharp_space_around_binary_operators = before_and_after:warning 179 | csharp_space_around_declaration_statements = do_not_ignore:warning 180 | csharp_space_before_colon_in_inheritance_clause = true:warning 181 | csharp_space_before_comma = false:warning 182 | csharp_space_before_dot = false:warning 183 | csharp_space_before_semicolon_in_for_statement = false:warning 184 | csharp_space_before_open_square_brackets = false:warning 185 | csharp_space_between_empty_square_brackets = false:warning 186 | csharp_space_between_method_declaration_name_and_open_parenthesis = false:warning 187 | csharp_space_between_method_declaration_parameter_list_parentheses = false:warning 188 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false:warning 189 | csharp_space_between_method_call_name_and_opening_parenthesis = false:warning 190 | csharp_space_between_method_call_parameter_list_parentheses = false:warning 191 | csharp_space_between_method_call_empty_parameter_list_parentheses = false:warning 192 | csharp_space_between_parentheses = expressions:warning 193 | csharp_space_between_square_brackets = false:warning 194 | # Wrapping Options 195 | csharp_preserve_single_line_blocks = true:warning 196 | csharp_preserve_single_line_statements = false:warning 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASP.NET Core 2.1 & Angular 7(+) Advanced Starter - PWA & Server-side prerendering (for Angular SEO)! 2 | 3 | --- 4 | 5 | <br> 6 | <p align="center"> 7 | <a href="https://trilon.io" target="_blank"> 8 | <img width="500" height="auto" src="https://trilon.io/trilon-logo-clear.png" alt="Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training"> 9 | </a> 10 | </p> 11 | 12 | 13 | <h3 align="center"> Made with :heart: by <a href="https://trilon.io">Trilon.io</a></h3> 14 | 15 | --- 16 | 17 | ### Harness the power of Angular 7+, ASP.NET Core 2.1, now with SEO ! 18 | 19 | Angular SEO in action: 20 | 21 | <p align="center"> 22 | <img src="./docs/angular2-seo.png" alt="ASP.NET Core Angular7 SEO" title="ASP.NET Core Angular7 SEO"> 23 | </p> 24 | 25 | ### Angular Universal Application Architecture 26 | 27 | <p align="center"> 28 | <img src="./docs/architecture.png" alt="ASP.NET Core 2.1 Angular 7+ Starter" title="ASP.NET Core 2.1 Angular 7+ Starter"> 29 | </p> 30 | 31 | ### What is this repo? Live Demo here: http://aspnetcore-angular2-universal.azurewebsites.net 32 | 33 | This repository is maintained by [Trilon.io](https://Trilon.io) and the [Angular](https://github.com/angular/angular) Universal team and is meant to be an advanced starter 34 | for both ASP.NET Core 2.1 using Angular 7.0+, not only for the client-side, but to be rendered on the server for instant 35 | application paints (Note: If you don't need SSR [read here](#faq) on how to disable it). 36 | 37 | This is meant to be a Feature-Rich Starter application containing all of the latest technologies, best build systems available, and include many real-world examples and libraries needed in todays Single Page Applications (SPAs). 38 | 39 | This utilizes all the latest standards, no gulp, no bower, no typings, no manually "building" anything. NPM, Webpack and .NET handle everything for you! 40 | 41 | ### Join us on Gitter 42 | 43 | [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/aspnetcore-angular2-universal/) 44 | 45 | # Table of Contents 46 | 47 | * [Features](#features) 48 | * [Getting Started](#getting-started) 49 | * [Deployment](#deployment) 50 | * [Upcoming Features](#upcoming-features) 51 | * [Application Structure](#application-structure) 52 | * [Gotchas](#gotchas) 53 | * [FAQ](#faq---also-check-out-the-faq-issues-label-and-the-how-to-issues-label) 54 | * [Special Thanks](#special-thanks) 55 | * [License](#license) 56 | * [Trilon - Consulting & Training](#trilon---angular--aspnet---consulting--training--development) 57 | 58 | --- 59 | 60 | # Features: 61 | 62 | > These are just some of the features found in this starter! 63 | 64 | - ASP.NET 2.1 - VS2017 support now! 65 | - Azure delpoyment straight from VS2017 66 | - Built in docker support through VS2017 67 | - RestAPI (WebAPI) integration 68 | - SQL Database CRUD demo 69 | - Swagger WebAPI documentation when running in development mode 70 | - SignalR Chat demo! (Thanks to [@hakonamatata](https://github.com/hakonamatata)) 71 | 72 | - **Angular 7.0.0** : 73 | - PWA (Progressive Web App) 74 | - (Minimal) Angular-CLI integration 75 | - This is to be used mainly for Generating Components/Services/etc. 76 | - Usage examples: 77 | - `ng g c components/example-component` 78 | - `ng g s shared/some-service` 79 | - Featuring Server-side rendering (Platform-Server, aka: "Universal") 80 | - Faster initial paints, SEO (Search-engine optimization w Title/Meta/Link tags), social media link-previews, etc 81 | - i18n internationalization support (via/ ngx-translate) 82 | - Baked in best-practices (follows Angular style guide) 83 | - Bootstrap3 (with ngx-bootstrap) - (can be rendered on the server!) 84 | - Can be easily replaced with bootstrap4 (3 is provided for browser support) 85 | - Bootstrap using SCSS / SASS for easy theming / styling! 86 | 87 | - **Webpack build system (Webpack 4)** 88 | - HMR : Hot Module Reloading/Replacement 89 | - Production builds w/ AoT Compilation 90 | 91 | - **Testing frameworks** 92 | - Unit testing with Jest (Going back to Karma soon) 93 | 94 | - **Productivity** 95 | - Typescript 2 96 | - Codelyzer (for Real-time static code analysis) 97 | - VSCode & Atom provide real-time analysis out of the box. 98 | 99 | - **ASP.NET Core 2.1** 100 | 101 | - Integration with NodeJS to provide pre-rendering, as well as any other Node module asset you want to use. 102 | 103 | - **Azure** 104 | - Microsoft Application Insights setup (for MVC & Web API routing) 105 | - Client-side Angular Application Insights integration 106 | - If you're using Azure simply install `npm i -S @markpieszak/ng-application-insights` as a dependencies. 107 | - Note: Make sure only the Browser makes these calls ([usage info here](https://github.com/MarkPieszak/angular-application-insights/blob/master/README.md#usage)) 108 | - More information here: - https://github.com/MarkPieszak/angular-application-insights 109 | ```typescript 110 | // Add the Module to your imports 111 | ApplicationInsightsModule.forRoot({ 112 | instrumentationKey: 'Your-Application-Insights-instrumentationKey' 113 | }) 114 | ``` 115 | - **Docker** 116 | - Built in Visual Studio F5 Debugging support 117 | - Uses the very light weight microsoft/dotnet image 118 | - Currently limited to Linux image as there is a bug with running nodejs in a container on Windows. 119 | 120 | 121 | ---- 122 | 123 | ---- 124 | 125 | # Getting Started? 126 | 127 | - **Make sure you have at least Node 8.11.1 or higher (w/ npm 5+) installed!** 128 | - **This repository uses ASP.Net Core 2.1, which has a hard requirement on .NET Core Runtime 2.1 and .NET Core SDK 2.1. Please install these items from [here](https://blogs.msdn.microsoft.com/dotnet/2018/05/30/announcing-net-core-2-1/?WT.mc_id=blog-twitter-timheuer)** 129 | 130 | 131 | ### Visual Studio 2017 132 | 133 | Make sure you have .NET Core 2.1 installed and/or VS2017 15.3. 134 | VS2017 will automatically install all the neccessary npm & .NET dependencies when you open the project. 135 | 136 | Simply push F5 to start debugging ! 137 | 138 | **Docker-Support**: Change the startup project to docker-compose and press F5 139 | 140 | **Note**: If you get any errors after this such as `module not found: boot.server` (or similar), open up command line and run `npm run build:dev` to make sure all the assets have been properly built by Webpack. 141 | 142 | ### Visual Studio Code 143 | 144 | > Note: Make sure you have the C# extension & .NET Core Debugger installed. 145 | 146 | The project comes with the configured Launch.json files to let you just push F5 to start the project. 147 | 148 | ```bash 149 | # cd into the directory you cloned the project into 150 | npm install && npm run build:dev && dotnet restore 151 | # or yarn install 152 | ``` 153 | 154 | If you're running the project from command line with `dotnet run` make sure you set your environment variables to Development (otherwise things like HMR might not work). 155 | 156 | ```bash 157 | # on Windows: 158 | set ASPNETCORE_ENVIRONMENT=Development 159 | # on Mac/Linux 160 | export ASPNETCORE_ENVIRONMENT=Development 161 | ``` 162 | 163 | # Upcoming Features: 164 | 165 | - Clean API / structure / simplify application 166 | - Refactor to latest RxJs pipeable syntax 167 | - Attempt to integrate with Angular-CLI fully 168 | 169 | ---- 170 | 171 | ---- 172 | 173 | # Deployment 174 | 175 | ### Dotnet publish 176 | Using `dotnet publish`, when it's finished place the generated folder onto your server and use IIS to fire everything up. 177 | 178 | ### Heroku 179 | <a href="https://dashboard.heroku.com/new?template=https://github.com/MarkPieszak/aspnetcore-angular2-universal.git"> 180 | <img src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy on Heroku"> 181 | </a> 182 | 183 | ### Azure 184 | 185 | ```bash 186 | git remote add azure https://your-user-name@my-angular2-site.scm.azurewebsites.net:443/my-angular2-site.git 187 | // ^ get this from Azure (Web App Overview section - Git clone url) 188 | 189 | git push --set-upstream azure master 190 | ``` 191 | 192 | # Application Structure: 193 | 194 | > Note: This application has WebAPI (our REST API) setup inside the same project, but of course all of this 195 | could be abstracted out into a completely separate project('s) ideally. .NET Core things are all done in the same project 196 | for simplicity's sake. 197 | 198 | **Root level files** 199 | 200 | Here we have the *usual suspects* found at the root level. 201 | 202 | *Front-end oriented files:* 203 | 204 | - `package.json` - NPM project dependencies & scripts 205 | - `.tsconfig` - TypeScript configuration (here we setup PATHs as well) 206 | - `webpack` - configuration files (modular bundling + so much more) 207 | - `karma` - configuration files (unit testing) 208 | - `protractor` - config files (e2e testing) 209 | - `tslint` - TypeScript code linting rules 210 | 211 | ### /ClientApp/ - Everything Angular 212 | 213 | > Let's take a look at how this is structured so we can make some sense of it all! 214 | 215 | With Angular Universal, we need to split our applicatoin logic **per platform** so [if we look inside this folder](./ClientApp), 216 | you'll see the 2 root files, that branch the entire logic for browser & server respectively. 217 | 218 | - [**Boot.Browser.ts**](./ClientApp/boot.browser.ts) - 219 | This file starts up the entire Angular application for the Client/browser platform. 220 | 221 | Here we setup a few things, client Angular bootstrapping. 222 | 223 | You'll barely need to touch this file, but something to note, this is the file where you would import libraries that you **only** want 224 | being used in the Browser. (Just know that you'd have to provide a mock implementation for the Server when doing that). 225 | 226 | - [**Boot.Server.ts**](./ClientApp/boot.server.ts) - 227 | This file is where Angular _platform-server_ *serializes* the Angular application itself on the .NET server 228 | within a very quick Node process, and renders it a string. This is what causes that initial fast paint 229 | of the entire application to the Browser, and helps us get all our _SEO_ goodness :sparkles: 230 | 231 | --- 232 | 233 | Notice the folder structure here in `./ClientApp/` : 234 | 235 | ```diff 236 | + /ClientApp/ 237 | 238 | + /app/ 239 | App NgModule - our Root NgModule (you'll insert Components/etc here most often) 240 | AppComponent / App Routes / global css styles 241 | 242 | * Notice that we have 2 dividing NgModules: 243 | app.module.browser & app.module.server 244 | You'll almost always be using the common app.module, but these 2 are used to split up platform logic 245 | for situations where you need to use Dependency Injection / etc, between platforms. 246 | 247 | Note: You could use whatever folder conventions you'd like, I prefer to split up things in terms of whether they are re-usable 248 | "components" or routeable / page-like components that group together and organize entire sections. 249 | ++ > ++ > /components/ 250 | Here are all the regular Components that aren't "Pages" or container Components 251 | 252 | ++ > ++ > /containers/ 253 | These are the routeable or "Page / Container" Components, sometimes known as "Dumb" Components 254 | 255 | ++ > ++ > /shared/ 256 | Here we put all shared Services / Directives / Pipes etc 257 | ``` 258 | 259 | When adding new features/components/etc to your application you'll be commonly adding things to the Root **NgModule** (located 260 | in `/ClientApp/app/app.module.ts`), but why are there **two** other NgModules in this folder? 261 | 262 | This is because we want to split our logic **per Platform**, but notice they both share the Common NgModule 263 | named `app.module.ts`. When adding most things to your application, this is the only 264 | place you'll have to add in your new Component / Directive / Pipe / etc. You'll only occassional need to manually 265 | add in the Platform specific things to either `app.module.browser || app.module.server`. 266 | 267 | To illustrate this point with an example, you can see how we're using Dependency Injection to inject a `StorageService` that is different 268 | for the Browser & Server. 269 | 270 | ```typescript 271 | // For the Browser (app.module.browser) 272 | { provide: StorageService, useClass: BrowserStorage } 273 | 274 | // For the Server (app.module.server) 275 | { provide: StorageService, useClass: ServerStorage } 276 | ``` 277 | 278 | > Just remember, you'll usually only need to worry about `app.module.ts`, as that's where you'll be adding most 279 | of your applications new aspects! 280 | 281 | 282 | ### /Server/ - Our REST API (WebApi) - MVC Controller 283 | 284 | > As we pointed out, these are here for simplicities sake, and realistically you may want separate projects 285 | for all your microservices / REST API projects / etc. 286 | 287 | We're utilizing MVC within this application, but we only need & have ONE Controller, named `HomeController`. This is where our entire 288 | Angular application gets serialized into a String, sent to the Browser, along with all the assets it needs to then bootstrap on the client-side, and become a full-blown SPA afterwards. 289 | 290 | --- 291 | 292 | The short-version is that we invoke that Node process, passing in our Request object & invoke the `boot.server` file, and we get back a nice object that we pass into .NETs `ViewData` object, and sprinkle through out our `Views/Shared/_Layout.cshtml` and `/Views/Home/index.cshtml` files! 293 | 294 | A more detailed explanation can be found here: [ng-AspnetCore-Engine Readme](https://github.com/angular/universal/tree/master/modules/aspnetcore-engine) 295 | 296 | ```csharp 297 | // Prerender / Serialize application 298 | var prerenderResult = await Prerenderer.RenderToString( 299 | /* all of our parameters / options / boot.server file / customData object goes here */ 300 | ); 301 | 302 | ViewData["SpaHtml"] = prerenderResult.Html; 303 | ViewData["Title"] = prerenderResult.Globals["title"]; 304 | ViewData["Styles"] = prerenderResult.Globals["styles"]; 305 | ViewData["Meta"] = prerenderResult.Globals["meta"]; 306 | ViewData["Links"] = prerenderResult.Globals["links"]; 307 | 308 | return View(); // let's render the MVC View 309 | ``` 310 | 311 | Take a look at the `_Layout.cshtml` file for example, notice how we let .NET handle and inject all our SEO magic (that we extracted from Angular itself) ! 312 | 313 | ```html 314 | <!DOCTYPE html> 315 | <html> 316 | <head> 317 | <base href="/" /> 318 | <!-- Title will be the one you set in your Angular application --> 319 | <title>@ViewData["Title"] - AspNET.Core Angular 7.0.0 (+) starter 320 | 321 | 322 | 323 | @Html.Raw(ViewData["Meta"]) 324 | @Html.Raw(ViewData["Links"]) 325 | 326 | 327 | 328 | @Html.Raw(ViewData["Styles"]) 329 | 330 | 331 | ... etc ... 332 | ``` 333 | 334 | Our `Views/Home/index.cshtml` simply renders the application and serves the bundled webpack files in it. 335 | 336 | ```html 337 | @Html.Raw(ViewData["SpaHtml"]) 338 | 339 | 340 | @section scripts { 341 | 342 | } 343 | ``` 344 | 345 | ### What happens after the App gets server rendered? 346 | 347 | Well now, your Client-side Angular will take over, and you'll have a fully functioning SPA. (But we gained all these great SEO benefits of being server-rendered) ! 348 | 349 | :sparkles: 350 | 351 | 352 | ---- 353 | 354 | ---- 355 | 356 | # "Gotchas" 357 | 358 | - This repository uses ASP.Net Core 2.1, which has a hard requirement on .NET Core Runtime 2.1 and .NET Core SDK 2.1. Please install these items from [here](https://blogs.msdn.microsoft.com/dotnet/2018/05/30/announcing-net-core-2-1/?WT.mc_id=blog-twitter-timheuer) 359 | 360 | > When building components in Angular 7 there are a few things to keep in mind. 361 | 362 | - Make sure you provide Absolute URLs when calling any APIs. (The server can't understand relative paths, so `/api/whatever` will fail). 363 | 364 | - API calls will be ran during a server, and once again during the client render, so make sure you're using transfering data that's important to you so that you don't see a flicker. 365 | 366 | - **`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: 367 | - 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. 368 | 369 | ```typescript 370 | import { PLATFORM_ID } from '@angular/core'; 371 | import { isPlatformBrowser, isPlatformServer } from '@angular/common'; 372 | 373 | constructor(@Inject(PLATFORM_ID) private platformId: Object) { ... } 374 | 375 | ngOnInit() { 376 | if (isPlatformBrowser(this.platformId)) { 377 | // Client only code. 378 | ... 379 | } 380 | if (isPlatformServer(this.platformId)) { 381 | // Server only code. 382 | ... 383 | } 384 | } 385 | ``` 386 | 387 | - Try to *limit or* **avoid** using **`setTimeout`**. It will slow down the server-side rendering process. Make sure to remove them [`ngOnDestroy`](https://angular.io/docs/ts/latest/api/core/index/OnDestroy-class.html) in Components. 388 | - Also for RxJs timeouts, make sure to _cancel_ their stream on success, for they can slow down rendering as well. 389 | - **Don't manipulate the nativeElement directly**. Use the _Renderer2_. We do this to ensure that in any environment we're able to change our view. 390 | ```typescript 391 | constructor(element: ElementRef, renderer: Renderer2) { 392 | this.renderer.setStyle(element.nativeElement, 'font-size', 'x-large'); 393 | } 394 | ``` 395 | - The application runs XHR requests on the server & once again on the Client-side (when the application bootstraps) 396 | - Use a cache that's transferred from server to client (TODO: Point to the example) 397 | - Know the difference between attributes and properties in relation to the DOM. 398 | - Keep your directives stateless as much as possible. For stateful directives, you may need to provide an attribute that reflects the corresponding property with an initial string value such as url in img tag. For our native element the src attribute is reflected as the src property of the element type HTMLImageElement. 399 | - Error: `sass-loader` requires `node-sass` >=4: Either in the docker container or localhost run npm rebuild node-sass -f 400 | 401 | ---- 402 | 403 | ---- 404 | 405 | # FAQ - Also check out the [!FAQ Issues label!](https://github.com/MarkPieszak/aspnetcore-angular2-universal/issues?utf8=%E2%9C%93&q=is%3Aissue%20label%3Afaq) and the [!HOW-TO Issues Label!](https://github.com/MarkPieszak/aspnetcore-angular2-universal/issues?q=is%3Aissue+label%3A%22HOW+TO+-+Guide%22) 406 | 407 | ### How can I disable SSR (Server-side rendering)? 408 | 409 | Simply comment out the logic within HomeController, and replace `@Html.Raw(ViewData["SpaHtml"])` with just your applications root 410 | AppComponent tag ("app-root" in our case): ``. 411 | 412 | > You could also remove any `isPlatformBrowser/etc` logic, and delete the boot.server, app.module.browser & app.module.server files, just make sure your `boot.browser` file points to `app.module`. 413 | 414 | ### How do I have code run only in the Browser? 415 | 416 | Check the [Gotchas](#gotchas) on how to use `isPlatformBrowser()`. 417 | 418 | ### How do I Material2 with this repo? 419 | 420 | ~~You'll either want to remove SSR for now, or wait as support should be coming to handle platform-server rendering.~~ 421 | This is now possible, with the recently updated Angular Material changes. We do not have a tutorial available for this yet. 422 | 423 | ### How can I use jQuery and/or some jQuery plugins with this repo? 424 | 425 | > Note: If at all possible, try to avoid using jQuery or libraries dependent on it, as there are 426 | better, more abstract ways of dealing with the DOM in Angular (5+) such as using the Renderer, etc. 427 | 428 | Yes, of course but there are a few things you need to setup before doing this. First, make sure jQuery 429 | is included in webpack vendor file, and that you have a webpack Plugin setup for it. `new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' })` 430 | 431 | Now, make sure any "plugins" etc that you have, are only included in your `boot.browser.ts` file. (ie: `import 'slick-carousel';`) 432 | In a Component you want to use jQuery, make sure to import it near the top like so: 433 | 434 | ```typescript 435 | import * as $ from 'jquery'; 436 | ``` 437 | 438 | **Always make sure to wrap anything jQuery oriented in Angular's `isPlatformBrowser()` conditional!** 439 | 440 | ### How can I support IE9 through IE11? 441 | 442 | To support IE9 through IE11 open the `polyfills.ts` file in the `polyfills` folder and uncomment out the 'import polyfills' as needed. ALSO - make sure that your `webpack.config` and `webpack.config.vendor` change option of `TerserPlugin` from `ecma: 6` to **`ecma: 5`**. 443 | 444 | ---- 445 | 446 | # Special Thanks 447 | 448 | Many thanks go out to Steve Sanderson ([@SteveSandersonMS](https://github.com/SteveSandersonMS)) from Microsoft and his amazing work on JavaScriptServices and integrating the world of Node with ASP.NET Core. 449 | 450 | Also thank you to the many Contributors ! 451 | - [@GRIMMR3AP3R](https://github.com/GRIMMR3AP3R) 452 | - [@Isaac2004](https://github.com/Isaac2004) 453 | - [@AbrarJahin](https://github.com/AbrarJahin) 454 | - [@LiverpoolOwen](https://github.com/LiverpoolOwen) 455 | - [@hakonamatata](https://github.com/hakonamatata) 456 | - [@markwhitfeld](https://github.com/markwhitfeld) 457 | - [@Ketrex](https://github.com/Ketrex) 458 | 459 | ---- 460 | 461 | # Found a Bug? Want to Contribute? 462 | 463 | [Check out our easier issues here](https://github.com/MarkPieszak/aspnetcore-angular2-universal/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3A%22PRs%20welcome!%22%20) 464 | 465 | Nothing's ever perfect, but please let me know by creating an issue (make sure there isn't an existing one about it already), and we'll try and work out a fix for it! If you have any good ideas, or want to contribute, feel free to either make an Issue with the Proposal, or just make a PR from your Fork. 466 | 467 | ---- 468 | 469 | # License 470 | 471 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](/LICENSE) 472 | 473 | Copyright (c) 2016-2019 [Mark Pieszak](https://github.com/MarkPieszak) 474 | 475 | [![Twitter Follow](https://img.shields.io/twitter/follow/MarkPieszak.svg?style=social)](https://twitter.com/MarkPieszak) 476 | 477 | ---- 478 | 479 | # Trilon - Angular & ASP.NET - Consulting | Training | Development 480 | 481 | Check out **[Trilon.io](https://Trilon.io)** for more info! Twitter [@Trilon_io](http://www.twitter.com/Trilon_io) 482 | 483 | Contact us at , and let's talk about your projects needs. 484 | 485 |

486 | 487 | Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training 488 | 489 |

490 | 491 | ## Follow Trilon online: 492 | 493 | Twitter: [@Trilon_io](http://twitter.com/Trilon_io) 494 | --------------------------------------------------------------------------------