diff --git a/.angular-cli.json b/.angular-cli.json deleted file mode 100644 index 4e8d017d..00000000 --- a/.angular-cli.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "project": { - "name": "AspnetCore-Angular-Universal" - }, - "apps": [ - { - "root": "ClientApp" - } - ], - "defaults": { - "styleExt": "scss", - "component": { - "spec": false - } - }, - "lint":[ - { - "project": "ClientApp/tsconfig.app.json" - }, - { - "project": "ClientApp/tsconfig.spec.json" - } - ] -} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..04583682 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.dockerignore +.env +.git +.gitignore +.vs +.vscode +docker-compose.yml +docker-compose.*.yml +*/bin +*/obj +node_modules +docs \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index f1cc3ad3..90bafbc1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,15 +1,196 @@ -# http://editorconfig.org - +############################### +# Core EditorConfig Options # +############################### root = true +# All files [*] -charset = utf-8 indent_style = space -indent_size = 2 -end_of_line = lf + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 insert_final_newline = true -trim_trailing_whitespace = true +charset = utf-8-bom [*.md] insert_final_newline = false trim_trailing_whitespace = false + +############################### +# .NET Coding Conventions # +############################### + +# Solution Files +[*.sln] +indent_style = tab + +# XML Project Files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# Configuration Files +[*.{json,xml,yml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] +indent_size = 2 + +# Dotnet Code Style Settings +# See https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference +# See http://kent-boogaart.com/blog/editorconfig-reference-for-c-developers +[*.{cs,csx,cake,vb}] +dotnet_sort_system_directives_first = true:warning +dotnet_style_coalesce_expression = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_null_propagation = true:warning +dotnet_style_object_initializer = true:warning +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning +dotnet_style_qualification_for_event = true:warning +dotnet_style_qualification_for_field = true:warning +dotnet_style_qualification_for_method = true:warning +dotnet_style_qualification_for_property = true:warning + +# Naming Symbols +# constant_fields - Define constant fields +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +# non_private_readonly_fields - Define public, internal and protected readonly fields +dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, internal, protected +dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly +# static_readonly_fields - Define static and readonly fields +dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.static_readonly_fields.required_modifiers = static, readonly +# private_readonly_fields - Define private readonly fields +dotnet_naming_symbols.private_readonly_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.private_readonly_fields.required_modifiers = readonly +# public_internal_fields - Define public and internal fields +dotnet_naming_symbols.public_internal_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_internal_fields.applicable_kinds = field +# private_protected_fields - Define private and protected fields +dotnet_naming_symbols.private_protected_fields.applicable_accessibilities = private, protected +dotnet_naming_symbols.private_protected_fields.applicable_kinds = field +# public_symbols - Define any public symbol +dotnet_naming_symbols.public_symbols.applicable_accessibilities = public, internal, protected, protected_internal +dotnet_naming_symbols.public_symbols.applicable_kinds = method, property, event, delegate +# parameters - Defines any parameter +dotnet_naming_symbols.parameters.applicable_kinds = parameter +# non_interface_types - Defines class, struct, enum and delegate types +dotnet_naming_symbols.non_interface_types.applicable_kinds = class, struct, enum, delegate +# interface_types - Defines interfaces +dotnet_naming_symbols.interface_types.applicable_kinds = interface + +# Naming Styles +# camel_case - Define the camelCase style +dotnet_naming_style.camel_case.capitalization = camel_case +# pascal_case - Define the Pascal_case style +dotnet_naming_style.pascal_case.capitalization = pascal_case +# first_upper - The first character must start with an upper-case character +dotnet_naming_style.first_upper.capitalization = first_word_upper +# prefix_interface_interface_with_i - Interfaces must be PascalCase and the first character of an interface must be an 'I' +dotnet_naming_style.prefix_interface_interface_with_i.capitalization = pascal_case +dotnet_naming_style.prefix_interface_interface_with_i.required_prefix = I + +# Naming Rules +# Constant fields must be PascalCase +dotnet_naming_rule.constant_fields_must_be_pascal_case.severity = warning +dotnet_naming_rule.constant_fields_must_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_must_be_pascal_case.style = pascal_case +# Public, internal and protected readonly fields must be PascalCase +dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.severity = warning +dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.symbols = non_private_readonly_fields +dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.style = pascal_case +# Static readonly fields must be PascalCase +dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.severity = warning +dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.symbols = static_readonly_fields +dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.style = pascal_case +# Private readonly fields must be camelCase +dotnet_naming_rule.private_readonly_fields_must_be_camel_case.severity = warning +dotnet_naming_rule.private_readonly_fields_must_be_camel_case.symbols = private_readonly_fields +dotnet_naming_rule.private_readonly_fields_must_be_camel_case.style = camel_case +# Public and internal fields must be PascalCase +dotnet_naming_rule.public_internal_fields_must_be_pascal_case.severity = warning +dotnet_naming_rule.public_internal_fields_must_be_pascal_case.symbols = public_internal_fields +dotnet_naming_rule.public_internal_fields_must_be_pascal_case.style = pascal_case +# Private and protected fields must be camelCase +dotnet_naming_rule.private_protected_fields_must_be_camel_case.severity = warning +dotnet_naming_rule.private_protected_fields_must_be_camel_case.symbols = private_protected_fields +dotnet_naming_rule.private_protected_fields_must_be_camel_case.style = camel_case +# Public members must be capitalized +dotnet_naming_rule.public_members_must_be_capitalized.severity = warning +dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols +dotnet_naming_rule.public_members_must_be_capitalized.style = first_upper +# Parameters must be camelCase +dotnet_naming_rule.parameters_must_be_camel_case.severity = warning +dotnet_naming_rule.parameters_must_be_camel_case.symbols = parameters +dotnet_naming_rule.parameters_must_be_camel_case.style = camel_case +# Class, struct, enum and delegates must be PascalCase +dotnet_naming_rule.non_interface_types_must_be_pascal_case.severity = warning +dotnet_naming_rule.non_interface_types_must_be_pascal_case.symbols = non_interface_types +dotnet_naming_rule.non_interface_types_must_be_pascal_case.style = pascal_case +# Interfaces must be PascalCase and start with an 'I' +dotnet_naming_rule.interface_types_must_be_prefixed_with_i.severity = warning +dotnet_naming_rule.interface_types_must_be_prefixed_with_i.symbols = interface_types +dotnet_naming_rule.interface_types_must_be_prefixed_with_i.style = prefix_interface_interface_with_i + +# C# Code Style Settings +# See https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference +# See http://kent-boogaart.com/blog/editorconfig-reference-for-c-developers +[*.{cs,csx,cake}] +# Indentation Options +csharp_indent_block_contents = true:warning +csharp_indent_braces = false:warning +csharp_indent_case_contents = true:warning +csharp_indent_labels = no_change:warning +csharp_indent_switch_labels = true:warning +# Style Options +csharp_style_conditional_delegate_call = true:warning +csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_constructors = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_methods = true:warning +csharp_style_expression_bodied_operators = true:warning +csharp_style_expression_bodied_properties = true:warning +csharp_style_inlined_variable_declaration = true:warning +csharp_style_pattern_matching_over_as_with_null_check = true:warning +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_throw_expression = true:warning +csharp_style_var_elsewhere = true:warning +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning +# New Line Options +csharp_new_line_before_catch = true:warning +csharp_new_line_before_else = true:warning +csharp_new_line_before_finally = true:warning +csharp_new_line_before_members_in_anonymous_types = true:warning +csharp_new_line_before_members_in_object_initializers = true:warning +# BUG: Warning level cannot be set https://github.com/dotnet/roslyn/issues/18010 +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true:warning +# Spacing Options +csharp_space_after_cast = false:warning +csharp_space_after_colon_in_inheritance_clause = true:warning +csharp_space_after_comma = true:warning +csharp_space_after_dot = false:warning +csharp_space_after_keywords_in_control_flow_statements = true:warning +csharp_space_after_semicolon_in_for_statement = true:warning +csharp_space_around_binary_operators = before_and_after:warning +csharp_space_around_declaration_statements = do_not_ignore:warning +csharp_space_before_colon_in_inheritance_clause = true:warning +csharp_space_before_comma = false:warning +csharp_space_before_dot = false:warning +csharp_space_before_semicolon_in_for_statement = false:warning +csharp_space_before_open_square_brackets = false:warning +csharp_space_between_empty_square_brackets = false:warning +csharp_space_between_method_declaration_name_and_open_parenthesis = false:warning +csharp_space_between_method_declaration_parameter_list_parentheses = false:warning +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false:warning +csharp_space_between_method_call_name_and_opening_parenthesis = false:warning +csharp_space_between_method_call_parameter_list_parentheses = false:warning +csharp_space_between_method_call_empty_parameter_list_parentheses = false:warning +csharp_space_between_parentheses = expressions:warning +csharp_space_between_square_brackets = false:warning +# Wrapping Options +csharp_preserve_single_line_blocks = true:warning +csharp_preserve_single_line_statements = false:warning diff --git a/.env b/.env new file mode 100644 index 00000000..bb89c961 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +#Use this variable to set your Docker Repository to make it easier to push later. +#DOCKER_REGISTRY=your repo name \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 08d4edf9..c26ab807 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug/netcoreapp2.0/Asp2017.dll", + "program": "${workspaceRoot}/bin/Debug/netcoreapp2.1/Asp2017.dll", "args": [], "cwd": "${workspaceRoot}", "stopAtEntry": false, @@ -51,7 +51,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug/netcoreapp2.0/Asp2017.dll", + "program": "${workspaceRoot}/bin/Debug/netcoreapp2.1/Asp2017.dll", "args": [], "cwd": "${workspaceRoot}", "stopAtEntry": false, @@ -82,7 +82,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug/netcoreapp2.0/Asp2017.dll", + "program": "${workspaceRoot}/bin/Debug/netcoreapp2.1/Asp2017.dll", "args": [], "cwd": "${workspaceRoot}", "stopAtEntry": false, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 33256db7..da1cb069 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -8,7 +8,7 @@ "tasks": [ { "taskName": "build", - "args": [ ], + "args": ["Asp2017.sln"], "isBuildCommand": true, "showOutput": "silent", "problemMatcher": "$msCompile" diff --git a/Asp2017.csproj b/Asp2017.csproj index efb619c0..b307acba 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -1,21 +1,23 @@  - netcoreapp2.0 + netcoreapp2.1 true Latest false + Linux + docker-compose.dcproj - - - - - - + + + + + + + + - - - + diff --git a/Asp2017.sln b/Asp2017.sln new file mode 100644 index 00000000..9dcc66c0 --- /dev/null +++ b/Asp2017.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27703.2018 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Asp2017", "Asp2017.csproj", "{BC28E9F7-E6EC-447D-AABD-17683BEAD625}" +EndProject +Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{10B71BFC-C3ED-40B0-BB25-E38F04135E17}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BC28E9F7-E6EC-447D-AABD-17683BEAD625}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC28E9F7-E6EC-447D-AABD-17683BEAD625}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC28E9F7-E6EC-447D-AABD-17683BEAD625}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC28E9F7-E6EC-447D-AABD-17683BEAD625}.Release|Any CPU.Build.0 = Release|Any CPU + {10B71BFC-C3ED-40B0-BB25-E38F04135E17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10B71BFC-C3ED-40B0-BB25-E38F04135E17}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10B71BFC-C3ED-40B0-BB25-E38F04135E17}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10B71BFC-C3ED-40B0-BB25-E38F04135E17}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DE341460-9041-458F-99D5-43FC7572CFA6} + EndGlobalSection +EndGlobal diff --git a/ClientApp/app/_styles.scss b/ClientApp/app/_styles.scss new file mode 100644 index 00000000..87ea278f --- /dev/null +++ b/ClientApp/app/_styles.scss @@ -0,0 +1,49 @@ +$body-bg: #f1f1f1; +$body-color: #111; +$theme-colors: ( + 'primary': #216dad +); +$theme-colors: ( + 'accent': #669ecd +); + +@import 'http://github.com/~bootstrap/scss/bootstrap'; +.panel { + box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), + 0 1px 5px 0 rgba(0, 0, 0, 0.12); + height: 100%; + flex: 1; + background-color: rgba(255, 255, 255, 0.3); + border-radius: 0.25rem; + .title { + background-color: #86afd0; + color: #ffffff; + text-align: center; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; + } + .body { + display: flex; + } +} + +@include media-breakpoint-down(md) { + .panel { + .body { + flex-direction: column; + padding: 0px; + } + .title { + font-size: 1.5rem; + } + } +} + +@include media-breakpoint-up(md) { + .panel { + .body { + flex-direction: row; + padding: 1.5rem; + } + } +} diff --git a/ClientApp/app/_variables.scss b/ClientApp/app/_variables.scss new file mode 100644 index 00000000..313015eb --- /dev/null +++ b/ClientApp/app/_variables.scss @@ -0,0 +1,6 @@ +@import 'http://github.com/styles'; +$header-height: 50px; +$menu-max-width: 25%; +$navbar-default-bg: #312312; +$light-orange: #ff8c00; +$navbar-default-color: $light-orange; diff --git a/ClientApp/app/app.component.html b/ClientApp/app/app.component.html index a3e3bf9b..45ca7f89 100644 --- a/ClientApp/app/app.component.html +++ b/ClientApp/app/app.component.html @@ -1,6 +1,6 @@ -
- -
-
+
+ +
+
diff --git a/ClientApp/app/app.component.scss b/ClientApp/app/app.component.scss index 481063dc..1e7eea64 100644 --- a/ClientApp/app/app.component.scss +++ b/ClientApp/app/app.component.scss @@ -1,63 +1,71 @@ -$navbar-default-bg: #312312; -$light-orange: #ff8c00; -$navbar-default-color: $light-orange; +@import 'http://github.com/variables'; +/* *** Overall APP Styling can go here *** + -------------------------------------------- + Note: This Component has ViewEncapsulation.None so the styles will bleed out -/* Import Bootstrap & Fonts */ -$icon-font-path: '~bootstrap-sass/assets/fonts/bootstrap/'; -@import "http://github.com/~bootstrap-sass/assets/stylesheets/bootstrap"; +*/ +body { + line-height: 18px; + padding-top: $header-height; +} +.body-content { + margin: auto; +} +h1 { + border-bottom: 3px theme-color('accent') solid; + font-size: 24px; +} -/* *** Overall APP Styling can go here *** - -------------------------------------------- - Note: This Component has ViewEncapsulation.None so the styles will bleed out +h2 { + font-size: 20px; +} -*/ -@media (max-width: 767px) { - body { - background: #f1f1f1; - line-height: 18px; - padding-top: 30px; - } +h1, +h2, +h3 { + padding: 3px 0; +} - h1 { - border-bottom: 3px #4189C7 solid; - font-size: 24px; - } +ul { + padding: 10px 25px; +} - h2 { - font-size: 20px; - } +ul li { + padding: 5px 0; +} - h1, h2, h3 { - padding: 3px 0; - } +blockquote { + margin: 25px 10px; + padding: 10px 35px 10px 10px; + border-left: 10px color('green') solid; + background: $gray-100; +} + +blockquote a, +blockquote a:hover { + color: $green; } -@media (min-width: 768px) { +@include media-breakpoint-up(lg) { body { - background: #f1f1f1; - line-height: 18px; - padding-top: 0px; + padding-top: 30px; + } + .body-content { + margin-left: $menu-max-width; } - h1 { - border-bottom: 5px #4189C7 solid; + border-bottom: 5px #4189c7 solid; font-size: 36px; } - h2 { font-size: 30px; } - - h1, h2, h3 { + h1, + h2, + h3 { padding: 10px 0; } } - -ul { padding: 10px 25px; } -ul li { padding: 5px 0; } - -blockquote { margin: 25px 10px; padding: 10px 35px 10px 10px; border-left: 10px #158a15 solid; background: #edffed; } -blockquote a, blockquote a:hover { color: #068006; } diff --git a/ClientApp/app/app.component.ts b/ClientApp/app/app.component.ts index 02b91787..415c196a 100644 --- a/ClientApp/app/app.component.ts +++ b/ClientApp/app/app.component.ts @@ -1,96 +1,103 @@ -import { Component, OnInit, OnDestroy, Inject, ViewEncapsulation, RendererFactory2, PLATFORM_ID } from '@angular/core'; -import { Router, NavigationEnd, ActivatedRoute, PRIMARY_OUTLET } from '@angular/router'; -import { Meta, Title, DOCUMENT, MetaDefinition } from '@angular/platform-browser'; -import { Subscription } from 'rxjs/Subscription'; -import { isPlatformServer } from '@angular/common'; -import { LinkService } from './shared/link.service'; - +import { + Component, + Injector, + OnDestroy, + OnInit, + ViewEncapsulation +} from '@angular/core'; +import { Meta, Title } from '@angular/platform-browser'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; +import { REQUEST } from '@nguniversal/aspnetcore-engine/tokens'; // i18n support import { TranslateService } from '@ngx-translate/core'; -import { REQUEST } from './shared/constants/request'; +import { Subscription } from 'rxjs'; +import { filter, map, mergeMap } from 'rxjs/operators'; +import { LinkService } from './shared/link.service'; @Component({ - selector: 'app', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], + encapsulation: ViewEncapsulation.None }) export class AppComponent implements OnInit, OnDestroy { - - // This will go at the END of your title for example "Home - Angular Universal..." <-- after the dash (-) - private endPageTitle: string = 'Angular Universal and ASP.NET Core Starter'; - // If no Title is provided, we'll use a default one before the dash(-) - private defaultPageTitle: string = 'My App'; - - private routerSub$: Subscription; - - constructor( - private router: Router, - private activatedRoute: ActivatedRoute, - private title: Title, - private meta: Meta, - private linkService: LinkService, - public translate: TranslateService, - @Inject(REQUEST) private request - ) { - // this language will be used as a fallback when a translation isn't found in the current language - translate.setDefaultLang('en'); - - // the lang to use, if the lang isn't available, it will use the current loader to get them - translate.use('en'); - - console.log(`What's our REQUEST Object look like?`); - console.log(`The Request object only really exists on the Server, but on the Browser we can at least see Cookies`); - console.log(this.request); - } - - ngOnInit() { - // Change "Title" on every navigationEnd event - // Titles come from the data.title property on all Routes (see app.routes.ts) - this._changeTitleOnNavigation(); - } - - ngOnDestroy() { - // Subscription clean-up - this.routerSub$.unsubscribe(); + // This will go at the END of your title for example "Home - Angular Universal..." <-- after the dash (-) + private endPageTitle: string = 'Angular Universal and ASP.NET Core Starter'; + // If no Title is provided, we'll use a default one before the dash(-) + private defaultPageTitle: string = 'My App'; + + private routerSub$: Subscription; + private request; + + constructor( + private router: Router, + private activatedRoute: ActivatedRoute, + private title: Title, + private meta: Meta, + private linkService: LinkService, + public translate: TranslateService, + private injector: Injector + ) { + // this language will be used as a fallback when a translation isn't found in the current language + translate.setDefaultLang('en'); + + // the lang to use, if the lang isn't available, it will use the current loader to get them + translate.use('en'); + + this.request = this.injector.get(REQUEST); + + console.log(`What's our REQUEST Object look like?`); + console.log( + `The Request object only really exists on the Server, but on the Browser we can at least see Cookies` + ); + console.log(this.request); + } + + ngOnInit() { + // Change "Title" on every navigationEnd event + // Titles come from the data.title property on all Routes (see app.routes.ts) + this._changeTitleOnNavigation(); + } + + ngOnDestroy() { + // Subscription clean-up + this.routerSub$.unsubscribe(); + } + + private _changeTitleOnNavigation() { + this.routerSub$ = this.router.events + .pipe( + filter(event => event instanceof NavigationEnd), + map(() => this.activatedRoute), + map(route => { + while (route.firstChild) route = route.firstChild; + return route; + }), + filter(route => route.outlet === 'primary'), + mergeMap(route => route.data) + ) + .subscribe(event => { + this._setMetaAndLinks(event); + }); + } + + private _setMetaAndLinks(event) { + // Set Title if available, otherwise leave the default Title + const title = event['title'] + ? `${event['title']} - ${this.endPageTitle}` + : `${this.defaultPageTitle} - ${this.endPageTitle}`; + + this.title.setTitle(title); + + const metaData = event['meta'] || []; + const linksData = event['links'] || []; + + for (let i = 0; i < metaData.length; i++) { + this.meta.updateTag(metaData[i]); } - private _changeTitleOnNavigation() { - - this.routerSub$ = this.router.events - .filter(event => event instanceof NavigationEnd) - .map(() => this.activatedRoute) - .map(route => { - while (route.firstChild) route = route.firstChild; - return route; - }) - .filter(route => route.outlet === 'primary') - .mergeMap(route => route.data) - .subscribe((event) => { - this._setMetaAndLinks(event); - }); + for (let i = 0; i < linksData.length; i++) { + this.linkService.addTag(linksData[i]); } - - private _setMetaAndLinks(event) { - - // Set Title if available, otherwise leave the default Title - const title = event['title'] - ? `${event['title']} - ${this.endPageTitle}` - : `${this.defaultPageTitle} - ${this.endPageTitle}`; - - this.title.setTitle(title); - - const metaData = event['meta'] || []; - const linksData = event['links'] || []; - - for (let i = 0; i < metaData.length; i++) { - this.meta.updateTag(metaData[i]); - } - - for (let i = 0; i < linksData.length; i++) { - this.linkService.addTag(linksData[i]); - } - } - + } } - diff --git a/ClientApp/app/app.module.browser.ts b/ClientApp/app/app.module.browser.ts index ac318d44..5162c3ca 100644 --- a/ClientApp/app/app.module.browser.ts +++ b/ClientApp/app/app.module.browser.ts @@ -1,15 +1,9 @@ import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { APP_BASE_HREF } from '@angular/common'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -import { ORIGIN_URL } from './shared/constants/baseurl.constants'; -import { AppModuleShared } from './app.module'; +import { ORIGIN_URL, REQUEST } from '@nguniversal/aspnetcore-engine/tokens'; +import { PrebootModule } from 'preboot'; import { AppComponent } from './app.component'; -import { REQUEST } from './shared/constants/request'; -import { BrowserTransferStateModule } from '../modules/transfer-state/browser-transfer-state.module'; - -import { BrowserPrebootModule } from 'preboot/browser'; +import { AppModuleShared } from './app.module'; export function getOriginUrl() { return window.location.origin; @@ -21,30 +15,26 @@ export function getRequest() { } @NgModule({ - bootstrap: [AppComponent], - imports: [ - BrowserModule.withServerTransition({ - appId: 'my-app-id' // make sure this matches with your Server NgModule - }), - BrowserPrebootModule.replayEvents(), - BrowserAnimationsModule, - BrowserTransferStateModule, - - // Our Common AppModule - AppModuleShared + bootstrap: [AppComponent], + imports: [ + PrebootModule.withConfig({ appRoot: 'app-root' }), + BrowserAnimationsModule, - ], - providers: [ - { - // We need this for our Http calls since they'll be using an ORIGIN_URL provided in main.server - // (Also remember the Server requires Absolute URLs) - provide: ORIGIN_URL, - useFactory: (getOriginUrl) - }, { - // The server provides these in main.server - provide: REQUEST, - useFactory: (getRequest) - } - ] + // Our Common AppModule + AppModuleShared + ], + providers: [ + { + // We need this for our Http calls since they'll be using an ORIGIN_URL provided in main.server + // (Also remember the Server requires Absolute URLs) + provide: ORIGIN_URL, + useFactory: getOriginUrl + }, + { + // The server provides these in main.server + provide: REQUEST, + useFactory: getRequest + } + ] }) -export class AppModule { } +export class AppModule {} diff --git a/ClientApp/app/app.module.server.ts b/ClientApp/app/app.module.server.ts index 9e22b71e..06af629a 100644 --- a/ClientApp/app/app.module.server.ts +++ b/ClientApp/app/app.module.server.ts @@ -1,37 +1,28 @@ import { NgModule } from '@angular/core'; -import { ServerModule } from '@angular/platform-server'; -import { BrowserModule } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - -import { AppModuleShared } from './app.module'; +import { ServerModule } from '@angular/platform-server'; +import { PrebootModule } from 'preboot'; import { AppComponent } from './app.component'; -import { ServerTransferStateModule } from '../modules/transfer-state/server-transfer-state.module'; -import { TransferState } from '../modules/transfer-state/transfer-state'; +import { AppModuleShared } from './app.module'; -import { ServerPrebootModule } from 'preboot/server'; +import { TransferHttpCacheModule, StateTransferInitializerModule } from '@nguniversal/common'; @NgModule({ bootstrap: [AppComponent], imports: [ - BrowserModule.withServerTransition({ - appId: 'my-app-id' // make sure this matches with your Browser NgModule - }), + // Our Common AppModule + AppModuleShared, + ServerModule, - ServerPrebootModule.recordEvents({ appRoot: 'app' }), + PrebootModule.withConfig({ appRoot: 'app-root' }), NoopAnimationsModule, - ServerTransferStateModule, - - // Our Common AppModule - AppModuleShared + TransferHttpCacheModule, // still needs fixes for 5.0 + // Leave this commented out for now, as it breaks Server-renders + // Looking into fixes for this! - @MarkPieszak + // StateTransferInitializerModule // <-- broken for the time-being with ASP.NET ] }) export class AppModule { - - constructor(private transferState: TransferState) { } - - // Gotcha (needs to be an arrow function) - ngOnBootstrap = () => { - this.transferState.inject(); - } + constructor() {} } diff --git a/ClientApp/app/app.module.ts b/ClientApp/app/app.module.ts index 1c492f1d..a30f46d8 100644 --- a/ClientApp/app/app.module.ts +++ b/ClientApp/app/app.module.ts @@ -1,153 +1,212 @@ -import { NgModule, Inject } from '@angular/core'; -import { RouterModule, PreloadAllModules } from '@angular/router'; -import { CommonModule, APP_BASE_HREF } from '@angular/common'; -import { HttpModule, Http } from '@angular/http'; -import { FormsModule } from '@angular/forms'; - -import { Ng2BootstrapModule } from 'ngx-bootstrap'; - +import { CommonModule } from '@angular/common'; +import { HttpClient, HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { + BrowserModule, + BrowserTransferStateModule +} from '@angular/platform-browser'; +import { PreloadAllModules, RouterModule } from '@angular/router'; +import { ORIGIN_URL } from '@nguniversal/aspnetcore-engine/tokens'; +import { TransferHttpCacheModule } from '@nguniversal/common'; // i18n support -import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; - +import { AccordionModule } from 'ngx-bootstrap'; import { AppComponent } from './app.component'; import { NavMenuComponent } from './components/navmenu/navmenu.component'; -import { HomeComponent } from './containers/home/home.component'; -import { UsersComponent } from './containers/users/users.component'; import { UserDetailComponent } from './components/user-detail/user-detail.component'; import { CounterComponent } from './containers/counter/counter.component'; -// import { ChatComponent } from './containers/chat/chat.component'; -import { NotFoundComponent } from './containers/not-found/not-found.component'; +import { HomeComponent } from './containers/home/home.component'; import { NgxBootstrapComponent } from './containers/ngx-bootstrap-demo/ngx-bootstrap.component'; - +import { NotFoundComponent } from './containers/not-found/not-found.component'; +import { UsersComponent } from './containers/users/users.component'; import { LinkService } from './shared/link.service'; import { UserService } from './shared/user.service'; -// import { ConnectionResolver } from './shared/route.resolver'; -import { ORIGIN_URL } from './shared/constants/baseurl.constants'; -import { TransferHttpModule } from '../modules/transfer-http/transfer-http.module'; -export function createTranslateLoader(http: Http, baseHref) { - // Temporary Azure hack - if (baseHref === null && typeof window !== 'undefined') { - baseHref = window.location.origin; - } - // i18n files are in `wwwroot/assets/` - return new TranslateHttpLoader(http, `${baseHref}/assets/i18n/`, '.json'); +export function createTranslateLoader(http: HttpClient, baseHref) { + // Temporary Azure hack + if (baseHref === null && typeof window !== 'undefined') { + baseHref = window.location.origin; + } + // i18n files are in `wwwroot/assets/` + return new TranslateHttpLoader(http, `${baseHref}/assets/i18n/`, '.json'); } @NgModule({ - declarations: [ - AppComponent, - NavMenuComponent, - CounterComponent, - UsersComponent, - UserDetailComponent, - HomeComponent, - // ChatComponent, - NotFoundComponent, - NgxBootstrapComponent - ], - imports: [ - CommonModule, - HttpModule, - FormsModule, - Ng2BootstrapModule.forRoot(), // You could also split this up if you don't want the Entire Module imported - - TransferHttpModule, // Our Http TransferData method + declarations: [ + AppComponent, + NavMenuComponent, + CounterComponent, + UsersComponent, + UserDetailComponent, + HomeComponent, + NotFoundComponent, + NgxBootstrapComponent + ], + imports: [ + CommonModule, + BrowserModule.withServerTransition({ + appId: 'my-app-id' // make sure this matches with your Server NgModule + }), + HttpClientModule, + TransferHttpCacheModule, + BrowserTransferStateModule, + FormsModule, + ReactiveFormsModule, + AccordionModule.forRoot(), // You could also split this up if you don't want the Entire Module imported - // i18n support - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: (createTranslateLoader), - deps: [Http, [ORIGIN_URL]] - } - }), + // i18n support + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: createTranslateLoader, + deps: [HttpClient, [ORIGIN_URL]] + } + }), - // App Routing - RouterModule.forRoot([ - { - path: '', - redirectTo: 'home', - pathMatch: 'full' - }, - { - path: 'home', component: HomeComponent, + // App Routing + RouterModule.forRoot( + [ + { + path: '', + redirectTo: 'home', + pathMatch: 'full' + }, + { + path: 'home', + component: HomeComponent, - // *** SEO Magic *** - // We're using "data" in our Routes to pass in our <meta> <link> tag information - // 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 - // When you change Routes it will automatically append these to your document for you on the Server-side - // - check out app.component.ts to see how it's doing this - data: { - title: 'Homepage', - meta: [{ name: 'description', content: 'This is an example Description Meta tag!' }], - links: [ - { rel: 'canonical', href: 'http://blogs.example.com/blah/nice' }, - { rel: 'alternate', hreflang: 'es', href: 'http://es.example.com/' } - ] - } - }, - { - path: 'counter', component: CounterComponent, - data: { - title: 'Counter', - meta: [{ name: 'description', content: 'This is an Counter page Description!' }], - links: [ - { rel: 'canonical', href: 'http://blogs.example.com/counter/something' }, - { rel: 'alternate', hreflang: 'es', href: 'http://es.example.com/counter' } - ] - } - }, - { - path: 'users', component: UsersComponent, - data: { - title: 'Users REST example', - meta: [{ name: 'description', content: 'This is User REST API example page Description!' }], - links: [ - { rel: 'canonical', href: 'http://blogs.example.com/chat/something' }, - { rel: 'alternate', hreflang: 'es', href: 'http://es.example.com/users' } - ] - } - }, - { - path: 'ngx-bootstrap', component: NgxBootstrapComponent, - data: { - title: 'Ngx-bootstrap demo!!', - meta: [{ name: 'description', content: 'This is an Demo Bootstrap page Description!' }], - links: [ - { rel: 'canonical', href: 'http://blogs.example.com/bootstrap/something' }, - { rel: 'alternate', hreflang: 'es', href: 'http://es.example.com/bootstrap-demo' } - ] - } - }, + // *** SEO Magic *** + // We're using "data" in our Routes to pass in our <title> <meta> <link> tag information + // 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 + // When you change Routes it will automatically append these to your document for you on the Server-side + // - check out app.component.ts to see how it's doing this + data: { + title: 'Homepage', + meta: [ + { + name: 'description', + content: 'This is an example Description Meta tag!' + } + ], + links: [ + { rel: 'canonical', href: 'http://blogs.example.com/blah/nice' }, + { + rel: 'alternate', + hreflang: 'es', + href: 'http://es.example.com/' + } + ] + } + }, + { + path: 'counter', + component: CounterComponent, + data: { + title: 'Counter', + meta: [ + { + name: 'description', + content: 'This is an Counter page Description!' + } + ], + links: [ + { + rel: 'canonical', + href: 'http://blogs.example.com/counter/something' + }, + { + rel: 'alternate', + hreflang: 'es', + href: 'http://es.example.com/counter' + } + ] + } + }, + { + path: 'users', + component: UsersComponent, + data: { + title: 'Users REST example', + meta: [ + { + name: 'description', + content: 'This is User REST API example page Description!' + } + ], + links: [ + { + rel: 'canonical', + href: 'http://blogs.example.com/chat/something' + }, + { + rel: 'alternate', + hreflang: 'es', + href: 'http://es.example.com/users' + } + ] + } + }, + { + path: 'ngx-bootstrap', + component: NgxBootstrapComponent, + data: { + title: 'Ngx-bootstrap demo!!', + meta: [ + { + name: 'description', + content: 'This is an Demo Bootstrap page Description!' + } + ], + links: [ + { + rel: 'canonical', + href: 'http://blogs.example.com/bootstrap/something' + }, + { + rel: 'alternate', + hreflang: 'es', + href: 'http://es.example.com/bootstrap-demo' + } + ] + } + }, - { path: 'lazy', loadChildren: './containers/lazy/lazy.module#LazyModule'}, + { + path: 'lazy', + loadChildren: './containers/lazy/lazy.module#LazyModule' + }, - { - path: '**', component: NotFoundComponent, - data: { - title: '404 - Not found', - meta: [{ name: 'description', content: '404 - Error' }], - links: [ - { rel: 'canonical', href: 'http://blogs.example.com/bootstrap/something' }, - { rel: 'alternate', hreflang: 'es', href: 'http://es.example.com/bootstrap-demo' } - ] - } - } - ], { - // Router options - useHash: false, - preloadingStrategy: PreloadAllModules, - initialNavigation: 'enabled' - }) - ], - providers: [ - LinkService, - UserService, - // ConnectionResolver, - TranslateModule - ] + { + path: '**', + component: NotFoundComponent, + data: { + title: '404 - Not found', + meta: [{ name: 'description', content: '404 - Error' }], + links: [ + { + rel: 'canonical', + href: 'http://blogs.example.com/bootstrap/something' + }, + { + rel: 'alternate', + hreflang: 'es', + href: 'http://es.example.com/bootstrap-demo' + } + ] + } + } + ], + { + // Router options + useHash: false, + preloadingStrategy: PreloadAllModules, + initialNavigation: 'enabled' + } + ) + ], + providers: [LinkService, UserService, TranslateModule], + bootstrap: [AppComponent] }) -export class AppModuleShared { -} +export class AppModuleShared {} diff --git a/ClientApp/app/components/navmenu/navmenu.component.css b/ClientApp/app/components/navmenu/navmenu.component.css deleted file mode 100644 index 8d86aa03..00000000 --- a/ClientApp/app/components/navmenu/navmenu.component.css +++ /dev/null @@ -1,106 +0,0 @@ -li .glyphicon { - margin-right: 10px; -} - -/* Highlighting rules for nav menu items */ -li.link-active a, -li.link-active a:hover, -li.link-active a:focus { - background-color: #4189C7; - color: white; -} - -/* Keep the nav menu independent of scrolling and on top of other items */ -.main-nav { - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 1; -} - -.icon-bar { - background-color: #4189C7; -} - - -@media (max-width: 767px) { - /* Apply for small displays */ - .main-nav { - width: 100%; - } - - .navbar-brand { - font-size: 14px; - background-color: #f1f1f1; - } - .navbar-toggle { - padding: 0px 5px; - margin-top: 0px; - height: 26px; - } - - .navbar-link { - margin-top: 4px; - margin-left: 45px; - position: fixed; - } - - .navbar-collapse { - background-color: white; - } - - .navbar a { - /* If a menu item's text is too long, truncate it */ - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - padding-right: 5px; - } -} - -@media (min-width: 768px) { - /* On small screens, convert the nav menu to a vertical sidebar */ - .main-nav { - height: 100%; - max-width: 330px; - width: calc(25% - 20px); - } - .navbar { - border-radius: 0px; - border-width: 0px; - height: 100%; - } - .navbar-brand{ - width: 100%; - } - .navbar-link { - display: block; - width: 100% - } - .navbar-header { - float: none; - } - .navbar-collapse { - border-top: 1px solid #444; - padding: 0px; - } - .navbar ul { - float: none; - } - .navbar li { - float: none; - font-size: 15px; - margin: 6px; - } - .navbar li a { - padding: 10px 16px; - border-radius: 4px; - } - .navbar a { - /* If a menu item's text is too long, truncate it */ - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } -} diff --git a/ClientApp/app/components/navmenu/navmenu.component.html b/ClientApp/app/components/navmenu/navmenu.component.html index bd6eadda..ba9ff74e 100644 --- a/ClientApp/app/components/navmenu/navmenu.component.html +++ b/ClientApp/app/components/navmenu/navmenu.component.html @@ -1,50 +1,41 @@ -<div class='main-nav'> - <div class='navbar'> - <div class='navbar-header'> - <div class='navbar-brand'> - <button type='button' class='navbar-toggle' (click)="collapseNavbar()"> - <span class='sr-only'>Toggle navigation</span> - <span class='icon-bar'></span> - <span class='icon-bar'></span> - <span class='icon-bar'></span> - </button> - <a [routerLink]="['/home']" class='navbar-link'>Angular 4 Universal & ASP.NET Core</a> - </div> - </div> - <div class='clearfix'></div> - <div class='navbar-collapse {{collapse}}'> - <ul class='nav navbar-nav'> - <li [routerLinkActive]="['link-active']" (click)="collapseMenu()"> - <a [routerLink]="['/home']"> - <span class='glyphicon glyphicon-home'></span> {{ 'HOME' | translate }} - </a> - </li> - <li [routerLinkActive]="['link-active']" (click)="collapseMenu()"> - <a [routerLink]="['/counter']"> - <span class='glyphicon glyphicon-education'></span> {{ 'COUNTER' | translate }} - </a> - </li> - <li [routerLinkActive]="['link-active']" (click)="collapseMenu()"> - <a [routerLink]="['/users']"> - <span class='glyphicon glyphicon-user'></span> Rest API Demo - </a> - </li> - <li [routerLinkActive]="['link-active']" (click)="collapseMenu()"> - <a [routerLink]="['/ngx-bootstrap']"> - <span class='glyphicon glyphicon-th-large'></span> ngx-Bootstrap demo - </a> - </li> - <li [routerLinkActive]="['link-active']" (click)="collapseMenu()"> - <a [routerLink]="['/lazy']"> - <span class='glyphicon glyphicon-star-empty'></span> Lazy-loaded demo - </a> - </li> - <!-- <li [routerLinkActive]="['link-active']" (click)="collapseMenu()"> - <a [routerLink]="['/chat']"> - <span class='glyphicon glyphicon-comment'></span> Chat - </a> - </li> --> - </ul> - </div> +<nav class="navbar navbar-expand-lg"> + <a [routerLink]="['/home']" class='navbar-brand'>Angular 7 Universal & ASP.NET Core</a> + <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#mainNav" aria-controls="mainNav" aria-expanded="false" + aria-label="Toggle navigation" (click)="collapseNavbar()"> + <i class="fas fa-bars"></i> + </button> + <div class="navbar-collapse {{collapse}}" id="mainNav"> + <ul class="nav flex-column"> + <li class="nav-item" [routerLinkActive]="['link-active']" (click)="collapseMenu()"> + <a class="nav-link" [routerLink]="['/home']"> + <i class="fas fa-home"></i> {{ "HOME" | translate }} + </a> + </li> + <li class="nav-item" [routerLinkActive]="['link-active']" (click)="collapseMenu()"> + <a class="nav-link" [routerLink]="['/counter']"> + <i class="fas fa-graduation-cap"></i> {{ "COUNTER" | translate }} + </a> + </li> + <li class="nav-item" [routerLinkActive]="['link-active']" (click)="collapseMenu()"> + <a class="nav-link" [routerLink]="['/users']"> + <i class="fas fa-user"></i> Rest API Demo + </a> + </li> + <li class="nav-item" [routerLinkActive]="['link-active']" (click)="collapseMenu()"> + <a class="nav-link angular" [routerLink]="['/ngx-bootstrap']"> + <i class="fab fa-angular"></i> ngx-Bootstrap demo + </a> + </li> + <li class="nav-item" [routerLinkActive]="['link-active']" (click)="collapseMenu()"> + <a class="nav-link" [routerLink]="['/lazy']"> + <i class="far fa-star"></i> Lazy-loaded demo + </a> + </li> + <!-- <li class="nav-item" [routerLinkActive]="['link-active']" (click)="collapseMenu()"> + <a class="nav-link" [routerLink]="['/chat']"> + <span class="glyphicon glyphicon-comment"></span> Chat + </a> + </li> --> + </ul> </div> -</div> +</nav> diff --git a/ClientApp/app/components/navmenu/navmenu.component.scss b/ClientApp/app/components/navmenu/navmenu.component.scss new file mode 100644 index 00000000..66115049 --- /dev/null +++ b/ClientApp/app/components/navmenu/navmenu.component.scss @@ -0,0 +1,114 @@ +@import 'http://github.com/variables'; +// Mobile first styling. +/* Apply for small displays */ + +.navbar { + position: fixed; + background-color: theme-color('primary'); + top: 0; + right: 0; + left: 0; + z-index: 1030; +} + +.navbar, +a, +button { + color: $body-bg; +} + +.nav { + padding: 0px; + background-color: white; +} + +.nav-item a { + /* If a menu item's text is too long, truncate it */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-right: 5px; +} + +.nav-link { + display: flex; + svg { + margin-right: 10px; + width: 20px; + height: 20px; + } +} + +.navbar-brand { + font-size: 1.15rem; +} + +li a { + border: 1px solid transparent; + &:hover { + border: 1px solid theme-color-level(primary, 2); + } +} + +li.link-active a, +li.link-active a:hover, +li.link-active a:focus { + background-color: theme-color('accent'); + color: $body-bg; +} + +@include media-breakpoint-up(lg) { + .navbar { + width: 275px; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + background-color: $body-bg; + flex: 0 0 $menu-max-width; + min-width: $menu-max-width; + position: fixed; + display: flex; + flex-direction: column; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 100; + padding: 0px; + align-items: flex-start; + box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1); + } + .navbar, + a, + button { + color: theme-color('primary'); + } + .navbar-brand { + padding: 15px 15px; + font-size: 100%; + line-height: 20px; + height: 50px; + } + .navbar-collapse { + width: 100%; + flex: 1; + align-items: flex-start; + } + .nav { + width: 100%; + border-top: 1px solid #444; + background-color: transparent; + } + .nav-item { + float: none; + font-size: 15px; + margin: 6px; + } + .nav-item a { + padding: 10px 16px; + border-radius: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/ClientApp/app/components/navmenu/navmenu.component.ts b/ClientApp/app/components/navmenu/navmenu.component.ts index ac36f036..d3dadb4d 100644 --- a/ClientApp/app/components/navmenu/navmenu.component.ts +++ b/ClientApp/app/components/navmenu/navmenu.component.ts @@ -1,23 +1,22 @@ import { Component } from '@angular/core'; @Component({ - selector: 'nav-menu', - templateUrl: './navmenu.component.html', - styleUrls: ['./navmenu.component.css'] + selector: 'app-nav-menu', + templateUrl: './navmenu.component.html', + styleUrls: ['./navmenu.component.scss'] }) - export class NavMenuComponent { - collapse: string = "collapse"; + collapse: string = 'collapse'; - collapseNavbar(): void { - if (this.collapse.length > 1) { - this.collapse = ""; - } else { - this.collapse = "collapse"; - } + collapseNavbar(): void { + if (this.collapse.length > 1) { + this.collapse = ''; + } else { + this.collapse = 'collapse'; } + } - collapseMenu() { - this.collapse = "collapse" - } + collapseMenu() { + this.collapse = 'collapse'; + } } diff --git a/ClientApp/app/components/user-detail/user-detail.component.html b/ClientApp/app/components/user-detail/user-detail.component.html index 585bbe40..f43b4bbb 100644 --- a/ClientApp/app/components/user-detail/user-detail.component.html +++ b/ClientApp/app/components/user-detail/user-detail.component.html @@ -1,9 +1,24 @@ -<div *ngIf="user"> - <h2>{{user.name}} details:</h2> - <div><label>id: </label>{{user.id}}</div> +<div class="panel"> + <h2 class="title">{{user.name}} details:</h2> + <form class="body" [formGroup]="userForm" (ngSubmit)="updateUser()"> + <div class="form-group"> + <label for="userId">Id</label> + <input type="input" formControlName="id" class="form-control" id="userId" placeholder="Id" readonly> + </div> + <div class="form-group"> + <label for="userName">Name</label> + <input formControlName="name" placeholder="name" id="userName" class="form-control" placeholder="Name"> + </div> + <div class="d-flex flex-row justify-content-end"> + <button type="submit" class="btn btn-primary">Save</button> + </div> + </form> +</div> +<!-- + <div> + <label>id: </label>{{user.id}}</div> <div> <label>name: </label> <input [(ngModel)]="user.name" placeholder="name" #details="ngModel" /> </div> - <button [class.disabled]="details.pristine" class="btn btn-success" (click)="updateUser(user)">Save</button> -</div> \ No newline at end of file + <button [class.disabled]="details.pristine" class="btn btn-success" (click)="updateUser(user)">Save</button> --> diff --git a/ClientApp/app/components/user-detail/user-detail.component.scss b/ClientApp/app/components/user-detail/user-detail.component.scss new file mode 100644 index 00000000..a7809e89 --- /dev/null +++ b/ClientApp/app/components/user-detail/user-detail.component.scss @@ -0,0 +1,4 @@ +@import 'http://github.com/variables'; +:host-context() { + flex: 1; +} diff --git a/ClientApp/app/components/user-detail/user-detail.component.ts b/ClientApp/app/components/user-detail/user-detail.component.ts index 9db88355..d14dbdc1 100644 --- a/ClientApp/app/components/user-detail/user-detail.component.ts +++ b/ClientApp/app/components/user-detail/user-detail.component.ts @@ -1,22 +1,52 @@ -import { Component, Input } from '@angular/core'; +import { + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges +} from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { IUser } from '../../models/User'; import { UserService } from '../../shared/user.service'; @Component({ - selector: 'user-detail', - templateUrl: './user-detail.component.html' + selector: 'app-user-detail', + styleUrls: ['./user-detail.component.scss'], + templateUrl: './user-detail.component.html' }) -export class UserDetailComponent { - @Input() user: IUser; +export class UserDetailComponent implements OnInit, OnChanges { + @Input() user: IUser; + @Output() userUpdate: EventEmitter<any> = new EventEmitter(); + userForm = new FormGroup({ + id: new FormControl(), + name: new FormControl() + }); + constructor(private userService: UserService) {} - constructor(private userService: UserService) { } + ngOnInit() { + this.userForm.valueChanges + .pipe( + debounceTime(400), + distinctUntilChanged() + ) + .subscribe(user => (this.user = user)); + } + ngOnChanges(changes: SimpleChanges) { + this.userForm.patchValue(this.user); + } - - updateUser(user) { - this.userService.updateUser(user).subscribe(result => { - console.log('Put user result: ', result); - }, error => { - console.log(`There was an issue. ${error._body}.`); - }); - } + updateUser() { + this.userService.updateUser(this.userForm.value).subscribe( + result => { + console.log('Put user result: ', result); + }, + error => { + console.log(`There was an issue. ${error._body}.`); + } + ); + this.userUpdate.emit(this.user); + } } diff --git a/ClientApp/app/containers/counter/counter.component.html b/ClientApp/app/containers/counter/counter.component.html index cc2bbd59..d4dcafe4 100644 --- a/ClientApp/app/containers/counter/counter.component.html +++ b/ClientApp/app/containers/counter/counter.component.html @@ -2,6 +2,8 @@ <h1>Counter</h1> <p>This is a simple example of an Angular 2 component.</p> -<p>Current count: <strong>{{ currentCount }}</strong></p> +<p>Current count: + <strong>{{ currentCount }}</strong> +</p> <button (click)="incrementCounter()">Increment</button> diff --git a/ClientApp/app/containers/counter/counter.component.spec.ts b/ClientApp/app/containers/counter/counter.component.spec.ts index c1e54ed4..960f08d3 100644 --- a/ClientApp/app/containers/counter/counter.component.spec.ts +++ b/ClientApp/app/containers/counter/counter.component.spec.ts @@ -1,29 +1,29 @@ -/// <reference path="../../../../node_modules/@types/jasmine/index.d.ts" /> -import { assert } from 'chai'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { CounterComponent } from './counter.component'; -import { TestBed, async, ComponentFixture } from '@angular/core/testing'; + +import {} from 'jasmine'; let fixture: ComponentFixture<CounterComponent>; describe('Counter component', () => { - beforeEach(() => { - TestBed.configureTestingModule({ declarations: [CounterComponent] }); - fixture = TestBed.createComponent(CounterComponent); - fixture.detectChanges(); - }); + beforeEach(() => { + TestBed.configureTestingModule({ declarations: [CounterComponent] }); + fixture = TestBed.createComponent(CounterComponent); + fixture.detectChanges(); + }); - it('should display a title', async(() => { - const titleText = fixture.nativeElement.querySelector('h1').textContent; - expect(titleText).toEqual('Counter'); - })); + it('should display a title', async(() => { + const titleText = fixture.nativeElement.querySelector('h1').textContent; + expect(titleText).toEqual('Counter'); + })); - it('should start with count 0, then increments by 1 when clicked', async(() => { - const countElement = fixture.nativeElement.querySelector('strong'); - expect(countElement.textContent).toEqual('0'); + it('should start with count 0, then increments by 1 when clicked', async(() => { + const countElement = fixture.nativeElement.querySelector('strong'); + expect(countElement.textContent).toEqual('0'); - const incrementButton = fixture.nativeElement.querySelector('button'); - incrementButton.click(); - fixture.detectChanges(); - expect(countElement.textContent).toEqual('1'); - })); + const incrementButton = fixture.nativeElement.querySelector('button'); + incrementButton.click(); + fixture.detectChanges(); + expect(countElement.textContent).toEqual('1'); + })); }); diff --git a/ClientApp/app/containers/counter/counter.component.ts b/ClientApp/app/containers/counter/counter.component.ts index 69de17d9..42123687 100644 --- a/ClientApp/app/containers/counter/counter.component.ts +++ b/ClientApp/app/containers/counter/counter.component.ts @@ -1,13 +1,13 @@ import { Component } from '@angular/core'; @Component({ - selector: 'counter', - templateUrl: './counter.component.html' + selector: 'app-counter', + templateUrl: './counter.component.html' }) export class CounterComponent { - public currentCount = 0; + public currentCount = 0; - public incrementCounter() { - this.currentCount++; - } + public incrementCounter() { + this.currentCount++; + } } diff --git a/ClientApp/app/containers/home/home.component.html b/ClientApp/app/containers/home/home.component.html index 06d801e7..c3a92f5c 100644 --- a/ClientApp/app/containers/home/home.component.html +++ b/ClientApp/app/containers/home/home.component.html @@ -1,38 +1,32 @@ <h1>{{ title }}</h1> <blockquote> - <strong>Enjoy the latest features from .NET Core & Angular 4.0!</strong> - <br> For more info check the repo here: <a href="https://github.com/MarkPieszak/aspnetcore-angular2-universal">AspNetCore-Angular2-Universal repo</a> - <br><br> + <strong>Enjoy the latest features from .NET Core & Angular 7.x!</strong> + <br> For more info check the repo here: + <a href="https://github.com/MarkPieszak/aspnetcore-angular-universal" target="_blank">AspNetCore-Angular-Universal repo</a> + <br> + <br> </blockquote> <div class="row"> <div class="col-lg-6"> <h2>{{ 'HOME_FEATURE_LIST_TITLE' | translate }} </h2> <ul> - <li>ASP.NET Core 2.0 :: ( Visual Studio 2017 )</li> + <li>ASP.NET Core 2.1 :: ( Visual Studio 2017 )</li> <li> - Angular 4.* front-end UI framework + Angular 7.* front-end UI framework <ul> - <li>Angular **platform-server** (aka: Universal) - server-side rendering for SEO, deep-linking, and - incredible performance.</li> + <li>Angular **platform-server** (aka: Universal) - server-side rendering for SEO, deep-linking, and incredible performance.</li> <!--<li>HMR State Management - Don't lose your applications state during HMR!</li>--> <li>AoT (Ahead-of-time) production compilation for even faster Prod builds.</li> </ul> </li> <li> The latest TypeScript 2.* features - <!--<ul> - <li> - "Path" support example - create your own custom directory paths to avoid `../../` directory diving.<br /> - Check the <a href="https://github.com/MarkPieszak/aspnetcore-angular2-universal/blob/master/tsconfig.json">tsconfig</a> to see how they are setup. - </li> - </ul>--> </li> <li> Webpack <ul> - <!--<li>TS2 aware path support</li>--> <li>Hot Module Reloading/Replacement for an amazing development experience.</li> <li>Tree-shaking</li> </ul> @@ -40,7 +34,6 @@ <h2>{{ 'HOME_FEATURE_LIST_TITLE' | translate }} </h2> <li>Bootstrap (ngx-bootstrap) : Bootstrap capable of being rendered even on the server.</li> <li>Unit testing via karma & jasmine.</li> - <!--<li>e2e testing via protractor.</li>--> </ul> </div> @@ -49,24 +42,44 @@ <h2>{{ 'HOME_FEATURE_LIST_TITLE' | translate }} </h2> <h2>{{ 'HOME_ISSUES_TITLE' | translate }}</h2> <ul> - <li><strong>Issues with this Starter?</strong> <br>Please post an issue here: <a href="https://github.com/MarkPieszak/aspnetcore-angular2-universal">AspNetCore-Angular2-Universal repo</a><br><br></li> + <li> + <strong>Issues with this Starter?</strong> + <br>Please post an issue here: + <a href="https://github.com/MarkPieszak/aspnetcore-angular2-universal" target="_blank">AspNetCore-Angular2-Universal repo</a> + <br> + <br> + </li> <!--<li><strong>Issues with <u>Universal</u> itself?</strong> <br>Please post an issue here: <a href="https://github.com/angular/universal">Angular Universal repo</a></li>--> </ul> + + <h2><a href="https://trilon.io/" target="_blank">Trilon Consulting - Trilon.io</a></h2> + + <strong>Consulting | Development | Training | Workshops</strong> + <br><br> + Get your Team or Application up to speed by working with some of the leading industry experts in JavaScript, Node / NestJS, & ASP.NET!<br> + <br> + <strong>Follow us on Twitter!</strong><br><br> + <a href="http://www.twitter.com/trilon_io" target="_blank">@trilon_io</a> | + <a href="http://www.twitter.com/MarkPieszak" target="_blank">@MarkPieszak</a> + <br> + <br> + </div> </div> + <div class="row"> <div class="col-lg-12"> <h2> {{ 'SWITCH_LANGUAGE' | translate }}</h2> - <button class="btn btn-default" (click)="setLanguage('en')"> - <span class="flag-icon flag-icon-us"></span> {{ 'ENGLISH' | translate }} + <button class="btn btn-outline-secondary mr-2" (click)="setLanguage('en')"> + <span class="flag-icon flag-icon-us"></span> {{ 'ENGLISH' | translate }} </button> - <button class="btn btn-default" (click)="setLanguage('no')"> - <span class="flag-icon flag-icon-no"></span> {{ 'NORWEGIAN' | translate }} + <button class="btn btn-outline-secondary" (click)="setLanguage('no')"> + <span class="flag-icon flag-icon-no"></span> {{ 'NORWEGIAN' | translate }} </button> </div> diff --git a/ClientApp/app/containers/home/home.component.ts b/ClientApp/app/containers/home/home.component.ts index 3065c0b1..850f7b8f 100644 --- a/ClientApp/app/containers/home/home.component.ts +++ b/ClientApp/app/containers/home/home.component.ts @@ -1,25 +1,22 @@ -import { Component, OnInit, Inject } from '@angular/core'; - +import { Component, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; @Component({ - selector: 'app-home', - templateUrl: './home.component.html' + selector: 'app-home', + templateUrl: './home.component.html' }) export class HomeComponent implements OnInit { + title: string = + 'Angular 7.x Universal & ASP.NET Core 2.1 advanced starter-kit'; - title: string = 'Angular 4.0 Universal & ASP.NET Core 2.0 advanced starter-kit'; - - // Use "constructor"s only for dependency injection - constructor( - public translate: TranslateService - ) { } + // Use "constructor"s only for dependency injection + constructor(public translate: TranslateService) {} - // Here you want to handle anything with @Input()'s @Output()'s - // Data retrieval / etc - this is when the Component is "ready" and wired up - ngOnInit() { } + // Here you want to handle anything with @Input()'s @Output()'s + // Data retrieval / etc - this is when the Component is "ready" and wired up + ngOnInit() {} - public setLanguage(lang) { - this.translate.use(lang); - } + public setLanguage(lang) { + this.translate.use(lang); + } } diff --git a/ClientApp/app/containers/lazy/lazy.component.ts b/ClientApp/app/containers/lazy/lazy.component.ts index 25d6d1a4..53327733 100644 --- a/ClientApp/app/containers/lazy/lazy.component.ts +++ b/ClientApp/app/containers/lazy/lazy.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; @Component({ - selector: 'lazy-view', + selector: 'app-lazy-view', template: ` <h1>Lazy-Loaded Component!</h1> <blockquote> @@ -10,4 +10,4 @@ import { Component } from '@angular/core'; </blockquote> ` }) -export class LazyComponent { } +export class LazyComponent {} diff --git a/ClientApp/app/containers/lazy/lazy.module.ts b/ClientApp/app/containers/lazy/lazy.module.ts index 8d468754..aa33a605 100644 --- a/ClientApp/app/containers/lazy/lazy.module.ts +++ b/ClientApp/app/containers/lazy/lazy.module.ts @@ -1,4 +1,4 @@ -import { NgModule, Component } from '@angular/core'; +import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { LazyComponent } from './lazy.component'; @@ -10,6 +10,4 @@ import { LazyComponent } from './lazy.component'; ]) ] }) -export class LazyModule { - -} +export class LazyModule {} diff --git a/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html b/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html index 5f7c3820..2b95c774 100644 --- a/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html +++ b/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html @@ -1,38 +1,35 @@ <h1>Ngx-bootstrap Demo:</h1> <blockquote> - <strong>Here we're using Bootstrap via <a href="https://github.com/valor-software/ngx-bootstrap">ngx-bootstrap</a>, which can even be rendered on the server!</strong> + <strong>Here we're using Bootstrap via + <a href="https://github.com/valor-software/ngx-bootstrap">ngx-bootstrap</a>, which can even be rendered on the server!</strong> <br> </blockquote> <hr> -<br><br> +<br> +<br> <h3>Bootstrap Accordion demo:</h3> <p> - <button type="button" class="btn btn-primary btn-sm" - (click)="group.isOpen = !group.isOpen"> + <button type="button" class="btn btn-primary btn-sm" (click)="group.isOpen = !group.isOpen"> Toggle last panel </button> - <button type="button" class="btn btn-primary btn-sm" - (click)="status.isFirstDisabled = ! status.isFirstDisabled"> + <button type="button" class="btn btn-primary btn-sm ml-2" (click)="status.isFirstDisabled = ! status.isFirstDisabled"> Enable / Disable first panel </button> </p> <div class="checkbox"> <label> - <input type="checkbox" [(ngModel)]="oneAtATime"> - Open only one at a time + <input type="checkbox" [(ngModel)]="oneAtATime"> Open only one at a time </label> </div> <accordion [closeOthers]="oneAtATime"> - <accordion-group heading="Static Header, initially expanded" - [isOpen]="status.isFirstOpen" - [isDisabled]="status.isFirstDisabled"> + <accordion-group heading="Static Header, initially expanded" [isOpen]="status.isFirstOpen" [isDisabled]="status.isFirstDisabled"> This content is straight in the template. </accordion-group> <accordion-group *ngFor="let group of groups" [heading]="group.title"> @@ -46,9 +43,8 @@ <h3>Bootstrap Accordion demo:</h3> <accordion-group #group [isOpen]="status.open"> <div accordion-heading> I can have markup, too! - <i class="pull-right glyphicon" - [ngClass]="{'glyphicon-chevron-down': group?.isOpen, 'glyphicon-chevron-right': !group?.isOpen}"></i> + <i class="pull-right glyphicon" [ngClass]="{'glyphicon-chevron-down': group?.isOpen, 'glyphicon-chevron-right': !group?.isOpen}"></i> </div> This is just some content to illustrate fancy headings. </accordion-group> -</accordion> \ No newline at end of file +</accordion> diff --git a/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts b/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts index a8ba7dec..d1122dad 100644 --- a/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts +++ b/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts @@ -1,36 +1,34 @@ import { Component } from '@angular/core'; @Component({ - selector: 'app-bootstrap', - templateUrl: './ngx-bootstrap.component.html' + selector: 'app-bootstrap', + templateUrl: './ngx-bootstrap.component.html' }) export class NgxBootstrapComponent { + public oneAtATime: boolean = true; + public items = ['Item 1', 'Item 2', 'Item 3']; - public oneAtATime: boolean = true; - public items = ['Item 1', 'Item 2', 'Item 3']; + public status = { + isFirstOpen: true, + isFirstDisabled: false, + open: false + }; - public status = { - isFirstOpen: true, - isFirstDisabled: false, - open: false - }; - - public groups = [ - { - title: 'Angular is neato gang!', - content: 'ASP.NET Core is too :)' - }, - { - title: 'Another One!', - content: 'Some content going here' - } - ]; - - // Use "constructor"s only for dependency injection - constructor() { } - - addItem(): void { - this.items.push(`Items ${this.items.length + 1}`); + public groups = [ + { + title: 'Angular is neato gang!', + content: 'ASP.NET Core is too :)' + }, + { + title: 'Another One!', + content: 'Some content going here' } + ]; + + // Use "constructor"s only for dependency injection + constructor() {} -} \ No newline at end of file + addItem(): void { + this.items.push(`Items ${this.items.length + 1}`); + } +} diff --git a/ClientApp/app/containers/not-found/not-found.component.html b/ClientApp/app/containers/not-found/not-found.component.html index 347dcb93..b0b7072d 100644 --- a/ClientApp/app/containers/not-found/not-found.component.html +++ b/ClientApp/app/containers/not-found/not-found.component.html @@ -1,8 +1,8 @@ <div class="wrapper"> <header class="header header--large"> <h1 class="title">Ahhhhhhhhhhh! This page doesn't exist</h1> - <h2 class="strapline">Not to worry. You can either head back to <a href="http://github.com/">our homepage</a>, or sit there and listen to a goat scream like - a human.</h2> + <h2 class="strapline">Not to worry. You can either head back to + <a href="http://github.com/">our homepage</a>, or sit there and listen to a goat scream like a human.</h2> </header> <div class="content fit-vid vid"> <iframe src="http://www.youtube.com/embed/SIaFtAKnqBU?vq=hd720&rel=0&showinfo=0&controls=0&iv_load_policy=3&loop=1&playlist=SIaFtAKnqBU&modestbranding=1&autoplay=1" diff --git a/ClientApp/app/containers/not-found/not-found.component.ts b/ClientApp/app/containers/not-found/not-found.component.ts index e39faa05..6bd9147a 100644 --- a/ClientApp/app/containers/not-found/not-found.component.ts +++ b/ClientApp/app/containers/not-found/not-found.component.ts @@ -1,11 +1,11 @@ import { Component, OnInit } from '@angular/core'; @Component({ - selector: 'not-found', - templateUrl: './not-found.component.html' + selector: 'app-not-found', + templateUrl: './not-found.component.html' }) export class NotFoundComponent implements OnInit { - constructor() { } + constructor() {} - ngOnInit() { } + ngOnInit() {} } diff --git a/ClientApp/app/containers/users/users.component.css b/ClientApp/app/containers/users/users.component.css deleted file mode 100644 index 4e5440e1..00000000 --- a/ClientApp/app/containers/users/users.component.css +++ /dev/null @@ -1,64 +0,0 @@ -.selected { - background-color: #CFD8DC !important; - color: white; -} - -.users { - margin: 0 0 2em 0; - list-style-type: none; - padding: 0; - width: 15em; -} - - .users li { - cursor: pointer; - position: relative; - left: 0; - background-color: #EEE; - margin: .5em; - padding: .3em 0; - height: 2.4em; - border-radius: 4px; - } - - .users li:hover { - color: #607D8B; - background-color: #DDD; - left: .1em; - } - - .users li.selected:hover { - background-color: #BBD8DC !important; - color: white; - } - - .users .text { - position: relative; - top: -3px; - } - - .users .badge { - display: inline-block; - font-size: small; - color: white; - padding: 0.8em 0.7em 0 0.7em; - background-color: #607D8B; - line-height: 1em; - position: relative; - left: -1px; - top: -4px; - height: 2.5em; - margin-right: .8em; - border-radius: 4px 0 0 4px; - } -button.delete { - background-color: #eee; - border: none; - padding: 5px 10px; - border-radius: 4px; - float: right; - margin-top: 2px; - margin-right: .8em; - background-color: gray !important; - color: white; -} \ No newline at end of file diff --git a/ClientApp/app/containers/users/users.component.html b/ClientApp/app/containers/users/users.component.html index 5bb77691..4b8a7ecc 100644 --- a/ClientApp/app/containers/users/users.component.html +++ b/ClientApp/app/containers/users/users.component.html @@ -1,29 +1,38 @@ -<h1>This is a RestAPI Example (hitting WebAPI in our case)</h1> +<h1>This is a RestAPI Example (hitting WebAPI in our case)</h1> <blockquote> - Let's get some fake users from Rest:<br> - You can find the Web API Routes in <code>{{ "/Server/RestAPI/ ... "}}</code> + Let's get some fake users from Rest: + <br> You can find the Web API Routes in + <code>{{ "/Server/RestAPI/ ... "}}</code> </blockquote> +<div class="panel"> + <h2 class="title">Users</h2> + <div class="body"> + <div class="users-wrapper col-md-5 col-xl-4"> + <p *ngIf="!users"> + <em>Loading...</em> + </p> + <ul class="users"> + <li *ngFor="let user of users" class="user" [class.selected]="user === selectedUser" (click)="onSelect(user)" [@flyInOut]> + <span class="user-id">{{user.id}}</span> + <p> {{user.name}} </p> + <button class="delete-user" (click)="deleteUser(user); $event.stopPropagation()"> + <i class="fas fa-times"></i> + </button> + </li> + </ul> + </div> + <div class="card-wrapper"> + <form class="mb-3" (ngSubmit)="addUser(userName.value); userName.value=''"> + <div class="input-group "> + <input type="text" class="form-control" placeholder="User name:" #userName aria-label="User name:" aria-describedby=""> + <div class="input-group-append"> + <button class="btn btn-primary" type="submit">Add</button> + </div> + </div> + </form> + <app-user-detail (userUpdate)="onUserUpdate($event)" *ngIf="selectedUser" [user]="selectedUser"></app-user-detail> + </div> + </div> -<div> - <label>User name:</label> <input #userName /> - <button class="btn btn-default" (click)="addUser(userName.value); userName.value=''"> - Add - </button> </div> - -<p *ngIf="!users"><em>Loading...</em></p> -<h2>Users</h2> -<ul class="users"> - <li *ngFor="let user of users" - [class.selected]="user === selectedUser" - (click)="onSelect(user)" - [@flyInOut]> - <span class="badge">{{user.id}}</span> {{user.name}} - <button class="delete" - (click)="deleteUser(user); $event.stopPropagation()"> - x - </button> - </li> -</ul> -<user-detail [user]="selectedUser"></user-detail> diff --git a/ClientApp/app/containers/users/users.component.scss b/ClientApp/app/containers/users/users.component.scss new file mode 100644 index 00000000..67acfed0 --- /dev/null +++ b/ClientApp/app/containers/users/users.component.scss @@ -0,0 +1,99 @@ +@import 'http://github.com/variables'; +.users-wrapper { + max-height: 65vh; + overflow-y: auto; + width: 100%; + margin-bottom: 15px; +} + +.card-wrapper { + display: flex; + flex-direction: column; + flex: 1; + padding-right: 15px; + padding-left: 15px; +} + +.users { + list-style-type: none; + display: flex; + flex-direction: column; + margin: 0px; + padding: 0px 2px; +} + +h1 { + font-size: 1.75rem; +} + +.user { + @extend .btn; + border: 0px; + background-color: #d9d9d9; + display: flex; + flex-direction: row; + align-items: center; + padding: 0px; + &:hover { + color: #607d8b; + background-color: #e6e6e6; + left: 0.1em; + } + &.selected { + background-color: #7eaacd !important; + color: white; + } +} + +.users li > * { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.users li p { + flex: 1; + margin: 0px; +} + +.users li.selected:hover { + background-color: #bbd8dc !important; + color: white; +} + +.user-id { + @extend .input-group-text; + background-color: #607d8b; + color: white; + width: 2.5rem; + border: 0px; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; +} + +.delete-user { + @extend .btn, .badge, .badge-secondary; + height: 25px; + width: 25px; + margin-right: 10px; +} + +@include media-breakpoint-down(sm) { + .body { + flex-direction: column-reverse !important; + } +} + +@include media-breakpoint-up(md) { + .users-wrapper { + max-height: 65vh; + overflow-y: auto; + margin-right: 1.25rem; + margin-bottom: 0px; + width: 100%; + } + .body { + flex-direction: column; + } +} diff --git a/ClientApp/app/containers/users/users.component.ts b/ClientApp/app/containers/users/users.component.ts index 9dd28781..b64ea0ee 100644 --- a/ClientApp/app/containers/users/users.component.ts +++ b/ClientApp/app/containers/users/users.component.ts @@ -1,72 +1,87 @@ -import { - Component, OnInit, - // animation imports - trigger, state, style, transition, animate, Inject -} from '@angular/core'; +import { + animate, + state, + style, + transition, + trigger +} from '@angular/animations'; +import { Component, OnInit } from '@angular/core'; import { IUser } from '../../models/User'; import { UserService } from '../../shared/user.service'; @Component({ - selector: 'users', - templateUrl: './users.component.html', - styleUrls: ['./users.component.css'], - animations: [ - // Animation example - // Triggered in the ngFor with [@flyInOut] - trigger('flyInOut', [ - state('in', style({ transform: 'translateY(0)' })), - transition('void => *', [ - style({ transform: 'translateY(-100%)' }), - animate(1000) - ]), - transition('* => void', [ - animate(1000, style({ transform: 'translateY(100%)' })) - ]) - ]) - ] + selector: 'app-users', + templateUrl: './users.component.html', + styleUrls: ['./users.component.scss'], + animations: [ + // Animation example + // Triggered in the ngFor with [@flyInOut] + trigger('flyInOut', [ + state('in', style({ transform: 'translateY(0)' })), + transition('void => *', [ + style({ transform: 'translateY(-100%)' }), + animate(1000) + ]), + transition('* => void', [ + animate(1000, style({ transform: 'translateY(100%)' })) + ]) + ]) + ] }) export class UsersComponent implements OnInit { + users: IUser[]; + selectedUser: IUser; - users: IUser[]; - selectedUser: IUser; + // Use "constructor"s only for dependency injection + constructor(private userService: UserService) {} - // Use "constructor"s only for dependency injection - constructor(private userService: UserService) { } + // Here you want to handle anything with @Input()'s @Output()'s + // Data retrieval / etc - this is when the Component is "ready" and wired up + ngOnInit() { + this.userService.getUsers().subscribe(result => { + console.log('HttpClient [GET] /api/users/allresult', result); + this.users = result; + }); + } - // Here you want to handle anything with @Input()'s @Output()'s - // Data retrieval / etc - this is when the Component is "ready" and wired up - ngOnInit() { - this.userService.getUsers().subscribe(result => { - console.log('Get user result: ', result); - console.log('TransferHttp [GET] /api/users/allresult', result); - this.users = result as IUser[]; - }); - } + onSelect(user: IUser): void { + this.selectedUser = user; + } - onSelect(user: IUser): void { - this.selectedUser = user; - } + deleteUser(user) { + this.clearUser(); + this.userService.deleteUser(user).subscribe( + result => { + console.log('Delete user result: ', result); + let position = this.users.indexOf(user); + this.users.splice(position, 1); + }, + error => { + console.log(`There was an issue. ${error._body}.`); + } + ); + } - deleteUser(user) { - this.userService.deleteUser(user).subscribe(result => { - console.log('Delete user result: ', result); - if (result.ok) { - let position = this.users.indexOf(user); - this.users.splice(position, 1); - } - }, error => { - console.log(`There was an issue. ${error._body}.`); - }); - } + onUserUpdate(user: IUser) { + this.users[this.users.findIndex(u => u.id == user.id)] = user; + } + + addUser(newUserName) { + this.userService.addUser(newUserName).subscribe( + result => { + console.log('Post user result: ', result); + this.users.push(result); + this.selectedUser = result; + }, + error => { + console.log(`There was an issue. ${error._body}.`); + } + ); + } - addUser(newUserName) { - this.userService.addUser(newUserName).subscribe(result => { - console.log('Post user result: ', result); - if (result.ok) { - this.users.push(result.json()); - } - }, error => { - console.log(`There was an issue. ${error._body}.`); - }); + clearUser() { + if (this.selectedUser) { + this.selectedUser = null; } + } } diff --git a/ClientApp/app/models/User.ts b/ClientApp/app/models/User.ts index 53f9df3a..f140ecb1 100644 --- a/ClientApp/app/models/User.ts +++ b/ClientApp/app/models/User.ts @@ -1,4 +1,4 @@ export interface IUser { - id: number; - name: string; -} \ No newline at end of file + id: number; + name: string; +} diff --git a/ClientApp/app/shared/constants/baseurl.constants.ts b/ClientApp/app/shared/constants/baseurl.constants.ts deleted file mode 100644 index 58807bc4..00000000 --- a/ClientApp/app/shared/constants/baseurl.constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { InjectionToken } from '@angular/core'; - -export const ORIGIN_URL = new InjectionToken<string>('ORIGIN_URL'); diff --git a/ClientApp/app/shared/constants/request.ts b/ClientApp/app/shared/constants/request.ts deleted file mode 100644 index 4c553d8a..00000000 --- a/ClientApp/app/shared/constants/request.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { InjectionToken } from '@angular/core'; - -export const REQUEST = new InjectionToken<string>('REQUEST'); diff --git a/ClientApp/app/shared/link.service.ts b/ClientApp/app/shared/link.service.ts index c5a2f16d..2e6754a4 100644 --- a/ClientApp/app/shared/link.service.ts +++ b/ClientApp/app/shared/link.service.ts @@ -6,90 +6,90 @@ * Soon there will be an overall HeadService within Angular that handles Meta/Link everything */ -import { Injectable, PLATFORM_ID, Optional, RendererFactory2, ViewEncapsulation, Inject } from '@angular/core'; -import { DOCUMENT } from '@angular/platform-browser'; import { isPlatformServer } from '@angular/common'; +import { + Inject, + Injectable, + PLATFORM_ID, + RendererFactory2, + ViewEncapsulation +} from '@angular/core'; +import { DOCUMENT } from '@angular/platform-browser'; @Injectable() export class LinkService { - - private isServer: boolean = isPlatformServer(this.platform_id); - - constructor( - private rendererFactory: RendererFactory2, - @Inject(DOCUMENT) private document, - @Inject(PLATFORM_ID) private platform_id - ) { - } - - /** - * Inject the State into the bottom of the <head> - */ - addTag(tag: LinkDefinition, forceCreation?: boolean) { - - try { - const renderer = this.rendererFactory.createRenderer(this.document, { - id: '-1', - encapsulation: ViewEncapsulation.None, - styles: [], - data: {} - }); - - const link = renderer.createElement('link'); - const head = this.document.head; - const selector = this._parseSelector(tag); - - if (head === null) { - throw new Error('<head> not found within DOCUMENT.'); - } - - Object.keys(tag).forEach((prop: string) => { - return renderer.setAttribute(link, prop, tag[prop]); - }); - - // [TODO]: get them to update the existing one (if it exists) ? - renderer.appendChild(head, link); - - } catch (e) { - console.error('Error within linkService : ', e); - } + private isServer: boolean = isPlatformServer(this.platform_id); + + constructor( + private rendererFactory: RendererFactory2, + @Inject(DOCUMENT) private document, + @Inject(PLATFORM_ID) private platform_id + ) {} + + /** + * Inject the State into the bottom of the <head> + */ + addTag(tag: LinkDefinition, forceCreation?: boolean) { + try { + const renderer = this.rendererFactory.createRenderer(this.document, { + id: '-1', + encapsulation: ViewEncapsulation.None, + styles: [], + data: {} + }); + + const link = renderer.createElement('link'); + const head = this.document.head; + const selector = this._parseSelector(tag); + + if (head === null) { + throw new Error('<head> not found within DOCUMENT.'); + } + + Object.keys(tag).forEach((prop: string) => { + return renderer.setAttribute(link, prop, tag[prop]); + }); + + // [TODO]: get them to update the existing one (if it exists) ? + renderer.appendChild(head, link); + } catch (e) { + console.error('Error within linkService : ', e); } - - // updateTag(tag: LinkDefinition, selector?: string) { - // if (!tag) return null; - // selector = selector || this._parseSelector(tag); - // const meta = this.getTag(selector); - // if (meta) { - // return this._setMetaElementAttributes(tag, meta); - // } - // return this._getOrCreateElement(tag, true); - // } - - // getTag(attrSelector: string): HTMLMetaElement { - // if (!attrSelector) return null; - // return this._dom.querySelector(this._doc, `meta[${attrSelector}]`); - // } - - private _parseSelector(tag: LinkDefinition): string { - // Possibly re-work this - const attr: string = tag.rel ? 'rel' : 'hreflang'; - return `${attr}="${tag[attr]}"`; - } - + } + + // updateTag(tag: LinkDefinition, selector?: string) { + // if (!tag) return null; + // selector = selector || this._parseSelector(tag); + // const meta = this.getTag(selector); + // if (meta) { + // return this._setMetaElementAttributes(tag, meta); + // } + // return this._getOrCreateElement(tag, true); + // } + + // getTag(attrSelector: string): HTMLMetaElement { + // if (!attrSelector) return null; + // return this._dom.querySelector(this._doc, `meta[${attrSelector}]`); + // } + + private _parseSelector(tag: LinkDefinition): string { + // Possibly re-work this + const attr: string = tag.rel ? 'rel' : 'hreflang'; + return `${attr}="${tag[attr]}"`; + } } - export declare type LinkDefinition = { - charset?: string; - crossorigin?: string; - href?: string; - hreflang?: string; - media?: string; - rel?: string; - rev?: string; - sizes?: string; - target?: string; - type?: string; + charset?: string; + crossorigin?: string; + href?: string; + hreflang?: string; + media?: string; + rel?: string; + rev?: string; + sizes?: string; + target?: string; + type?: string; } & { - [prop: string]: string; - }; \ No newline at end of file + [prop: string]: string; +}; diff --git a/ClientApp/app/shared/user.service.ts b/ClientApp/app/shared/user.service.ts index 8bcc3bb3..85249249 100644 --- a/ClientApp/app/shared/user.service.ts +++ b/ClientApp/app/shared/user.service.ts @@ -1,42 +1,35 @@ -import { Injectable, Inject } from '@angular/core'; -import { Http, URLSearchParams } from '@angular/http'; -import { APP_BASE_HREF } from '@angular/common'; -import { ORIGIN_URL } from './constants/baseurl.constants'; +import { HttpClient } from '@angular/common/http'; +import { Injectable, Injector } from '@angular/core'; +import { ORIGIN_URL } from '@nguniversal/aspnetcore-engine/tokens'; import { IUser } from '../models/User'; -import { TransferHttp } from '../../modules/transfer-http/transfer-http'; -import { Observable } from 'rxjs/Observable'; @Injectable() export class UserService { - constructor( - private transferHttp: TransferHttp, // Use only for GETS that you want re-used between Server render -> Client render - private http: Http, // Use for everything else - @Inject(ORIGIN_URL) private baseUrl: string) { + private baseUrl: string; - } + constructor(private http: HttpClient, private injector: Injector) { + this.baseUrl = this.injector.get(ORIGIN_URL); + } - getUsers(): Observable<IUser[]> { - // ** TransferHttp example / concept ** - // - Here we make an Http call on the server, save the result on the window object and pass it down with the SSR, - // The Client then re-uses this Http result instead of hitting the server again! + getUsers() { + return this.http.get<IUser[]>(`${this.baseUrl}/api/users`); + } - // NOTE : transferHttp also automatically does .map(res => res.json()) for you, so no need for these calls - return this.transferHttp.get(`${this.baseUrl}/api/users`); - } + getUser(user: IUser) { + return this.http.get<IUser>(`${this.baseUrl}/api/users/` + user.id); + } - getUser(user: IUser): Observable<IUser> { - return this.transferHttp.get(`${this.baseUrl}/api/users/` + user.id); - } + deleteUser(user: IUser) { + return this.http.delete<IUser>(`${this.baseUrl}/api/users/` + user.id); + } - deleteUser(user: IUser): Observable<any> { - return this.http.delete(`${this.baseUrl}/api/users/` + user.id); - } + updateUser(user: IUser) { + return this.http.put<IUser>(`${this.baseUrl}/api/users/` + user.id, user); + } - updateUser(user: IUser): Observable<any> { - return this.http.put(`${this.baseUrl}/api/users/` + user.id, user); - } - - addUser(newUserName: string): Observable<any> { - return this.http.post(`${this.baseUrl}/api/users`, { name: newUserName }) - } + addUser(newUserName: string) { + return this.http.post<IUser>(`${this.baseUrl}/api/users`, { + name: newUserName + }); + } } diff --git a/ClientApp/boot.browser.ts b/ClientApp/boot.browser.ts index a7830543..68de6e5b 100644 --- a/ClientApp/boot.browser.ts +++ b/ClientApp/boot.browser.ts @@ -3,16 +3,14 @@ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module.browser'; -const rootElemTagName = 'app'; // Update this if you change your root component selector - // // Enable either Hot Module Reloading or production mode if (module['hot']) { - module['hot'].accept(); - module['hot'].dispose(() => { - modulePromise.then(appModule => appModule.destroy()); - }); + module['hot'].accept(); + module['hot'].dispose(() => { + modulePromise.then(appModule => appModule.destroy()); + }); } else { - enableProdMode(); + enableProdMode(); } const modulePromise = platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/ClientApp/boot.server.PRODUCTION.ts b/ClientApp/boot.server.PRODUCTION.ts new file mode 100644 index 00000000..e3c33ff0 --- /dev/null +++ b/ClientApp/boot.server.PRODUCTION.ts @@ -0,0 +1,37 @@ +import 'zone.js/dist/zone-node'; +import './polyfills/server.polyfills'; +import { enableProdMode } from '@angular/core'; +import { createServerRenderer } from 'aspnet-prerendering'; + +// Grab the (Node) server-specific NgModule +const { AppModuleNgFactory } = require('./app/app.module.server.ngfactory'); // <-- ignore this - this is Production only +import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from '@nguniversal/aspnetcore-engine'; + +enableProdMode(); + +export default createServerRenderer(params => { + // Platform-server provider configuration + const setupOptions: IEngineOptions = { + appSelector: '<app-root></app-root>', + ngModule: AppModuleNgFactory, + request: params, + providers: [ + // Optional - Any other Server providers you want to pass + // (remember you'll have to provide them for the Browser as well) + ] + }; + + return ngAspnetCoreEngine(setupOptions).then(response => { + // Apply your transferData to response.globals + response.globals.transferData = createTransferScript({ + someData: + 'Transfer this to the client on the window.TRANSFER_CACHE {} object', + fromDotnet: params.data.thisCameFromDotNET // example of data coming from dotnet, in HomeController + }); + + return { + html: response.html, // our <app-root> serialized + globals: response.globals // all of our styles/scripts/meta-tags/link-tags for aspnet to serve up + }; + }); +}); diff --git a/ClientApp/boot.server.ts b/ClientApp/boot.server.ts index 86d65086..b6510018 100644 --- a/ClientApp/boot.server.ts +++ b/ClientApp/boot.server.ts @@ -1,15 +1,12 @@ import 'zone.js/dist/zone-node'; import './polyfills/server.polyfills'; + import { enableProdMode } from '@angular/core'; -import { INITIAL_CONFIG } from '@angular/platform-server'; -import { APP_BASE_HREF } from '@angular/common'; -import { createServerRenderer, RenderResult } from 'aspnet-prerendering'; +import { createTransferScript, IEngineOptions, ngAspnetCoreEngine } from '@nguniversal/aspnetcore-engine'; +import { createServerRenderer } from 'aspnet-prerendering'; -import { ORIGIN_URL } from './app/shared/constants/baseurl.constants'; // Grab the (Node) server-specific NgModule import { AppModule } from './app/app.module.server'; -// Temporary * the engine will be on npm soon (`@universal/ng-aspnetcore-engine`) -import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from './polyfills/temporary-aspnetcore-engine'; enableProdMode(); @@ -17,15 +14,17 @@ export default createServerRenderer((params) => { // Platform-server provider configuration const setupOptions: IEngineOptions = { - appSelector: '<app></app>', + appSelector: '<app-root></app-root>', ngModule: AppModule, request: params, providers: [ - // Optional - Any other Server providers you want to pass (remember you'll have to provide them for the Browser as well) + // Optional - Any other Server providers you want to pass + // (remember you'll have to provide them for the Browser as well) ] }; return ngAspnetCoreEngine(setupOptions).then(response => { + // Apply your transferData to response.globals response.globals.transferData = createTransferScript({ someData: 'Transfer this to the client on the window.TRANSFER_CACHE {} object', @@ -33,8 +32,8 @@ export default createServerRenderer((params) => { }); return ({ - html: response.html, - globals: response.globals + html: response.html, // our <app-root> serialized + globals: response.globals // all of our styles/scripts/meta-tags/link-tags for aspnet to serve up }); }); }); diff --git a/ClientApp/modules/transfer-http/transfer-http.module.ts b/ClientApp/modules/transfer-http/transfer-http.module.ts deleted file mode 100644 index c2875b33..00000000 --- a/ClientApp/modules/transfer-http/transfer-http.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NgModule } from '@angular/core'; -import { Http, HttpModule } from '@angular/http'; -import { TransferHttp } from './transfer-http'; - -@NgModule({ - providers: [ - TransferHttp - ] -}) -export class TransferHttpModule {} diff --git a/ClientApp/modules/transfer-http/transfer-http.ts b/ClientApp/modules/transfer-http/transfer-http.ts deleted file mode 100644 index 3f9b5d84..00000000 --- a/ClientApp/modules/transfer-http/transfer-http.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; -import { ConnectionBackend, Http, Request, RequestOptions, RequestOptionsArgs, Response } from '@angular/http'; -import { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Subject'; -import { TransferState } from '../transfer-state/transfer-state'; -import { isPlatformServer } from '@angular/common'; - -import 'rxjs/add/operator/map'; -import 'rxjs/add/operator/do'; -import 'rxjs/add/observable/fromPromise'; - -@Injectable() -export class TransferHttp { - - private isServer = isPlatformServer(this.platformId); - - constructor( - @Inject(PLATFORM_ID) private platformId, - private http: Http, - protected transferState: TransferState - ) { } - - request(uri: string | Request, options?: RequestOptionsArgs): Observable<any> { - return this.getData(uri, options, (url: string, options: RequestOptionsArgs) => { - return this.http.request(url, options); - }); - } - /** - * Performs a request with `get` http method. - */ - get(url: string, options?: RequestOptionsArgs): Observable<any> { - return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { - return this.http.get(url, options); - }); - } - /** - * Performs a request with `post` http method. - */ - post(url: string, body: any, options?: RequestOptionsArgs): Observable<any> { - return this.getPostData(url, body, options, (url: string, body: any, options: RequestOptionsArgs) => { - return this.http.post(url, body, options); - }); - } - /** - * Performs a request with `put` http method. - */ - put(url: string, body: any, options?: RequestOptionsArgs): Observable<any> { - - return this.getPostData(url, body, options, (url: string, body: any, options: RequestOptionsArgs) => { - return this.http.put(url, body, options); - }); - } - /** - * Performs a request with `delete` http method. - */ - delete(url: string, options?: RequestOptionsArgs): Observable<any> { - return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { - return this.http.delete(url, options); - }); - } - /** - * Performs a request with `patch` http method. - */ - patch(url: string, body: any, options?: RequestOptionsArgs): Observable<any> { - return this.getPostData(url, body, options, (url: string, body: any, options: RequestOptionsArgs) => { - return this.http.patch(url, body.options); - }); - } - /** - * Performs a request with `head` http method. - */ - head(url: string, options?: RequestOptionsArgs): Observable<any> { - return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { - return this.http.head(url, options); - }); - } - /** - * Performs a request with `options` http method. - */ - options(url: string, options?: RequestOptionsArgs): Observable<any> { - return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { - return this.http.options(url, options); - }); - } - - private getData(uri: string | Request, options: RequestOptionsArgs, callback: (uri: string | Request, options?: RequestOptionsArgs) => Observable<Response>) { - - let url = uri; - - if (typeof uri !== 'string') { - url = uri.url; - } - - const key = url + JSON.stringify(options); - - try { - return this.resolveData(key); - - } catch (e) { - return callback(url, options) - .map(res => res.json()) - .do(data => { - if (this.isServer) { - this.setCache(key, data); - } - }); - } - } - - private getPostData(uri: string | Request, body: any, options: RequestOptionsArgs, callback: (uri: string | Request, body: any, options?: RequestOptionsArgs) => Observable<Response>) { - - let url = uri; - - if (typeof uri !== 'string') { - url = uri.url; - } - - const key = url + JSON.stringify(body); - - try { - - return this.resolveData(key); - - } catch (e) { - return callback(uri, body, options) - .map(res => res.json()) - .do(data => { - if (this.isServer) { - this.setCache(key, data); - } - }); - } - } - - private resolveData(key: string) { - const data = this.getFromCache(key); - - if (!data) { - throw new Error(); - } - - return Observable.fromPromise(Promise.resolve(data)); - } - - private setCache(key, data) { - return this.transferState.set(key, data); - } - - private getFromCache(key): any { - return this.transferState.get(key); - } -} diff --git a/ClientApp/modules/transfer-state/browser-transfer-state.module.ts b/ClientApp/modules/transfer-state/browser-transfer-state.module.ts deleted file mode 100644 index 20e11421..00000000 --- a/ClientApp/modules/transfer-state/browser-transfer-state.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NgModule, PLATFORM_ID } from '@angular/core'; -import { TransferState } from './transfer-state'; - -export function getTransferState(): TransferState { - const transferState = new TransferState(); - transferState.initialize(window['TRANSFER_STATE'] || {}); - return transferState; -} - -@NgModule({ - providers: [ - { - provide: TransferState, - useFactory: getTransferState - } - ] -}) -export class BrowserTransferStateModule { - -} diff --git a/ClientApp/modules/transfer-state/server-transfer-state.module.ts b/ClientApp/modules/transfer-state/server-transfer-state.module.ts deleted file mode 100644 index 1a77f653..00000000 --- a/ClientApp/modules/transfer-state/server-transfer-state.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NgModule } from '@angular/core'; -import { ServerTransferState } from './server-transfer-state'; -import { TransferState } from './transfer-state'; - -@NgModule({ - providers: [ - { provide: TransferState, useClass: ServerTransferState } - ] -}) -export class ServerTransferStateModule { - -} diff --git a/ClientApp/modules/transfer-state/server-transfer-state.ts b/ClientApp/modules/transfer-state/server-transfer-state.ts deleted file mode 100644 index b2890b26..00000000 --- a/ClientApp/modules/transfer-state/server-transfer-state.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Injectable, Optional, RendererFactory2, ViewEncapsulation, Inject, PLATFORM_ID } from '@angular/core'; -import { TransferState } from './transfer-state'; -import { PlatformState } from '@angular/platform-server'; -@Injectable() -export class ServerTransferState extends TransferState { - constructor(private state: PlatformState, private rendererFactory: RendererFactory2) { - super(); - } - - /** - * Inject the State into the bottom of the <head> - */ - inject() { - try { - const document: any = this.state.getDocument(); - const transferStateString = JSON.stringify(this.toJson()); - const renderer = this.rendererFactory.createRenderer(document, { - id: '-1', - encapsulation: ViewEncapsulation.None, - styles: [], - data: {} - }); - - const body = document.body; - - const script = renderer.createElement('script'); - renderer.setValue(script, `window['TRANSFER_STATE'] = ${transferStateString}`); - renderer.appendChild(body, script); - } catch (e) { - console.log('Failed to append TRANSFER_STATE to body'); - console.error(e); - } - } - - -} diff --git a/ClientApp/modules/transfer-state/transfer-state.ts b/ClientApp/modules/transfer-state/transfer-state.ts deleted file mode 100644 index cc963b8f..00000000 --- a/ClientApp/modules/transfer-state/transfer-state.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; - -@Injectable() -export class TransferState { - private _map = new Map<string, any>(); - - constructor() { } - - keys() { - return this._map.keys(); - } - - get(key: string): any { - const cachedValue = this._map.get(key); - this._map.delete(key); - return cachedValue; - } - - set(key: string, value: any): Map<string, any> { - return this._map.set(key, value); - } - - toJson(): any { - const obj = {}; - Array.from(this.keys()) - .forEach(key => { - obj[key] = this.get(key); - }); - return obj; - } - - initialize(obj: any): void { - Object.keys(obj) - .forEach(key => { - this.set(key, obj[key]); - }); - } - - inject(): void { } -} diff --git a/ClientApp/polyfills/browser.polyfills.ts b/ClientApp/polyfills/browser.polyfills.ts index fa02ffab..9e6fe1d4 100644 --- a/ClientApp/polyfills/browser.polyfills.ts +++ b/ClientApp/polyfills/browser.polyfills.ts @@ -1,4 +1,8 @@ +// Note: * The order is IMPORTANT! * + import './polyfills.ts'; -import 'zone.js/dist/zone'; import 'reflect-metadata'; +import 'zone.js/dist/zone'; + +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. diff --git a/ClientApp/polyfills/polyfills.ts b/ClientApp/polyfills/polyfills.ts index fe3a6bd6..45b62d73 100644 --- a/ClientApp/polyfills/polyfills.ts +++ b/ClientApp/polyfills/polyfills.ts @@ -1,36 +1,29 @@ - +// Note: * The order is IMPORTANT! * + /*************************************************************************************************** * BROWSER POLYFILLS */ /** IE9, IE10 and IE11 requires all of the following polyfills. **/ - import 'core-js/es6/symbol'; - import 'core-js/es6/object'; - import 'core-js/es6/function'; - import 'core-js/es6/parse-int'; - import 'core-js/es6/parse-float'; - import 'core-js/es6/number'; - import 'core-js/es6/math'; - import 'core-js/es6/string'; - import 'core-js/es6/date'; - import 'core-js/es6/array'; - import 'core-js/es6/regexp'; - import 'core-js/es6/map'; - import 'core-js/es6/set'; +import 'core-js/es6/symbol'; +import 'core-js/es6/object'; +import 'core-js/es6/function'; +import 'core-js/es6/parse-int'; +import 'core-js/es6/parse-float'; +import 'core-js/es6/number'; +import 'core-js/es6/math'; +import 'core-js/es6/string'; +import 'core-js/es6/date'; +import 'core-js/es6/array'; +import 'core-js/es6/regexp'; +import 'core-js/es6/map'; +import 'core-js/es6/set'; + +/** */ +import 'reflect-metadata'; /** IE10 and IE11 requires the following for NgClass support on SVG elements */ // import 'classlist.js'; // Run `npm install --save classlist.js`. - -// import 'web-animations-js'; // Run `npm install --save web-animations-js`. - /** Evergreen browsers require these. **/ import 'core-js/es6/reflect'; -import 'core-js/es7/reflect'; - -/** - * Date, currency, decimal and percent pipes. - * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 - */ -// import 'intl'; // Run `npm install --save intl`. - -import './rx-imports'; +import 'core-js/es7/reflect'; \ No newline at end of file diff --git a/ClientApp/polyfills/rx-imports.ts b/ClientApp/polyfills/rx-imports.ts deleted file mode 100644 index 506f102b..00000000 --- a/ClientApp/polyfills/rx-imports.ts +++ /dev/null @@ -1,30 +0,0 @@ - -/* -=- RxJs imports -=- - * - * Here you can place any RxJs imports so you don't have to constantly - * import them throughout your App :) - * - * This file is automatically imported into `polyfills.ts` (which is imported into browser/server modules) - */ - -// General Operators -import 'rxjs/add/operator/map'; -import 'rxjs/add/operator/do'; -import 'rxjs/add/operator/throttleTime'; -import 'rxjs/add/operator/distinctUntilChanged'; -import 'rxjs/add/operator/switchMap'; -import 'rxjs/add/operator/take'; -import 'rxjs/add/operator/debounceTime'; -import 'rxjs/add/operator/filter'; -import 'rxjs/add/operator/mergeMap'; -import 'rxjs/add/operator/concat'; -import 'rxjs/add/operator/catch'; -import 'rxjs/add/operator/first'; - -// Observable operators -import 'rxjs/add/observable/fromEvent'; -import 'rxjs/add/observable/interval'; -import 'rxjs/add/observable/fromPromise'; -import 'rxjs/add/observable/of'; -import 'rxjs/add/observable/concat'; - diff --git a/ClientApp/polyfills/server.polyfills.ts b/ClientApp/polyfills/server.polyfills.ts index 7d3dc3bf..68867b14 100644 --- a/ClientApp/polyfills/server.polyfills.ts +++ b/ClientApp/polyfills/server.polyfills.ts @@ -1,4 +1,4 @@ -import './polyfills.ts'; +// Note: * The order is IMPORTANT! * +import './polyfills.ts'; -import 'reflect-metadata'; import 'zone.js'; diff --git a/ClientApp/polyfills/temporary-aspnetcore-engine.ts b/ClientApp/polyfills/temporary-aspnetcore-engine.ts deleted file mode 100644 index d3157a1b..00000000 --- a/ClientApp/polyfills/temporary-aspnetcore-engine.ts +++ /dev/null @@ -1,254 +0,0 @@ -/* ********* TEMPORARILY HERE ************** - * - will be on npm soon - - * import { ngAspnetCoreEngine } from `@nguniversal/aspnetcore-engine`; - */ -import { Type, NgModuleFactory, NgModuleRef, ApplicationRef, Provider, CompilerFactory, Compiler } from '@angular/core'; -import { platformServer, platformDynamicServer, PlatformState, INITIAL_CONFIG, renderModuleFactory } from '@angular/platform-server'; -import { ResourceLoader } from '@angular/compiler'; -import * as fs from 'fs'; - -import { REQUEST } from '../app/shared/constants/request'; -import { ORIGIN_URL } from '../app/shared/constants/baseurl.constants'; - -export function createTransferScript(transferData: Object): string { - return `<script>window['TRANSFER_CACHE'] = ${JSON.stringify(transferData)};</script>`; -} - -export class FileLoader implements ResourceLoader { - get(url: string): Promise<string> { - return new Promise((resolve, reject) => { - fs.readFile(url, (err: NodeJS.ErrnoException, buffer: Buffer) => { - if (err) { - return reject(err); - } - - resolve(buffer.toString()); - }); - }); - } -} - -export interface IRequestParams { - location: any; // e.g., Location object containing information '/some/path' - origin: string; // e.g., 'https://example.com:1234/' - url: string; // e.g., '/some/path' - baseUrl: string; // e.g., '' or '/myVirtualDir' - absoluteUrl: string; // e.g., 'https://example.com:1234/some/path' - domainTasks: Promise<any>; - data: any; // any custom object passed through from .NET -} - -export interface IEngineOptions { - appSelector: string; - request: IRequestParams; - ngModule: Type<{}> | NgModuleFactory<{}>; - providers?: Provider[]; -}; - -export function ngAspnetCoreEngine( - options: IEngineOptions -): Promise<{ html: string, globals: { styles: string, title: string, meta: string, transferData?: {}, [key: string]: any } }> { - - options.providers = options.providers || []; - - const compilerFactory: CompilerFactory = platformDynamicServer().injector.get(CompilerFactory); - const compiler: Compiler = compilerFactory.createCompiler([ - { - providers: [ - { provide: ResourceLoader, useClass: FileLoader } - ] - } - ]); - - return new Promise((resolve, reject) => { - - try { - const moduleOrFactory = options.ngModule; - if (!moduleOrFactory) { - throw new Error('You must pass in a NgModule or NgModuleFactory to be bootstrapped'); - } - - const extraProviders = options.providers.concat( - options.providers, - [ - { - provide: INITIAL_CONFIG, - useValue: { - document: options.appSelector, - url: options.request.url - } - }, - { - provide: ORIGIN_URL, - useValue: options.request.origin - }, { - provide: REQUEST, - useValue: options.request.data.request - } - ] - ); - - const platform = platformServer(extraProviders); - - getFactory(moduleOrFactory, compiler) - .then((factory: NgModuleFactory<{}>) => { - - return platform.bootstrapModuleFactory(factory).then((moduleRef: NgModuleRef<{}>) => { - - const state: PlatformState = moduleRef.injector.get(PlatformState); - const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef); - - appRef.isStable - .filter((isStable: boolean) => isStable) - .first() - .subscribe((stable) => { - - // Fire the TransferState Cache - const bootstrap = moduleRef.instance['ngOnBootstrap']; - bootstrap && bootstrap(); - - // The parse5 Document itself - const AST_DOCUMENT = state.getDocument(); - - // Strip out the Angular application - const htmlDoc = state.renderToString(); - - const APP_HTML = htmlDoc.substring( - htmlDoc.indexOf('<body>') + 6, - htmlDoc.indexOf('</body>') - ); - - // Strip out Styles / Meta-tags / Title - const STYLES = []; - const SCRIPTS = []; - const META = []; - const LINKS = []; - let TITLE = ''; - - let STYLES_STRING: string = htmlDoc.indexOf('<style ng-transition') > -1 - ? htmlDoc.substring( - htmlDoc.indexOf('<style ng-transition'), - htmlDoc.lastIndexOf('</style>') + 8) - : null; - - const HEAD = AST_DOCUMENT.head; - - let count = 0; - - for (let i = 0; i < HEAD.children.length; i++) { - let element = HEAD.children[i]; - - if (element.name === 'title') { - TITLE = element.children[0].data; - } - - if (element.name === 'script') { - SCRIPTS.push( - `<script>${element.children[0].data}</script>` - ); - } - - // Broken after 4.0 (worked in rc) - // if (element.name === 'style') { - // let styleTag = '<style '; - // for (let key in element.attribs) { - // if (key) { - // styleTag += `${key}="${element.attribs[key]}">`; - // } - // } - - // styleTag += `${element.children[0].data}</style>`; - // STYLES.push(styleTag); - // } - - if (element.name === 'meta') { - count = count + 1; - let metaString = '<meta'; - for (let key in element.attribs) { - if (key) { - metaString += ` ${key}="${element.attribs[key]}"`; - } - } - META.push(`${metaString} />\n`); - } - - if (element.name === 'link') { - let linkString = '<link'; - for (let key in element.attribs) { - if (key) { - linkString += ` ${key}="${element.attribs[key]}"`; - } - } - LINKS.push(`${linkString} />\n`); - } - } - - resolve({ - html: APP_HTML, - globals: { - styles: STYLES_STRING, - title: TITLE, - scripts: SCRIPTS.join(' '), - meta: META.join(' '), - links: LINKS.join(' ') - } - }); - - moduleRef.destroy(); - - }, (err) => { - // isStable subscription error (Template / code error) - reject(err); - }); - - }, err => { - // bootstrapModuleFactory error - reject(err); - }); - - }, err => { - // getFactory error - reject(err); - }); - - } catch (ex) { - // try/catch error - reject(ex); - } - - }); - -} - -/* ********************** Private / Internal ****************** */ - -const factoryCacheMap = new Map<Type<{}>, NgModuleFactory<{}>>(); -function getFactory( - moduleOrFactory: Type<{}> | NgModuleFactory<{}>, compiler: Compiler -): Promise<NgModuleFactory<{}>> { - - return new Promise<NgModuleFactory<{}>>((resolve, reject) => { - // If module has been compiled AoT - if (moduleOrFactory instanceof NgModuleFactory) { - resolve(moduleOrFactory); - return; - } else { - let moduleFactory = factoryCacheMap.get(moduleOrFactory); - - // If module factory is cached - if (moduleFactory) { - resolve(moduleFactory); - return; - } - - // Compile the module and cache it - compiler.compileModuleAsync(moduleOrFactory) - .then((factory) => { - factoryCacheMap.set(moduleOrFactory, factory); - resolve(factory); - }, (err => { - reject(err); - })); - } - }); -} diff --git a/ClientApp/test/boot-tests.js b/ClientApp/test/boot-tests.js index 647b8c02..51eeb6d3 100644 --- a/ClientApp/test/boot-tests.js +++ b/ClientApp/test/boot-tests.js @@ -12,13 +12,15 @@ const testing = require('@angular/core/testing'); const testingBrowser = require('@angular/platform-browser-dynamic/testing'); // Prevent Karma from running prematurely -__karma__.loaded = function () {}; +__karma__.loaded = function() {}; // First, initialize the Angular testing environment -testing.getTestBed().initTestEnvironment( +testing + .getTestBed() + .initTestEnvironment( testingBrowser.BrowserDynamicTestingModule, testingBrowser.platformBrowserDynamicTesting() -); + ); // Then we find all the tests const context = require.context('../', true, /\.spec\.ts$/); diff --git a/ClientApp/test/karma.conf.js b/ClientApp/test/karma.conf.js index b54ce8f9..6ab0db4b 100644 --- a/ClientApp/test/karma.conf.js +++ b/ClientApp/test/karma.conf.js @@ -1,15 +1,12 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/0.13/config/configuration-file.html -module.exports = function (config) { +module.exports = function(config) { config.set({ basePath: '.', frameworks: ['jasmine'], exclude: [], - files: [ - '../../wwwroot/dist/vendor.js', - './boot-tests.js' - ], + files: ['../../wwwroot/dist/vendor.js', './boot-tests.js'], preprocessors: { './boot-tests.js': ['coverage', 'webpack', 'sourcemap'] }, @@ -45,7 +42,7 @@ module.exports = function (config) { }, // you can define custom flags customLaunchers: { - 'PhantomJS_custom': { + PhantomJS_custom: { base: 'PhantomJS', options: { windowName: 'test-window', @@ -53,7 +50,7 @@ module.exports = function (config) { webSecurityEnabled: false } }, - flags: ['--load-images=true'], + flags: ['--load-images=true'] // debug: true } }, diff --git a/ClientApp/test/webpack.config.test.js b/ClientApp/test/webpack.config.test.js index 3a14d96d..5cbcecc5 100644 --- a/ClientApp/test/webpack.config.test.js +++ b/ClientApp/test/webpack.config.test.js @@ -5,7 +5,7 @@ const webpack = require('webpack'); var path = require('path'); var rootPath = path.join.bind(path, path.resolve(__dirname, '../../')); -module.exports = function (options) { +module.exports = function(options) { return { devtool: 'inline-source-map', resolve: { @@ -13,7 +13,8 @@ module.exports = function (options) { modules: [rootPath('ClientApp'), 'node_modules'] }, module: { - rules: [{ + rules: [ + { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader', @@ -24,7 +25,8 @@ module.exports = function (options) { }, { test: /\.ts$/, - use: [{ + use: [ + { loader: 'awesome-typescript-loader', query: { sourceMap: false, @@ -32,7 +34,7 @@ module.exports = function (options) { compilerOptions: { removeComments: true } - }, + } }, 'angular2-template-loader' ], @@ -58,13 +60,8 @@ module.exports = function (options) { esModules: true }, include: rootPath('ClientApp'), - exclude: [ - /ClientApp\\test/, - /\.(e2e|spec)\.ts$/, - /node_modules/ - ] + exclude: [/ClientApp\\test/, /\.(e2e|spec)\.ts$/, /node_modules/] } - ] }, plugins: [ @@ -91,8 +88,7 @@ module.exports = function (options) { * legacy options go here */ } - }), - + }) ], performance: { hints: false @@ -112,6 +108,5 @@ module.exports = function (options) { clearImmediate: false, setImmediate: false } - }; -} +}; diff --git a/ClientApp/tsconfig.app.json b/ClientApp/tsconfig.app.json index 5e2507db..a082833e 100644 --- a/ClientApp/tsconfig.app.json +++ b/ClientApp/tsconfig.app.json @@ -4,10 +4,11 @@ "outDir": "../out-tsc/app", "module": "es2015", "baseUrl": "", - "types": [] + "sourceMap": true, + "types": ["node"] }, "exclude": [ - "test.ts", + "test.ts", "**/*.spec.ts" ] } diff --git a/ClientApp/tsconfig.spec.json b/ClientApp/tsconfig.spec.json index 584cb0a4..c8fc1d84 100644 --- a/ClientApp/tsconfig.spec.json +++ b/ClientApp/tsconfig.spec.json @@ -5,15 +5,8 @@ "module": "commonjs", "target": "es5", "baseUrl": "", - "types": [ - "jasmine", - "node" - ] + "types": ["jasmine", "node"] }, - "files": [ - ], - "include": [ - "**/*.spec.ts", - "**/*.d.ts" - ] + "files": ["polyfills.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..1aaa4c6a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base +WORKDIR /app +RUN apt-get -qq update && apt-get install -y build-essential +RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - +RUN apt-get install -y nodejs +RUN npm i -g --unsafe-perm node-sass && npm rebuild --unsafe-perm node-sass -f +EXPOSE 80 + +FROM microsoft/dotnet:2.1-sdk AS build +WORKDIR /src +COPY Asp2017.csproj . +RUN apt-get -qq update && apt-get install build-essential -y && apt-get install -my wget gnupg && apt-get -qq -y install bzip2 +RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - +RUN apt-get install -y nodejs +RUN dotnet restore ./Asp2017.csproj +COPY . . +WORKDIR /src/ +RUN dotnet build Asp2017.csproj -c Release -o /app + +FROM build AS publish +RUN dotnet publish Asp2017.csproj -c Release -o /app + +FROM base AS final +WORKDIR /app +COPY --from=publish /app . +ENTRYPOINT ["dotnet", "Asp2017.dll"] diff --git a/Program.cs b/Program.cs new file mode 100644 index 00000000..fdf46454 --- /dev/null +++ b/Program.cs @@ -0,0 +1,47 @@ +using AspCoreServer; +using AspCoreServer.Data; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Threading.Tasks; + +public class Program +{ + public static async Task Main(string[] args) + { + var host = BuildWebHost(args); + using (var scope = host.Services.CreateScope()) + { + var services = scope.ServiceProvider; + + try + { + await EnsureDataStorageIsReady(services); + + } catch (Exception ex) + { + var logger = services.GetRequiredService<ILogger<Program>>(); + logger.LogError(ex, "An error occurred while seeding the database."); + } + } + + host.Run(); + } + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup<Startup>() + .Build(); + + private static async Task EnsureDataStorageIsReady(IServiceProvider services) + { + await CoreEFStartup.InitializeDatabaseAsync(services); + await SimpleContentEFStartup.InitializeDatabaseAsync(services); + await LoggingEFStartup.InitializeDatabaseAsync(services); + } +} diff --git a/README.md b/README.md index e5578667..f52bdf4d 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,47 @@ -# ASP.NET Core 2.0 & Angular 4 (+) advanced starter - with Server-side prerendering (for Angular SEO)! +# ASP.NET Core 2.1 & Angular 7(+) Advanced Starter - PWA & Server-side prerendering (for Angular SEO)! -> [(upcoming) Angular 5.0 demo Branch Here](https://github.com/MarkPieszak/aspnetcore-angular2-universal/tree/angular-5.0-WIP) +--- +<br> <p align="center"> - <img src="http://github.com/docs/architecture.png" alt="ASP.NET Core 2.0 Angular 4+ Starter" title="ASP.NET Core 2.0 Angular 4+ Starter"> + <a href="https://trilon.io/" target="_blank"> + <img width="500" height="auto" src="https://trilon.io/trilon-logo-clear.png" alt="Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training"> + </a> </p> -### Harness the power of Angular 4+, ASP.NET Core 2.0, now with SEO ! + +<h3 align="center"> Made with :heart: by <a href="https://trilon.io/">Trilon.io</a></h3> + +--- + +### Harness the power of Angular 7+, ASP.NET Core 2.1, now with SEO ! Angular SEO in action: <p align="center"> - <img src="http://github.com/docs/angular2-seo.png" alt="ASP.NET Core Angular4 SEO" title="ASP.NET Core Angular4 SEO"> + <img src="http://github.com/docs/angular2-seo.png" alt="ASP.NET Core Angular7 SEO" title="ASP.NET Core Angular7 SEO"> +</p> + +### Angular Universal Application Architecture + +<p align="center"> + <img src="http://github.com/docs/architecture.png" alt="ASP.NET Core 2.1 Angular 7+ Starter" title="ASP.NET Core 2.1 Angular 7+ Starter"> </p> ### What is this repo? Live Demo here: http://aspnetcore-angular2-universal.azurewebsites.net -This repository is maintained by [Angular](https://github.com/angular/angular) and is meant to be an advanced starter -for both ASP.NET Core 2.0 using Angular 4.0+, not only for the client-side, but to be rendered on the server for instant +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 +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 application paints (Note: If you don't need SSR [read here](#faq) on how to disable it). 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). This utilizes all the latest standards, no gulp, no bower, no typings, no manually "building" anything. NPM, Webpack and .NET handle everything for you! +### Join us on Gitter + +[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/aspnetcore-angular2-universal/) + # Table of Contents * [Features](#features) @@ -32,10 +50,10 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual * [Upcoming Features](#upcoming-features) * [Application Structure](#application-structure) * [Gotchas](#gotchas) -* [FAQ](#faq---also-check-out-the-faq-issues-label) +* [FAQ](#faq---also-check-out-the-faq-issues-label-and-the-how-to-issues-label) * [Special Thanks](#special-thanks) * [License](#license) -* [Consulting & Training](#looking-for-angular--aspnet-consulting--training--support) +* [Trilon - Consulting & Training](#trilon---angular--aspnet---consulting--training--development) --- @@ -43,7 +61,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual > These are just some of the features found in this starter! -- ASP.NET 2.0 - VS2017 15.3 support now! +- ASP.NET 2.1 - VS2017 support now! - Azure delpoyment straight from VS2017 - Built in docker support through VS2017 - RestAPI (WebAPI) integration @@ -51,7 +69,8 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - Swagger WebAPI documentation when running in development mode - SignalR Chat demo! (Thanks to [@hakonamatata](https://github.com/hakonamatata)) -- **Angular 4.0.0** : +- **Angular 7.0.0** : + - PWA (Progressive Web App) - (Minimal) Angular-CLI integration - This is to be used mainly for Generating Components/Services/etc. - Usage examples: @@ -65,7 +84,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - Can be easily replaced with bootstrap4 (3 is provided for browser support) - Bootstrap using SCSS / SASS for easy theming / styling! -- **Webpack build system (Webpack 2)** +- **Webpack build system (Webpack 4)** - HMR : Hot Module Reloading/Replacement - Production builds w/ AoT Compilation @@ -77,13 +96,13 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - Codelyzer (for Real-time static code analysis) - VSCode & Atom provide real-time analysis out of the box. -- **ASP.NET Core 2.0** +- **ASP.NET Core 2.1** - Integration with NodeJS to provide pre-rendering, as well as any other Node module asset you want to use. - **Azure** - Microsoft Application Insights setup (for MVC & Web API routing) - - Client-side Angular2 Application Insights integration + - Client-side Angular Application Insights integration - If you're using Azure simply install `npm i -S @markpieszak/ng-application-insights` as a dependencies. - Note: Make sure only the Browser makes these calls ([usage info here](https://github.com/MarkPieszak/angular-application-insights/blob/master/README.md#usage)) - More information here: - https://github.com/MarkPieszak/angular-application-insights @@ -93,9 +112,11 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual instrumentationKey: 'Your-Application-Insights-instrumentationKey' }) ``` - +- **Docker** + - Built in Visual Studio F5 Debugging support + - Uses the very light weight microsoft/dotnet image + - Currently limited to Linux image as there is a bug with running nodejs in a container on Windows. -> Looking for the older 2.x branch? Go [here](https://github.com/MarkPieszak/aspnetcore-angular2-universal/tree/old-2.x-universal-branch) ---- @@ -103,17 +124,19 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual # Getting Started? -- **Make sure you have at least Node 6.x or higher (w/ npm 3+) installed!** -- **This repository uses ASP.Net Core 2.0, which has a hard requirement on .NET Core Runtime 2.0.0 and .NET Core SDK 2.0.0. Please install these items from [here](https://github.com/dotnet/core/blob/master/release-notes/download-archives/2.0.0-download.md)** +- **Make sure you have at least Node 8.11.1 or higher (w/ npm 5+) installed!** +- **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)** ### Visual Studio 2017 -Make sure you have .NET Core 2.0 installed and/or VS2017 15.3. +Make sure you have .NET Core 2.1 installed and/or VS2017 15.3. VS2017 will automatically install all the neccessary npm & .NET dependencies when you open the project. Simply push F5 to start debugging ! +**Docker-Support**: Change the startup project to docker-compose and press F5 + **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. ### Visual Studio Code @@ -139,9 +162,9 @@ export ASPNETCORE_ENVIRONMENT=Development # Upcoming Features: -- Update to use npm [ngAspnetCoreEngine](https://github.com/angular/universal/pull/682) (still need to tweak a few things there) -- Potractor e2e testing -- Add basic Redux State store (Will also hold state durijg HMR builds) +- Clean API / structure / simplify application +- Refactor to latest RxJs pipeable syntax +- Attempt to integrate with Angular-CLI fully ---- @@ -268,7 +291,7 @@ Angular application gets serialized into a String, sent to the Browser, along wi 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! -A more detailed explanation can be found here: [ng-AspnetCore-Engine Readme](https://github.com/angular/universal/tree/master/modules/ng-aspnetcore-engine) +A more detailed explanation can be found here: [ng-AspnetCore-Engine Readme](https://github.com/angular/universal/tree/master/modules/aspnetcore-engine) ```csharp // Prerender / Serialize application @@ -293,7 +316,7 @@ Take a look at the `_Layout.cshtml` file for example, notice how we let .NET han <head> <base href="http://github.com/" /> <!-- Title will be the one you set in your Angular application --> - <title>@ViewData["Title"] - AspNET.Core Angular 4.0.0 (+) starter + @ViewData["Title"] - AspNET.Core Angular 7.0.0 (+) starter @@ -332,9 +355,13 @@ Well now, your Client-side Angular will take over, and you'll have a fully funct # "Gotchas" -- This repository uses ASP.Net Core 2.0, which has a hard requirement on .NET Core Runtime 2.0.0 and .NET Core SDK 2.0.0. Please install these items from [here](https://github.com/dotnet/core/blob/master/release-notes/download-archives/2.0.0-download.md) +- 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) -> When building components in Angular 4 there are a few things to keep in mind. +> When building components in Angular 7 there are a few things to keep in mind. + + - Make sure you provide Absolute URLs when calling any APIs. (The server can't understand relative paths, so `/api/whatever` will fail). + + - 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. - **`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: - 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. @@ -369,6 +396,7 @@ constructor(element: ElementRef, renderer: Renderer2) { - Use a cache that's transferred from server to client (TODO: Point to the example) - Know the difference between attributes and properties in relation to the DOM. - 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. + - Error: `sass-loader` requires `node-sass` >=4: Either in the docker container or localhost run npm rebuild node-sass -f ---- @@ -379,7 +407,7 @@ constructor(element: ElementRef, renderer: Renderer2) { ### How can I disable SSR (Server-side rendering)? Simply comment out the logic within HomeController, and replace `@Html.Raw(ViewData["SpaHtml"])` with just your applications root -AppComponent tag ("app" in our case): ``. +AppComponent tag ("app-root" in our case): ``. > 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`. @@ -395,7 +423,7 @@ This is now possible, with the recently updated Angular Material changes. We do ### How can I use jQuery and/or some jQuery plugins with this repo? > Note: If at all possible, try to avoid using jQuery or libraries dependent on it, as there are -better, more abstract ways of dealing with the DOM in Angular (4+) such as using the Renderer, etc. +better, more abstract ways of dealing with the DOM in Angular (5+) such as using the Renderer, etc. Yes, of course but there are a few things you need to setup before doing this. First, make sure jQuery is included in webpack vendor file, and that you have a webpack Plugin setup for it. `new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' })` @@ -411,7 +439,7 @@ import * as $ from 'jquery'; ### How can I support IE9 through IE11? -To support IE9 through IE11 open the `polyfills.ts` file in the `polyfills` folder and uncomment out the 'import polyfills' as needed. +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`**. ---- @@ -420,6 +448,7 @@ To support IE9 through IE11 open the `polyfills.ts` file in the `polyfills` fold 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. Also thank you to the many Contributors ! +- [@GRIMMR3AP3R](https://github.com/GRIMMR3AP3R) - [@Isaac2004](https://github.com/Isaac2004) - [@AbrarJahin](https://github.com/AbrarJahin) - [@LiverpoolOwen](https://github.com/LiverpoolOwen) @@ -441,18 +470,24 @@ Nothing's ever perfect, but please let me know by creating an issue (make sure t [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](/LICENSE) -Copyright (c) 2016-2017 [Mark Pieszak](https://github.com/MarkPieszak) +Copyright (c) 2016-2019 [Mark Pieszak](https://github.com/MarkPieszak) + +[![Twitter Follow](https://img.shields.io/twitter/follow/MarkPieszak.svg?style=social)](https://twitter.com/MarkPieszak) ---- -# DevHelp.Online - Angular & ASP.NET - Consulting | Training | Development +# Trilon - Angular & ASP.NET - Consulting | Training | Development -Check out **[www.DevHelp.Online](http://DevHelp.Online)** for more info! +Check out **[Trilon.io](https://Trilon.io)** for more info! Twitter [@Trilon_io](http://www.twitter.com/Trilon_io) -Contact us at , and let's talk about your projects needs. +Contact us at , and let's talk about your projects needs. ----- +

+ + Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training + +

-## Follow me online: +## Follow Trilon online: -Twitter: [@MarkPieszak](http://twitter.com/MarkPieszak) | Medium: [@MarkPieszak](https://medium.com/@MarkPieszak) +Twitter: [@Trilon_io](http://twitter.com/Trilon_io) diff --git a/Server/Controllers/HomeController.cs b/Server/Controllers/HomeController.cs index 8a5a77f6..18c423c6 100644 --- a/Server/Controllers/HomeController.cs +++ b/Server/Controllers/HomeController.cs @@ -1,61 +1,47 @@ -using Asp2017.Server.Helpers; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -using Microsoft.AspNetCore.SpaServices.Prerendering; -using Microsoft.AspNetCore.NodeServices; -using Microsoft.Extensions.DependencyInjection; +using Asp2017.Server.Helpers; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http; -using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; using System; -using Asp2017.Server.Models; +using System.Threading.Tasks; namespace AspCoreServer.Controllers { - public class HomeController : Controller - { - [HttpGet] - public async Task Index() - { - var prerenderResult = await Request.BuildPrerender(); - - ViewData["SpaHtml"] = prerenderResult.Html; // our from Angular - ViewData["Title"] = prerenderResult.Globals["title"]; // set our from Angular - ViewData["Styles"] = prerenderResult.Globals["styles"]; // put styles in the correct place - ViewData["Scripts"] = prerenderResult.Globals["scripts"]; // scripts (that were in our header) - ViewData["Meta"] = prerenderResult.Globals["meta"]; // set our <meta> SEO tags - ViewData["Links"] = prerenderResult.Globals["links"]; // set our <link rel="canonical"> etc SEO tags - ViewData["TransferData"] = prerenderResult.Globals["transferData"]; // our transfer data set to window.TRANSFER_CACHE = {}; - - return View(); - } - - [HttpGet] - [Route("sitemap.xml")] - public async Task<IActionResult> SitemapXml() - { - String xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"; - - xml += "<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9/">"; - xml += "<sitemap>"; - xml += "<loc>http://localhost:4251/home</loc>"; - xml += "<lastmod>" + DateTime.Now.ToString("yyyy-MM-dd") + "</lastmod>"; - xml += "</sitemap>"; - xml += "<sitemap>"; - xml += "<loc>http://localhost:4251/counter</loc>"; - xml += "<lastmod>" + DateTime.Now.ToString("yyyy-MM-dd") + "</lastmod>"; - xml += "</sitemap>"; - xml += "</sitemapindex>"; - - return Content(xml, "text/xml"); - - } - - public IActionResult Error() - { - return View(); + public class HomeController : Controller { + protected readonly IHostingEnvironment HostingEnvironment; + public HomeController(IHostingEnvironment hostingEnv) => this.HostingEnvironment = hostingEnv; + + [HttpGet] + public async Task<IActionResult> Index () { + var prerenderResult = await this.Request.BuildPrerender (); + + this.ViewData["SpaHtml"] = prerenderResult.Html; // our <app-root /> from Angular + this.ViewData["Title"] = prerenderResult.Globals["title"]; // set our <title> from Angular + this.ViewData["Styles"] = prerenderResult.Globals["styles"]; // put styles in the correct place + this.ViewData["Scripts"] = prerenderResult.Globals["scripts"]; // scripts (that were in our header) + this.ViewData["Meta"] = prerenderResult.Globals["meta"]; // set our <meta> SEO tags + this.ViewData["Links"] = prerenderResult.Globals["links"]; // set our <link rel="canonical"> etc SEO tags + this.ViewData["TransferData"] = prerenderResult.Globals["transferData"]; // our transfer data set to window.TRANSFER_CACHE = {}; + if (!this.HostingEnvironment.IsDevelopment ()) { + this.ViewData["ServiceWorker"] = "<script>'serviceWorker'in navigator&&navigator.serviceWorker.register('/serviceworker')</script>"; + } + + return View (); + } + + [HttpGet] + [Route("sitemap.xml")] + public IActionResult SitemapXml() => Content($@"<?xml version=""1.0"" encoding=""utf-8""?> + <urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9""> + <url> + <loc>http://localhost:4251/home</loc> + <lastmod>{ DateTime.Now.ToString("yyyy-MM-dd")}</lastmod> + </url> + <url> + <loc>http://localhost:4251/counter</loc> + <lastmod>{DateTime.Now.ToString("yyyy-MM-dd")}</lastmod> + </url> + </urlset>", "text/xml"); + + public IActionResult Error() => View(); } - } } diff --git a/Server/Data/CoreEFStartup.cs b/Server/Data/CoreEFStartup.cs new file mode 100644 index 00000000..d3947ef1 --- /dev/null +++ b/Server/Data/CoreEFStartup.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace AspCoreServer.Data +{ + public static class CoreEFStartup + { + public static async Task InitializeDatabaseAsync(IServiceProvider services) + { + var context = services.GetRequiredService<SpaDbContext>(); + + await context.Database.EnsureCreatedAsync(); + } + + } +} diff --git a/Server/Data/DbInitializer.cs b/Server/Data/DbInitializer.cs deleted file mode 100644 index bca0eed3..00000000 --- a/Server/Data/DbInitializer.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Linq; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using AspCoreServer.Models; -using AspCoreServer; - -namespace AspCoreServer.Data -{ - public static class DbInitializer - { - public static void Initialize(SpaDbContext context) - { - context.Database.EnsureCreated(); - - if (context.User.Any()) - { - return; // DB has been seeded - } - var users = new User[] - { - new User(){Name = "Mark Pieszak"}, - new User(){Name = "Abrar Jahin"}, - new User(){Name = "hakonamatata"}, - new User(){Name = "LiverpoolOwen"}, - new User(){Name = "Ketrex"}, - new User(){Name = "markwhitfeld"}, - new User(){Name = "daveo1001"}, - new User(){Name = "paonath"}, - new User(){Name = "nalex095"}, - new User(){Name = "ORuban"}, - new User(){Name = "Gaulomatic"} - }; - - foreach (User s in users) - { - context.User.Add(s); - } - context.SaveChanges(); - } - } -} diff --git a/Server/Data/LoggingEFStartup.cs b/Server/Data/LoggingEFStartup.cs new file mode 100644 index 00000000..edf33498 --- /dev/null +++ b/Server/Data/LoggingEFStartup.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; + +namespace AspCoreServer.Data +{ + public static class LoggingEFStartup + { + public static async Task InitializeDatabaseAsync(IServiceProvider services) + { + //Implent to your hearts' content + } + } +} diff --git a/Server/Data/SimpleContentEFStartup.cs b/Server/Data/SimpleContentEFStartup.cs new file mode 100644 index 00000000..db160dee --- /dev/null +++ b/Server/Data/SimpleContentEFStartup.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using AspCoreServer.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace AspCoreServer.Data +{ + public static class SimpleContentEFStartup + { + public static async Task InitializeDatabaseAsync(IServiceProvider services) + { + var context = services.GetRequiredService<SpaDbContext>(); + + + if (await context.User.AnyAsync()) + { + return; // DB has been seeded + } + var users = new User[] { + new User () { Name = "Mark Pieszak" }, + new User () { Name = "Abrar Jahin" }, + new User () { Name = "hakonamatata" }, + new User () { Name = "LiverpoolOwen" }, + new User () { Name = "Ketrex" }, + new User () { Name = "markwhitfeld" }, + new User () { Name = "daveo1001" }, + new User () { Name = "paonath" }, + new User () { Name = "nalex095" }, + new User () { Name = "ORuban" }, + new User () { Name = "Gaulomatic" }, + new User () { Name = "GRIMMR3AP3R" } + }; + await context.User.AddRangeAsync(users); + + await context.SaveChangesAsync(); + } + } +} diff --git a/Server/Data/SpaDbContext.cs b/Server/Data/SpaDbContext.cs index e6fee5f7..0f90c230 100644 --- a/Server/Data/SpaDbContext.cs +++ b/Server/Data/SpaDbContext.cs @@ -1,14 +1,10 @@ using AspCoreServer.Models; using Microsoft.EntityFrameworkCore; -namespace AspCoreServer.Data -{ - public class SpaDbContext : DbContext - { - public SpaDbContext(DbContextOptions<SpaDbContext> options) - : base(options) - { - Database.EnsureCreated(); +namespace AspCoreServer.Data { + public class SpaDbContext : DbContext { + public SpaDbContext (DbContextOptions<SpaDbContext> options) : base (options) { + Database.EnsureCreated (); } //List of DB Models - Add your DB models here diff --git a/Server/Helpers/HttpRequestExtensions.cs b/Server/Helpers/HttpRequestExtensions.cs index 12648b60..81c613b9 100644 --- a/Server/Helpers/HttpRequestExtensions.cs +++ b/Server/Helpers/HttpRequestExtensions.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using Asp2017.Server.Models; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -5,61 +9,35 @@ using Microsoft.AspNetCore.NodeServices; using Microsoft.AspNetCore.SpaServices.Prerendering; using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Asp2017.Server.Helpers -{ - public static class HttpRequestExtensions - { - public static IRequest AbstractRequestInfo(this HttpRequest request) - { - - IRequest requestSimplified = new IRequest(); - requestSimplified.cookies = request.Cookies; - requestSimplified.headers = request.Headers; - requestSimplified.host = request.Host; - - return requestSimplified; - } - - public static async Task<RenderToStringResult> BuildPrerender(this HttpRequest Request) - { - var nodeServices = Request.HttpContext.RequestServices.GetRequiredService<INodeServices>(); - var hostEnv = Request.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>(); - - var applicationBasePath = hostEnv.ContentRootPath; - var requestFeature = Request.HttpContext.Features.Get<IHttpRequestFeature>(); - var unencodedPathAndQuery = requestFeature.RawTarget; - var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}"; - - // ** TransferData concept ** - // Here we can pass any Custom Data we want ! - - // By default we're passing down Cookies, Headers, Host from the Request object here - TransferData transferData = new TransferData(); - transferData.request = Request.AbstractRequestInfo(); - transferData.thisCameFromDotNET = "Hi Angular it's asp.net :)"; - // Add more customData here, add it to the TransferData class - - //Prerender now needs CancellationToken - System.Threading.CancellationTokenSource cancelSource = new System.Threading.CancellationTokenSource(); - System.Threading.CancellationToken cancelToken = cancelSource.Token; - // Prerender / Serialize application (with Universal) - return await Prerenderer.RenderToString( +namespace Asp2017.Server.Helpers { + public static class HttpRequestExtensions { + public static IRequest AbstractRequestInfo(this HttpRequest request) => new IRequest() + { + cookies = request.Cookies, + headers = request.Headers, + host = request.Host + }; + + public static async Task<RenderToStringResult> BuildPrerender(this HttpRequest request) => + // Prerender / Serialize application (with Universal) + await Prerenderer.RenderToString( "/", - nodeServices, - cancelToken, - new JavaScriptModuleExport(applicationBasePath + "/ClientApp/dist/main-server"), - unencodedAbsoluteUrl, - unencodedPathAndQuery, - transferData, // Our simplified Request object & any other CustommData you want to send! + request.HttpContext.RequestServices.GetRequiredService<INodeServices>(), + new System.Threading.CancellationTokenSource().Token, + new JavaScriptModuleExport(request.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>().ContentRootPath + "/ClientApp/dist/main-server"), + $"{request.Scheme}://{request.Host}{request.HttpContext.Features.Get<IHttpRequestFeature>().RawTarget}", + request.HttpContext.Features.Get<IHttpRequestFeature>().RawTarget, + // ** TransferData concept ** + // Here we can pass any Custom Data we want ! + // By default we're passing down Cookies, Headers, Host from the Request object here + new TransferData + { + request = request.AbstractRequestInfo(), + thisCameFromDotNET = "Hi Angular it's asp.net :)" + }, // Our simplified Request object & any other CustommData you want to send! 30000, - Request.PathBase.ToString() + request.PathBase.ToString() ); } - } } diff --git a/Server/Models/IRequest.cs b/Server/Models/IRequest.cs index 407cf571..295c88fb 100644 --- a/Server/Models/IRequest.cs +++ b/Server/Models/IRequest.cs @@ -3,12 +3,10 @@ using System.Linq; using System.Threading.Tasks; -namespace Asp2017.Server.Models -{ - public class IRequest - { - public object cookies { get; set; } - public object headers { get; set; } - public object host { get; set; } - } +namespace Asp2017.Server.Models { + public class IRequest { + public object cookies { get; set; } + public object headers { get; set; } + public object host { get; set; } + } } diff --git a/Server/Models/TransferData.cs b/Server/Models/TransferData.cs index a89ab38d..c144d2df 100644 --- a/Server/Models/TransferData.cs +++ b/Server/Models/TransferData.cs @@ -3,13 +3,11 @@ using System.Linq; using System.Threading.Tasks; -namespace Asp2017.Server.Models -{ - public class TransferData - { - public dynamic request { get; set; } +namespace Asp2017.Server.Models { + public class TransferData { + public dynamic request { get; set; } - // Your data here ? - public object thisCameFromDotNET { get; set; } - } + // Your data here ? + public object thisCameFromDotNET { get; set; } + } } diff --git a/Server/Models/User.cs b/Server/Models/User.cs index fb3d74da..56e388bf 100644 --- a/Server/Models/User.cs +++ b/Server/Models/User.cs @@ -1,20 +1,17 @@ using System; using System.ComponentModel.DataAnnotations; -namespace AspCoreServer.Models -{ - public class User - { +namespace AspCoreServer.Models { + public class User { public int ID { get; set; } public string Name { get; set; } - [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] - [DataType(DataType.Date)] + [DisplayFormat (DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] + [DataType (DataType.Date)] public DateTime EntryTime { get; set; } //Setting Default value - public User() - { + public User () { EntryTime = DateTime.Now; } } diff --git a/Server/RestAPI/UsersController.cs b/Server/RestAPI/UsersController.cs index 2938eb8e..a23301e0 100644 --- a/Server/RestAPI/UsersController.cs +++ b/Server/RestAPI/UsersController.cs @@ -1,123 +1,95 @@ -using AspCoreServer.Data; +using System; +using System.Linq; +using System.Threading.Tasks; +using AspCoreServer.Data; using AspCoreServer.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using System; -using System.Linq; -using System.Threading.Tasks; -namespace AspCoreServer.Controllers -{ - [Route("api/[controller]")] - public class UsersController : Controller - { - private readonly SpaDbContext _context; +namespace AspCoreServer.Controllers { + [Route ("api/[controller]")] + public class UsersController : Controller { + private readonly SpaDbContext context; - public UsersController(SpaDbContext context) - { - _context = context; - } + public UsersController(SpaDbContext context) => this.context = context; - [HttpGet] - public async Task<IActionResult> Get(int currentPageNo = 1, int pageSize = 20) - { - var users = await _context.User - .OrderByDescending(u => u.EntryTime) - .Skip((currentPageNo - 1) * pageSize) - .Take(pageSize) - .ToArrayAsync(); + [HttpGet] + public async Task<IActionResult> Get (int currentPageNo = 1, int pageSize = 20) { + var users = await this.context.User + .OrderByDescending (u => u.EntryTime) + .Skip ((currentPageNo - 1) * pageSize) + .Take (pageSize) + .ToArrayAsync (); - if (!users.Any()) - { - return NotFound("Users not Found"); - } - else - { - return Ok(users); - } - } + if (!users.Any ()) { + return NotFound ("Users not Found"); + } else { + return Ok (users); + } + } - [HttpGet("{id}")] - public async Task<IActionResult> Get(int id) - { - var user = await _context.User - .Where(u => u.ID == id) - .AsNoTracking() - .SingleOrDefaultAsync(m => m.ID == id); + [HttpGet ("{id}")] + public async Task<IActionResult> Get (int id) { + var user = await this.context.User + .Where (u => u.ID == id) + .AsNoTracking () + .SingleOrDefaultAsync (m => m.ID == id); - if (user == null) - { - return NotFound("User not Found"); - } - else - { - return Ok(user); - } - } + if (user == null) { + return NotFound ("User not Found"); + } else { + return Ok (user); + } + } - [HttpPost] - public async Task<IActionResult> Post([FromBody]User user) - { - if (!string.IsNullOrEmpty(user.Name)) - { - _context.Add(user); - await _context.SaveChangesAsync(); - return CreatedAtAction("Post", user); - } - else - { - return BadRequest("User's name was not given"); - } - } + [HttpPost] + public async Task<IActionResult> Post ([FromBody] User user) { + if (!string.IsNullOrEmpty (user.Name)) { + this.context.Add (user); + await this.context.SaveChangesAsync (); + return CreatedAtAction ("Post", user); + } else { + return BadRequest ("User's name was not given"); + } + } - [HttpPut("{id}")] - public async Task<IActionResult> Put(int id, [FromBody]User userUpdateValue) - { - try - { - userUpdateValue.EntryTime = DateTime.Now; + [HttpPut ("{id}")] + public async Task<IActionResult> Put (int id, [FromBody] User userUpdateValue) { + try { + userUpdateValue.EntryTime = DateTime.Now; - var userToEdit = await _context.User - .AsNoTracking() - .SingleOrDefaultAsync(m => m.ID == id); + var userToEdit = await context.User + .AsNoTracking () + .SingleOrDefaultAsync (m => m.ID == id); - if (userToEdit == null) - { - return NotFound("Could not update user as it was not Found"); + if (userToEdit == null) { + return NotFound ("Could not update user as it was not Found"); + } else { + this.context.Update (userUpdateValue); + await this.context.SaveChangesAsync (); + return Json ("Updated user - " + userUpdateValue.Name); + } + } catch (DbUpdateException) { + //Log the error (uncomment ex variable name and write a log.) + this.ModelState.AddModelError ("", "Unable to save changes. " + + "Try again, and if the problem persists, " + + "see your system administrator."); + return NotFound ("User not Found"); + } } - else - { - _context.Update(userUpdateValue); - await _context.SaveChangesAsync(); - return Ok("Updated user - " + userUpdateValue.Name); - } - } - catch (DbUpdateException) - { - //Log the error (uncomment ex variable name and write a log.) - ModelState.AddModelError("", "Unable to save changes. " + - "Try again, and if the problem persists, " + - "see your system administrator."); - return NotFound("User not Found"); - } - } - [HttpDelete("{id}")] - public async Task<IActionResult> Delete(int id) - { - var userToRemove = await _context.User - .AsNoTracking() - .SingleOrDefaultAsync(m => m.ID == id); - if (userToRemove == null) - { - return NotFound("Could not delete user as it was not Found"); - } - else - { - _context.User.Remove(userToRemove); - await _context.SaveChangesAsync(); - return Ok("Deleted user - " + userToRemove.Name); - } + [HttpDelete ("{id}")] + public async Task<IActionResult> Delete (int id) { + var userToRemove = await this.context.User + .AsNoTracking () + .SingleOrDefaultAsync (m => m.ID == id); + if (userToRemove == null) { + return NotFound ("Could not delete user as it was not Found"); + } else { + this.context.User.Remove (userToRemove); + await this.context.SaveChangesAsync (); + return Json ("Deleted user - " + userToRemove.Name); + } + } } - } } diff --git a/Startup.cs b/Startup.cs index 7736f9e9..8d978fd4 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,120 +1,109 @@ -using System; +using System; using System.IO; +using AspCoreServer.Data; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SpaServices.Webpack; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.EntityFrameworkCore; -using AspCoreServer.Data; +using Microsoft.Net.Http.Headers; using Swashbuckle.AspNetCore.Swagger; - -namespace AspCoreServer -{ - public class Startup - { - - public static void Main(string[] args) - { - var host = new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseStartup<Startup>() - .Build(); - - host.Run(); - } - public Startup(IHostingEnvironment env) - { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .AddEnvironmentVariables(); - Configuration = builder.Build(); - } - - public IConfigurationRoot Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - // Add framework services. - services.AddMvc(); - services.AddNodeServices(); - - var connectionStringBuilder = new Microsoft.Data.Sqlite.SqliteConnectionStringBuilder { DataSource = "spa.db" }; - var connectionString = connectionStringBuilder.ToString(); - - services.AddDbContext<SpaDbContext>(options => - options.UseSqlite(connectionString)); - - // Register the Swagger generator, defining one or more Swagger documents - services.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", new Info { Title = "Angular 4.0 Universal & ASP.NET Core advanced starter-kit web API", Version = "v1" }); - }); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, SpaDbContext context) - { - loggerFactory.AddConsole(Configuration.GetSection("Logging")); - loggerFactory.AddDebug(); - - app.UseStaticFiles(); - - DbInitializer.Initialize(context); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions - { - HotModuleReplacement = true, - HotModuleReplacementEndpoint = "/dist/__webpack_hmr" - }); - app.UseSwagger(); - app.UseSwaggerUI(c => - { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); - }); - - // Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint. - - - app.MapWhen(x => !x.Request.Path.Value.StartsWith("/swagger", StringComparison.OrdinalIgnoreCase), builder => - { - builder.UseMvc(routes => - { - routes.MapSpaFallbackRoute( - name: "spa-fallback", - defaults: new { controller = "Home", action = "Index" }); - }); - }); - } - else - { - app.UseMvc(routes => - { - routes.MapRoute( - name: "default", - template: "{controller=Home}/{action=Index}/{id?}"); - - routes.MapRoute( - "Sitemap", - "sitemap.xml", - new { controller = "Home", action = "SitemapXml" }); - - routes.MapSpaFallbackRoute( - name: "spa-fallback", - defaults: new { controller = "Home", action = "Index" }); - - }); - app.UseExceptionHandler("/Home/Error"); - } +using WebEssentials.AspNetCore.Pwa; + +namespace AspCoreServer { + public class Startup { + public Startup (IHostingEnvironment env) { + var builder = new ConfigurationBuilder () + .SetBasePath (env.ContentRootPath) + .AddJsonFile ("appsettings.json", optional : true, reloadOnChange : true) + .AddJsonFile ($"appsettings.{env.EnvironmentName}.json", optional : true) + .AddEnvironmentVariables (); + this.Configuration = builder.Build (); + } + + public IConfigurationRoot Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices (IServiceCollection services) { + // Add framework services. + services.AddMvc (); + services.AddNodeServices (); + services.AddHttpContextAccessor (); + services.AddProgressiveWebApp (new PwaOptions { Strategy = ServiceWorkerStrategy.CacheFirst, RegisterServiceWorker = true, RegisterWebmanifest = true }, "manifest.json"); + + var connectionStringBuilder = new Microsoft.Data.Sqlite.SqliteConnectionStringBuilder { DataSource = "spa.db" }; + var connectionString = connectionStringBuilder.ToString(); + + services.AddDbContext<SpaDbContext>(options => + options.UseSqlite(connectionString)); + + // Register the Swagger generator, defining one or more Swagger documents + services.AddSwaggerGen (c => { + c.SwaggerDoc ("v1", new Info { Title = "Angular 7.0 Universal & ASP.NET Core advanced starter-kit web API", Version = "v1" }); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure (IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, SpaDbContext context) { + loggerFactory.AddConsole (this.Configuration.GetSection ("Logging")); + loggerFactory.AddDebug (); + + // app.UseStaticFiles(); + + app.UseStaticFiles (new StaticFileOptions () { + OnPrepareResponse = c => { + //Do not add cache to json files. We need to have new versions when we add new translations. + c.Context.Response.GetTypedHeaders ().CacheControl = !c.Context.Request.Path.Value.Contains (".json") + ? new CacheControlHeaderValue () { + MaxAge = TimeSpan.FromDays (30) // Cache everything except json for 30 days + } + : new CacheControlHeaderValue () { + MaxAge = TimeSpan.FromMinutes (15) // Cache json for 15 minutes + }; + } + }); + + if (env.IsDevelopment ()) { + app.UseDeveloperExceptionPage (); + app.UseWebpackDevMiddleware (new WebpackDevMiddlewareOptions { + HotModuleReplacement = true, + HotModuleReplacementEndpoint = "/dist/" + }); + app.UseSwagger (); + app.UseSwaggerUI (c => { + c.SwaggerEndpoint ("/swagger/v1/swagger.json", "My API V1"); + }); + + // Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint. + + app.MapWhen (x => !x.Request.Path.Value.StartsWith ("/swagger", StringComparison.OrdinalIgnoreCase), builder => { + builder.UseMvc (routes => { + routes.MapSpaFallbackRoute ( + name: "spa-fallback", + defaults : new { controller = "Home", action = "Index" }); + }); + }); + } else { + app.UseMvc (routes => { + routes.MapRoute ( + name: "default", + template: "{controller=Home}/{action=Index}/{id?}"); + + routes.MapRoute ( + "Sitemap", + "sitemap.xml", + new { controller = "Home", action = "SitemapXml" }); + + routes.MapSpaFallbackRoute ( + name: "spa-fallback", + defaults : new { controller = "Home", action = "Index" }); + + }); + app.UseExceptionHandler ("/Home/Error"); + } + } } - } } diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index b0796562..d8882a29 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -1,7 +1,13 @@ -@Html.Raw(ViewData["SpaHtml"]) +<!-- Remove this if you want to remove Server-side rendering --> +@Html.Raw(ViewData["SpaHtml"]) + +<!-- if you only want Client-side rendering uncomment this --> +<!-- <app-root></app-root> --> <script src="http://github.com/~/dist/vendor.js" asp-append-version="true"></script> @section scripts { <!-- Our webpack bundle --> <script src="http://github.com/~/dist/main-client.js" asp-append-version="true"></script> } + +@Html.Raw(ViewData["ServiceWorker"]) diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index bcf5be51..e749bc3b 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -1,19 +1,18 @@ <!DOCTYPE html> <html> <head> + <meta charset="utf-8" /> <base href="http://github.com/@(Url.Content("~/"))" /> <title>@ViewData["Title"] - - + @Html.Raw(ViewData["Meta"]) @Html.Raw(ViewData["Links"]) - - + @Html.Raw(ViewData["Styles"]) - + @RenderBody() @@ -22,7 +21,6 @@ @Html.Raw(ViewData["TransferData"]) @Html.Raw(ViewData["Scripts"]) - @RenderSection("scripts", required: false) - + @RenderSection("scripts", required: false) diff --git a/Views/_ViewImports.cshtml b/Views/_ViewImports.cshtml index 8629c125..1872fd6b 100644 --- a/Views/_ViewImports.cshtml +++ b/Views/_ViewImports.cshtml @@ -1,2 +1,3 @@ @using AspCoreServer @addTagHelper "*, Microsoft.AspNetCore.SpaServices" +@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" diff --git a/angular.json b/angular.json new file mode 100644 index 00000000..b6c5377c --- /dev/null +++ b/angular.json @@ -0,0 +1,107 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "AspnetCore-Angular-Universal": { + "root": "", + "sourceRoot": "ClientApp", + "projectType": "application", + "prefix": "app", + "schematics": {}, + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "wwwroot/dist", + "main": "ClientApp/boot-browser.ts", + "polyfills": "ClientApp/polyfills/polyfills.ts", + "tsConfig": "ClientApp/tsconfig.app.json", + "assets": [], + "styles": [], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "ClientApp/environments/environment.ts", + "with": "ClientApp/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true + } + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "AspnetCore-Angular-Universal:build" + }, + "configurations": { + "production": { + "browserTarget": "AspnetCore-Angular-Universal:build:production" + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "AspnetCore-Angular-Universal:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "ClientApp/test.ts", + "polyfills": "ClientApp/polyfills.ts", + "tsConfig": "ClientApp/tsconfig.spec.json", + "karmaConfig": "ClientApp/karma.conf.js", + "styles": ["ClientApp/styles.css"], + "scripts": [], + "assets": ["ClientApp/favicon.ico", "ClientApp/assets"] + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "ClientApp/tsconfig.app.json", + "ClientApp/tsconfig.spec.json" + ], + "exclude": ["**/node_modules/**"] + } + } + } + }, + "AspnetCore-Angular-Universal-e2e": { + "root": "e2e/", + "projectType": "application", + "architect": { + "e2e": { + "builder": "@angular-devkit/build-angular:protractor", + "options": { + "protractorConfig": "e2e/protractor.conf.js", + "devServerTarget": "AspnetCore-Angular-Universal:serve" + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": "e2e/tsconfig.e2e.json", + "exclude": ["**/node_modules/**"] + } + } + } + } + }, + "defaultProject": "AspnetCore-Angular-Universal" +} diff --git a/docker-compose.dcproj b/docker-compose.dcproj new file mode 100644 index 00000000..920d4a2a --- /dev/null +++ b/docker-compose.dcproj @@ -0,0 +1,18 @@ + + + + 2.1 + Linux + 10b71bfc-c3ed-40b0-bb25-e38f04135e17 + LaunchBrowser + {Scheme}://localhost:{ServicePort} + asp2017 + + + + docker-compose.yml + + + + + \ No newline at end of file diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 00000000..dabcc4da --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,9 @@ +version: '3.4' + +services: + asp2017: + environment: + - ASPNETCORE_ENVIRONMENT=Development + #- ASPNETCORE_ENVIRONMENT=Production + ports: + - "80" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..d7d80bba --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3.4' + +services: + asp2017: + image: ${DOCKER_REGISTRY}asp2017 + build: + context: . + dockerfile: Dockerfile diff --git a/package.json b/package.json index 9ebbf620..01bc52bc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,13 @@ { - "name": "angular4-aspnetcore-universal", - "version": "1.0.0-rc3", + "name": "angular7-aspnetcore-universal", + "author": { + "name": "Mark Pieszak | Trilon Consulting", + "email": "hello@trilon.io", + "url": "https://trilon.io/" + }, + "version": "1.0.0-rc4", "scripts": { + "clean:install": "npm run clean && rimraf ./node_modules ./bin ./obj ./package-lock.json && dotnet restore Asp2017.csproj && npm i", "lint": "tslint -p tsconfig.json", "test": "npm run build:vendor && karma start ClientApp/test/karma.conf.js", "test:watch": "npm run test -- --auto-watch --no-single-run", @@ -11,80 +17,89 @@ "build:dev": "npm run build:vendor && npm run build:webpack", "build:webpack": "webpack --progress --color", "build:prod": "npm run clean && npm run build:vendor -- --env.prod && npm run build:webpack -- --env.prod", + "build:p": "npm run build:webpack -- --env.prod", "build:vendor": "webpack --config webpack.config.vendor.js --progress --color", "clean": "rimraf wwwroot/dist clientapp/dist" }, "dependencies": { - "@angular/animations": "^4.3.0", - "@angular/common": "^4.3.0", - "@angular/compiler": "^4.3.0", - "@angular/compiler-cli": "^4.3.0", - "@angular/core": "^4.3.0", - "@angular/forms": "^4.3.0", - "@angular/http": "^4.3.0", - "@angular/platform-browser": "^4.3.0", - "@angular/platform-browser-dynamic": "^4.3.0", - "@angular/platform-server": "^4.3.0", - "@angular/router": "^4.3.0", - "@nguniversal/aspnetcore-engine": "^1.0.0-beta.2", - "@ngx-translate/core": "^6.0.1", - "@ngx-translate/http-loader": "0.0.3", - "@types/node": "^7.0.12", + "@angular/animations": "~7.2.0", + "@angular/common": "~7.2.0", + "@angular/compiler": "~7.2.0", + "@angular/core": "~7.2.0", + "@angular/forms": "~7.2.0", + "@angular/http": "~7.2.0", + "@angular/platform-browser": "~7.2.0", + "@angular/platform-browser-dynamic": "~7.2.0", + "@angular/platform-server": "~7.2.0", + "@angular/router": "~7.2.0", + "@nguniversal/aspnetcore-engine": "^7.1.0", + "@nguniversal/common": "^7.1.0", + "@ngx-translate/core": "^11.0.1", + "@ngx-translate/http-loader": "^4.0.0", + "@types/node": "^11.9.5", "angular2-router-loader": "^0.3.5", "angular2-template-loader": "^0.6.2", "aspnet-prerendering": "^3.0.1", - "aspnet-webpack": "^2.0.1", - "awesome-typescript-loader": "^3.0.0", - "bootstrap": "^3.3.7", - "bootstrap-sass": "^3.3.7", - "core-js": "^2.5.1", - "css": "^2.2.1", - "css-loader": "^0.28.7", - "event-source-polyfill": "^0.0.9", - "expose-loader": "^0.7.3", - "extract-text-webpack-plugin": "^3.0.0", - "file-loader": "^0.11.2", - "html-loader": "^0.5.1", + "aspnet-webpack": "^3.0.0", + "awesome-typescript-loader": "^5.2.1", + "bootstrap": "^4.3.1", + "core-js": "^2.6.5", + "css": "^2.2.4", + "css-loader": "^2.1.0", + "event-source-polyfill": "^1.0.5", + "expose-loader": "^0.7.5", + "file-loader": "^3.0.1", + "html-loader": "^0.5.5", "isomorphic-fetch": "^2.2.1", - "jquery": "^2.2.1", - "json-loader": "^0.5.4", - "moment": "2.18.1", - "ngx-bootstrap": "2.0.0-beta.3", - "node-sass": "^4.5.2", - "preboot": "^5.0.0", - "raw-loader": "^0.5.1", - "rimraf": "^2.6.2", - "rxjs": "^5.4.3", - "sass-loader": "^6.0.6", - "style-loader": "^0.18.2", + "jquery": "^3.3.1", + "json-loader": "^0.5.7", + "moment": "^2.24.0", + "ngx-bootstrap": "^3.2.0", + "node-sass": "^4.11.0", + "preboot": "^7.0.0", + "raw-loader": "^1.0.0", + "rimraf": "^2.6.3", + "rxjs": "6.2.2", + "sass-loader": "^7.1.0", + "style-loader": "^0.23.1", "to-string-loader": "^1.1.5", - "typescript": "2.5.2", - "url-loader": "^0.5.7", - "webpack": "^3.6.0", - "webpack-hot-middleware": "^2.19.1", - "webpack-merge": "^4.1.0", - "zone.js": "^0.8.17" + "url-loader": "^1.1.2", + "webpack": "^4.29.5", + "webpack-hot-middleware": "^2.24.3", + "webpack-merge": "^4.2.1", + "zone.js": "^0.8.29" }, "devDependencies": { - "@angular/cli": "^1.3.2", - "@ngtools/webpack": "^1.3.0", - "@types/chai": "^3.4.34", - "@types/jasmine": "^2.5.37", - "chai": "^3.5.0", - "codelyzer": "^3.0.0", - "istanbul-instrumenter-loader": "^3.0.0", - "jasmine-core": "^2.5.2", - "karma": "^1.7.1", - "karma-chai": "^0.1.0", - "karma-chrome-launcher": "^2.2.0", - "karma-coverage": "^1.1.1", - "karma-jasmine": "^1.1.0", - "karma-mocha-reporter": "^2.2.4", + "@angular-devkit/build-angular": "~0.13.3", + "@angular/cli": "~7.3.3", + "@angular/compiler-cli": "~7.2.0", + "@ngtools/webpack": "~7.3.3", + "@types/jasmine": "~2.8.8", + "codelyzer": "~4.5.0", + "istanbul-instrumenter-loader": "^3.0.1", + "jasmine-core": "^3.3.0", + "jasmine-spec-reporter": "^4.2.1", + "karma": "~4.0.0", + "karma-chrome-launcher": "~2.2.0", + "karma-coverage": "~1.1.2", + "karma-jasmine": "~2.0.1", + "karma-mocha-reporter": "^2.2.5", "karma-phantomjs-launcher": "^1.0.4", - "karma-remap-coverage": "^0.1.4", + "karma-remap-coverage": "^0.1.5", "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^2.0.3", - "tslint": "^5.7.0", - "webpack-bundle-analyzer": "^2.9.0" - } + "karma-webpack": "^3.0.5", + "mini-css-extract-plugin": "^0.5.0", + "terser-webpack-plugin": "^1.2.3", + "tslint": "~5.11.0", + "typescript": "~3.2.2", + "uglifyjs-webpack-plugin": "^2.1.2", + "webpack-bundle-analyzer": "^3.0.4", + "webpack-cli": "^3.2.3" + }, + "license": "MIT", + "repository": { + "type": "github", + "url": "https://github.com/MarkPieszak/aspnetcore-angular2-universal" + }, + "readme": "https://github.com/MarkPieszak/aspnetcore-angular2-universal/blob/master/README.md" } diff --git a/tslint.json b/tslint.json index 657d49b6..f318a0b8 100644 --- a/tslint.json +++ b/tslint.json @@ -1,4 +1,5 @@ { + "defaultSeverity": "warn", "rules": { "align": false, "ban": false, @@ -75,7 +76,7 @@ "no-switch-case-fall-through": true, "no-trailing-whitespace": false, "no-unused-expression": true, - "no-unused-variable": false, + "no-unused-variable": true, "no-use-before-declare": true, "no-var-keyword": true, "no-var-requires": false, diff --git a/webpack.additions.js b/webpack.additions.js index 62bf4bb3..9c63a2f8 100644 --- a/webpack.additions.js +++ b/webpack.additions.js @@ -7,12 +7,14 @@ // Shared rules[] we need to add const sharedModuleRules = [ // sass - { test: /\.scss$/, loaders: ['to-string-loader', 'css-loader', 'sass-loader'] }, + { + test: /\.scss$/, + loaders: ['to-string-loader', 'css-loader', 'sass-loader'] + }, // font-awesome { test: /\.(woff2?|ttf|eot|svg)$/, loader: 'url-loader?limit=10000' } ]; - module.exports = { sharedModuleRules }; diff --git a/webpack.config.js b/webpack.config.js index 02ff0a01..249c4f59 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,93 +11,206 @@ const path = require('path'); const webpack = require('webpack'); const merge = require('webpack-merge'); -const AotPlugin = require('@ngtools/webpack').AotPlugin; +const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin; const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin; -const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') + .BundleAnalyzerPlugin; +const TerserPlugin = require('terser-webpack-plugin'); const { sharedModuleRules } = require('./webpack.additions'); -module.exports = (env) => { - // Configuration in common to both client-side and server-side bundles - const isDevBuild = !(env && env.prod); - const sharedConfig = { - stats: { modules: false }, - context: __dirname, - resolve: { extensions: [ '.js', '.ts' ] }, - output: { - filename: '[name].js', - publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix +module.exports = env => { + // Configuration in common to both client-side and server-side bundles + const isDevBuild = !(env && env.prod); + const sharedConfig = { + mode: isDevBuild ? 'development' : 'production', + stats: { + modules: false + }, + context: __dirname, + resolve: { + extensions: ['.js', '.ts'] + }, + output: { + filename: '[name].js', + publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix + }, + module: { + rules: [ + { + test: /^(?!.*\.spec\.ts$).*\.ts$/, + use: isDevBuild + ? [ + 'awesome-typescript-loader?silent=true', + 'angular2-template-loader', + 'angular2-router-loader' + ] + : '@ngtools/webpack' }, - module: { - rules: [ - { test: /\.ts$/, use: isDevBuild ? ['awesome-typescript-loader?silent=true', 'angular2-template-loader', 'angular2-router-loader'] : '@ngtools/webpack' }, - { test: /\.html$/, use: 'html-loader?minimize=false' }, - { test: /\.css$/, use: [ 'to-string-loader', isDevBuild ? 'css-loader' : 'css-loader?minimize' ] }, - { test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' }, - ...sharedModuleRules - ] + { + test: /\.html$/, + use: 'html-loader?minimize=false' + }, + { + test: /\.css$/, + use: [ + 'to-string-loader', + isDevBuild ? 'css-loader' : 'css-loader?minimize' + ] }, - plugins: [new CheckerPlugin()] - }; + { + test: /\.(png|jpg|jpeg|gif|svg)$/, + use: 'url-loader?limit=25000' + }, + ...sharedModuleRules + ] + }, + plugins: [new CheckerPlugin()] + }; - // Configuration for client-side bundle suitable for running in browsers - const clientBundleOutputDir = './wwwroot/dist'; - const clientBundleConfig = merge(sharedConfig, { - entry: { 'main-client': './ClientApp/boot.browser.ts' }, - output: { path: path.join(__dirname, clientBundleOutputDir) }, - plugins: [ - new webpack.DllReferencePlugin({ - context: __dirname, - manifest: require('./wwwroot/dist/vendor-manifest.json') - }) - ].concat(isDevBuild ? [ + // Configuration for client-side bundle suitable for running in browsers + const clientBundleOutputDir = './wwwroot/dist'; + const clientBundleConfig = merge(sharedConfig, { + entry: { + 'main-client': './ClientApp/boot.browser.ts' + }, + output: { + path: path.join(__dirname, clientBundleOutputDir) + }, + plugins: [ + new webpack.DllReferencePlugin({ + context: __dirname, + manifest: require('./wwwroot/dist/vendor-manifest.json') + }) + ].concat( + isDevBuild + ? [ // Plugins that apply in development builds only new webpack.SourceMapDevToolPlugin({ - filename: '[file].map', // Remove this line if you prefer inline source maps - moduleFilenameTemplate: path.relative(clientBundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk + filename: '[file].map', // Remove this line if you prefer inline source maps + moduleFilenameTemplate: path.relative( + clientBundleOutputDir, + '[resourcePath]' + ) // Point sourcemap entries to the original file locations on disk }) - ] : [ + ] + : [ // new BundleAnalyzerPlugin(), // Plugins that apply in production builds only - new webpack.optimize.UglifyJsPlugin(), - new AotPlugin({ - tsConfigPath: './tsconfig.json', - entryModule: path.join(__dirname, 'ClientApp/app/app.module.browser#AppModule'), - exclude: ['./**/*.server.ts'] + new AngularCompilerPlugin({ + mainPath: path.join(__dirname, 'ClientApp/boot.browser.ts'), + tsConfigPath: './ClientApp/tsconfig.app.json', + entryModule: path.join( + __dirname, + 'ClientApp/app/app.module.browser#AppModule' + ), + exclude: ['./**/*.server.ts'], + sourceMap: isDevBuild }) - ]) - }); + ] + ), + devtool: isDevBuild ? 'cheap-eval-source-map' : false, + node: { + fs: 'empty' + }, + optimization: { + minimizer: [].concat( + isDevBuild + ? [] + : [ + // we specify a custom TerserPlugin here to get source maps in production + new TerserPlugin({ + sourceMap: true, + terserOptions: { + compress: true, + ecma: 6, + mangle: true, + keep_classnames: true, + keep_fnames: true, + }, + }), + ] + ) + } + }); - // Configuration for server-side (prerendering) bundle suitable for running in Node - const serverBundleConfig = merge(sharedConfig, { - // resolve: { mainFields: ['main'] }, - entry: { 'main-server': './ClientApp/boot.server.ts' }, - plugins: [ - new webpack.DllReferencePlugin({ - context: __dirname, - manifest: require('./ClientApp/dist/vendor-manifest.json'), - sourceType: 'commonjs2', - name: './vendor' - }) - ].concat(isDevBuild ? [] : [ - new webpack.optimize.UglifyJsPlugin({ - compress: false, - mangle: false - }), + // Configuration for server-side (prerendering) bundle suitable for running in Node + const serverBundleConfig = merge(sharedConfig, { + // resolve: { mainFields: ['main'] }, + entry: { + 'main-server': isDevBuild + ? './ClientApp/boot.server.ts' + : './ClientApp/boot.server.PRODUCTION.ts' + }, + plugins: [ + new webpack.DllReferencePlugin({ + context: __dirname, + manifest: require('./ClientApp/dist/vendor-manifest.json'), + sourceType: 'commonjs2', + name: './vendor' + }) + ].concat( + isDevBuild + ? [ + new webpack.ContextReplacementPlugin( + // fixes WARNING Critical dependency: the request of a dependency is an expression + /(.+)?angular(\\|\/)core(.+)?/, + path.join(__dirname, 'src'), // location of your src + {} // a map of your routes + ), + new webpack.ContextReplacementPlugin( + // fixes WARNING Critical dependency: the request of a dependency is an expression + /(.+)?express(\\|\/)(.+)?/, + path.join(__dirname, 'src'), + {} + ) + ] + : [ // Plugins that apply in production builds only - new AotPlugin({ - tsConfigPath: './tsconfig.json', - entryModule: path.join(__dirname, 'ClientApp/app/app.module.server#AppModule'), - exclude: ['./**/*.browser.ts'] + new AngularCompilerPlugin({ + mainPath: path.join( + __dirname, + 'ClientApp/boot.server.PRODUCTION.ts' + ), + tsConfigPath: './ClientApp/tsconfig.app.json', + entryModule: path.join( + __dirname, + 'ClientApp/app/app.module.server#AppModule' + ), + exclude: ['./**/*.browser.ts'], + sourceMap: isDevBuild }) - ]), - output: { - libraryTarget: 'commonjs', - path: path.join(__dirname, './ClientApp/dist') - }, - target: 'node', - devtool: isDevBuild ? 'inline-source-map': false - }); + ] + ), + output: { + libraryTarget: 'commonjs', + path: path.join(__dirname, './ClientApp/dist') + }, + target: 'node', + // switch to "inline-source-map" if you want to debug the TS during SSR + devtool: isDevBuild ? 'cheap-eval-source-map' : false, + optimization: { + minimizer: [].concat( + isDevBuild + ? [] + : [ + // we specify a custom TerserPlugin here to get source maps in production + new TerserPlugin({ + cache: true, + parallel: true, + sourceMap: true, + terserOptions: { + compress: false, + ecma: 6, + mangle: true, + keep_classnames: true, + keep_fnames: true, + }, + }) + ] + ) + } + }); - return [clientBundleConfig, serverBundleConfig]; + return [clientBundleConfig, serverBundleConfig]; }; diff --git a/webpack.config.vendor.js b/webpack.config.vendor.js index 3e07b78b..775910fb 100644 --- a/webpack.config.vendor.js +++ b/webpack.config.vendor.js @@ -1,99 +1,162 @@ const path = require('path'); const webpack = require('webpack'); -const ExtractTextPlugin = require('extract-text-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const merge = require('webpack-merge'); +const TerserPlugin = require('terser-webpack-plugin'); const treeShakableModules = [ - '@angular/animations', - '@angular/common', - '@angular/compiler', - '@angular/core', - '@angular/forms', - '@angular/http', - '@angular/platform-browser', - '@angular/platform-browser-dynamic', - '@angular/router', - 'ngx-bootstrap', - 'zone.js', + '@angular/animations', + '@angular/common', + '@angular/compiler', + '@angular/core', + '@angular/forms', + '@angular/http', + '@angular/platform-browser', + '@angular/platform-browser-dynamic', + '@angular/router', + 'ngx-bootstrap', + 'zone.js', ]; const nonTreeShakableModules = [ - // 'bootstrap', - // 'bootstrap/dist/css/bootstrap.css', - 'core-js', - // 'es6-promise', - // 'es6-shim', - 'event-source-polyfill', - // 'jquery', + // 'bootstrap', + // 'bootstrap/dist/css/bootstrap.css', + 'core-js', + // 'es6-promise', + // 'es6-shim', + 'event-source-polyfill', + // 'jquery', ]; + const allModules = treeShakableModules.concat(nonTreeShakableModules); module.exports = (env) => { console.log(`env = ${JSON.stringify(env)}`) - const extractCSS = new ExtractTextPlugin('vendor.css'); - const isDevBuild = !(env && env.prod); - const sharedConfig = { - stats: { modules: false }, - resolve: { extensions: [ '.js' ] }, - module: { - rules: [ - { test: /\.(png|woff|woff2|eot|ttf|svg)(\?|$)/, use: 'url-loader?limit=100000' } - ] - }, - output: { - publicPath: 'dist/', - filename: '[name].js', - library: '[name]_[hash]' - }, - plugins: [ - // new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' }), // Maps these identifiers to the jQuery package (because Bootstrap expects it to be a global variable) - new webpack.ContextReplacementPlugin(/\@angular\b.*\b(bundles|linker)/, path.join(__dirname, './ClientApp')), // Workaround for https://github.com/angular/angular/issues/11580 - new webpack.ContextReplacementPlugin(/angular(\\|\/)core(\\|\/)@angular/, path.join(__dirname, './ClientApp')), // Workaround for https://github.com/angular/angular/issues/14898 - new webpack.IgnorePlugin(/^vertx$/) // Workaround for https://github.com/stefanpenner/es6-promise/issues/100 + const extractCSS = new MiniCssExtractPlugin({ + // Options similar to the same options in webpackOptions.output + // both options are optional + filename: "[name].css", + chunkFilename: "[id].css" + }); + const isDevBuild = !(env && env.prod); + const sharedConfig = { + mode: isDevBuild ? "development" : "production", + stats: { + modules: false + }, + resolve: { + extensions: ['.js'] + }, + module: { + rules: [{ + test: /\.(png|woff|woff2|eot|ttf|svg)(\?|$)/, + use: 'url-loader?limit=100000' + }] + }, + output: { + publicPath: 'dist/', + filename: '[name].js', + library: '[name]_[hash]' + }, + plugins: [ + // new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' }), // Maps these identifiers to the jQuery package (because Bootstrap expects it to be a global variable) + new webpack.ContextReplacementPlugin(/\@angular\b.*\b(bundles|linker)/, path.join(__dirname, './ClientApp')), // Workaround for https://github.com/angular/angular/issues/11580 + new webpack.ContextReplacementPlugin(/(.+)?angular(\\|\/)core(.+)?/, path.join(__dirname, './ClientApp')), // Workaround for https://github.com/angular/angular/issues/14898 + new webpack.IgnorePlugin(/^vertx$/) // Workaround for https://github.com/stefanpenner/es6-promise/issues/100 + ] + }; + + const clientBundleConfig = merge(sharedConfig, { + entry: { + // To keep development builds fast, include all vendor dependencies in the vendor bundle. + // But for production builds, leave the tree-shakable ones out so the AOT compiler can produce a smaller bundle. + vendor: isDevBuild ? allModules : nonTreeShakableModules + }, + output: { + path: path.join(__dirname, 'wwwroot', 'dist') + }, + module: { + rules: [{ + test: /\.css(\?|$)/, + use: [ + MiniCssExtractPlugin.loader, + isDevBuild ? 'css-loader' : 'css-loader?minimize' ] - }; + }] + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: 'vendor.css', + }), + new webpack.DllPlugin({ + path: path.join(__dirname, 'wwwroot', 'dist', '[name]-manifest.json'), + name: '[name]_[hash]' + }) + ].concat(isDevBuild ? [] : [ - const clientBundleConfig = merge(sharedConfig, { - entry: { - // To keep development builds fast, include all vendor dependencies in the vendor bundle. - // But for production builds, leave the tree-shakable ones out so the AOT compiler can produce a smaller bundle. - vendor: isDevBuild ? allModules : nonTreeShakableModules - }, - output: { path: path.join(__dirname, 'wwwroot', 'dist') }, - module: { - rules: [ - { test: /\.css(\?|$)/, use: extractCSS.extract({ use: isDevBuild ? 'css-loader' : 'css-loader?minimize' }) } - ] - }, - plugins: [ - extractCSS, - new webpack.DllPlugin({ - path: path.join(__dirname, 'wwwroot', 'dist', '[name]-manifest.json'), - name: '[name]_[hash]' - }) - ].concat(isDevBuild ? [] : [ - new webpack.optimize.UglifyJsPlugin() - ]) - }); + ]), + optimization: { + minimizer: [].concat(isDevBuild ? [] : [ + // we specify a custom TerserPlugin here to get source maps in production + new TerserPlugin({ + cache: true, + parallel: true, + sourceMap: true, + terserOptions: { + compress: false, + ecma: 6, + mangle: true, + keep_classnames: true, + keep_fnames: true, + }, + }) + ]) + } + }); - const serverBundleConfig = merge(sharedConfig, { - target: 'node', - resolve: { mainFields: ['main'] }, - entry: { vendor: allModules.concat(['aspnet-prerendering']) }, - output: { - path: path.join(__dirname, 'ClientApp', 'dist'), - libraryTarget: 'commonjs2', - }, - module: { - rules: [ { test: /\.css(\?|$)/, use: ['to-string-loader', isDevBuild ? 'css-loader' : 'css-loader?minimize' ] } ] - }, - plugins: [ - new webpack.DllPlugin({ - path: path.join(__dirname, 'ClientApp', 'dist', '[name]-manifest.json'), - name: '[name]_[hash]' - }) - ].concat(isDevBuild ? [] : [ - new webpack.optimize.UglifyJsPlugin() + const serverBundleConfig = merge(sharedConfig, { + target: 'node', + resolve: { + mainFields: ['main'] + }, + entry: { + vendor: allModules.concat(['aspnet-prerendering']) + }, + output: { + path: path.join(__dirname, 'ClientApp', 'dist'), + libraryTarget: 'commonjs2', + }, + module: { + rules: [{ + test: /\.css(\?|$)/, + use: [ + MiniCssExtractPlugin.loader, + isDevBuild ? 'css-loader' : 'css-loader?minimize' + ] + }] + }, + plugins: [ + new webpack.DllPlugin({ + path: path.join(__dirname, 'ClientApp', 'dist', '[name]-manifest.json'), + name: '[name]_[hash]' + }) + ].concat(isDevBuild ? [] : []), + optimization: { + minimizer: [].concat(isDevBuild ? [] : [ + // we specify a custom TerserPlugin here to get source maps in production + new TerserPlugin({ + cache: true, + parallel: true, + sourceMap: true, + terserOptions: { + compress: false, + ecma: 6, + mangle: true, + keep_classnames: true, + keep_fnames: true, + }, + }) ]) - }); + } + }); - return [clientBundleConfig, serverBundleConfig]; + return [clientBundleConfig, serverBundleConfig]; } diff --git a/wwwroot/favicon.ico b/wwwroot/favicon.ico index 6884543f..23015bba 100644 Binary files a/wwwroot/favicon.ico and b/wwwroot/favicon.ico differ diff --git a/wwwroot/images/icon144x144.png b/wwwroot/images/icon144x144.png new file mode 100644 index 00000000..b53f3791 Binary files /dev/null and b/wwwroot/images/icon144x144.png differ diff --git a/wwwroot/images/icon192x192.png b/wwwroot/images/icon192x192.png new file mode 100644 index 00000000..6f4b36a1 Binary files /dev/null and b/wwwroot/images/icon192x192.png differ diff --git a/wwwroot/images/icon256x256.png b/wwwroot/images/icon256x256.png new file mode 100644 index 00000000..66c420db Binary files /dev/null and b/wwwroot/images/icon256x256.png differ diff --git a/wwwroot/images/icon36x36.png b/wwwroot/images/icon36x36.png new file mode 100644 index 00000000..974921f1 Binary files /dev/null and b/wwwroot/images/icon36x36.png differ diff --git a/wwwroot/images/icon384x384.png b/wwwroot/images/icon384x384.png new file mode 100644 index 00000000..56c8c739 Binary files /dev/null and b/wwwroot/images/icon384x384.png differ diff --git a/wwwroot/images/icon48x48.png b/wwwroot/images/icon48x48.png new file mode 100644 index 00000000..734be054 Binary files /dev/null and b/wwwroot/images/icon48x48.png differ diff --git a/wwwroot/images/icon512x512.png b/wwwroot/images/icon512x512.png new file mode 100644 index 00000000..ed0b48d3 Binary files /dev/null and b/wwwroot/images/icon512x512.png differ diff --git a/wwwroot/images/icon72x72.png b/wwwroot/images/icon72x72.png new file mode 100644 index 00000000..77fde9eb Binary files /dev/null and b/wwwroot/images/icon72x72.png differ diff --git a/wwwroot/images/icon96x96.png b/wwwroot/images/icon96x96.png new file mode 100644 index 00000000..c5a86651 Binary files /dev/null and b/wwwroot/images/icon96x96.png differ diff --git a/wwwroot/manifest.json b/wwwroot/manifest.json new file mode 100644 index 00000000..b386fa18 --- /dev/null +++ b/wwwroot/manifest.json @@ -0,0 +1,49 @@ +{ + "name": "ASP.NET Core 2.1 & Angular 7(+) Advanced Starter", + "short_name": "aspnetcore-angular2-universal", + "description": + "ASP.NET Core 2.1 & Angular 7(+) Advanced Starter - with Server-side prerendering (for Angular SEO)!", + "icons": [ + { + "src": "/images/icon36x36.png", + "sizes": "36x36", + "type": "image/png" + }, { + "src": "/images/icon48x48.png", + "sizes": "48x48", + "type": "image/png" + }, { + "src": "/images/icon72x72.png", + "sizes": "72x72", + "type": "image/png" + }, { + "src": "/images/icon96x96.png", + "sizes": "96x96", + "type": "image/png" + }, { + "src": "/images/icon144x144.png", + "sizes": "144x144", + "type": "image/png" + }, { + "src": "/images/icon192x192.png", + "sizes": "192x192", + "type": "image/png" + }, { + "src": "/images/icon256x256.png", + "sizes": "256x256", + "type": "image/png" + }, { + "src": "/images/icon384x384.png", + "sizes": "384x384", + "type": "image/png" + }, { + "src": "/images/icon512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "background_color": "#3E4EB8", + "theme_color": "#2F3BA2", + "display": "standalone", + "start_url": "/" +}