2 |
7 |
8 | VIDEO
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 |
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 |
20 |
21 |
22 |
23 |
24 |
25 |
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 |
17 | Toggle last panel
18 |
19 |
20 | Enable / Disable first panel
21 |
22 |
23 |
24 |
25 |
26 | Open only one at a time
27 |
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 | Add Item
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 |
2 | Angular 7 Universal & ASP.NET Core
3 |
5 |
6 |
7 |
41 |
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 SEO tags
22 | this.ViewData["Links"] = prerenderResult.Globals["links"]; // set our 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"] = "";
26 | }
27 |
28 | return View ();
29 | }
30 |
31 | [HttpGet]
32 | [Route("sitemap.xml")]
33 | public IActionResult SitemapXml() => Content($@"
34 |
35 |
36 | http://localhost:4251/home
37 | { DateTime.Now.ToString("yyyy-MM-dd")}
38 |
39 |
40 | http://localhost:4251/counter
41 | {DateTime.Now.ToString("yyyy-MM-dd")}
42 |
43 | ", "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 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
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(' 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 | {{ title }}
2 |
3 |
4 | Enjoy the latest features from .NET Core & Angular 7.x!
5 | For more info check the repo here:
6 | AspNetCore-Angular-Universal repo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
{{ 'HOME_FEATURE_LIST_TITLE' | translate }}
14 |
15 | ASP.NET Core 2.1 :: ( Visual Studio 2017 )
16 |
17 | Angular 7.* front-end UI framework
18 |
19 | Angular **platform-server** (aka: Universal) - server-side rendering for SEO, deep-linking, and incredible performance.
20 |
21 | AoT (Ahead-of-time) production compilation for even faster Prod builds.
22 |
23 |
24 |
25 | The latest TypeScript 2.* features
26 |
27 |
28 | Webpack
29 |
30 | Hot Module Reloading/Replacement for an amazing development experience.
31 | Tree-shaking
32 |
33 |
34 |
35 | Bootstrap (ngx-bootstrap) : Bootstrap capable of being rendered even on the server.
36 | Unit testing via karma & jasmine.
37 |
38 |
39 |
40 |
41 |
42 |
{{ 'HOME_ISSUES_TITLE' | translate }}
43 |
44 |
54 |
55 |
56 |
57 |
Consulting | Development | Training | Workshops
58 |
59 | Get your Team or Application up to speed by working with some of the leading industry experts in JavaScript, Node / NestJS, & ASP.NET!
60 |
61 |
Follow us on Twitter!
62 |
@trilon_io |
63 |
@MarkPieszak
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
{{ 'SWITCH_LANGUAGE' | translate }}
76 |
77 |
78 | {{ 'ENGLISH' | translate }}
79 |
80 |
81 |
82 | {{ 'NORWEGIAN' | translate }}
83 |
84 |
85 |
86 |
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 |
2 |
3 | netcoreapp2.1
4 | true
5 | Latest
6 | false
7 | Linux
8 | docker-compose.dcproj
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | %(DistFiles.Identity)
56 | PreserveNewest
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
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 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 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 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 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 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(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 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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Made with :heart: by Trilon.io
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 |
22 |
23 |
24 |
25 | ### Angular Universal Application Architecture
26 |
27 |
28 |
29 |
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 | [](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 |
180 |
181 |
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 |
315 |
316 |
317 |
318 |
319 | @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 | [](/LICENSE)
472 |
473 | Copyright (c) 2016-2019 [Mark Pieszak](https://github.com/MarkPieszak)
474 |
475 | [](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 |
488 |
489 |
490 |
491 | ## Follow Trilon online:
492 |
493 | Twitter: [@Trilon_io](http://twitter.com/Trilon_io)
494 |
--------------------------------------------------------------------------------