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/.gitignore b/.gitignore index 9812c3c4..2f41fd03 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,7 @@ Obj/ .vs/ /wwwroot/dist/ -/Client/dist/ +/ClientApp/dist/ # MSTest test Results [Tt]est[Rr]esult*/ @@ -187,6 +187,7 @@ BundleArtifacts/ !*.[Cc]ache/ # Others +*.db ClientBin/ ~$* *~ @@ -259,3 +260,6 @@ _Pvt_Extensions # Jest Code Coverage report coverage/ + +.DS_Store +package-lock.json diff --git a/.vscode/launch.json b/.vscode/launch.json index 4464938c..c26ab807 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,73 +1,118 @@ { - "version": "0.2.0", - "configurations": [ - { - "name": "[Development] Launch Web", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug/netcoreapp1.1/Asp2017.dll", - "args": [], - "cwd": "${workspaceRoot}", - "stopAtEntry": false, - "internalConsoleOptions": "openOnSessionStart", - "launchBrowser": { - "enabled": true, - "args": "${auto-detect-url}", - "windows": { - "command": "cmd.exe", - "args": "/C start ${auto-detect-url}" - }, - "osx": { - "command": "open" - }, - "linux": { - "command": "xdg-open" - } - }, - "env": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "sourceFileMap": { - "/Views": "${workspaceRoot}/Views" - } + "version": "0.2.0", + "compounds": [{ + "name": "[Development] Debug Server & Client", + "configurations": ["[Development] Launch Server (no browser)", "[Development] Debug TypeScript"] + }], + "configurations": [{ + + "name": "[Development] Debug TypeScript", + "type": "chrome", + "request": "launch", + "url": "http://localhost:5000/", + "webRoot": "${workspaceRoot}/wwwroot", + "sourceMapPathOverrides": { + "webpack:///./*": "${workspaceRoot}\\*" + } + }, + { + "name": "[Development] Launch Server (no browser)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceRoot}/bin/Debug/netcoreapp2.1/Asp2017.dll", + "args": [], + "cwd": "${workspaceRoot}", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart", + "launchBrowser": { + "enabled": false, + "args": "${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" }, - { - "name": "[Production] Launch Web", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug/netcoreapp1.1/Asp2017.dll", - "args": [], - "cwd": "${workspaceRoot}", - "stopAtEntry": false, - "internalConsoleOptions": "openOnSessionStart", - "launchBrowser": { - "enabled": true, - "args": "${auto-detect-url}", - "windows": { - "command": "cmd.exe", - "args": "/C start ${auto-detect-url}" - }, - "osx": { - "command": "open" - }, - "linux": { - "command": "xdg-open" - } - }, - "env": { - "ASPNETCORE_ENVIRONMENT": "Production" - }, - "sourceFileMap": { - "/Views": "${workspaceRoot}/src/AspCoreServer/Views" - } + "osx": { + "command": "open" }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach", - "processId": "${command:pickProcess}" + "linux": { + "command": "xdg-open" } - ] -} \ No newline at end of file + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceRoot}/Views" + } + }, + { + "name": "[Development] Launch Web", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceRoot}/bin/Debug/netcoreapp2.1/Asp2017.dll", + "args": [], + "cwd": "${workspaceRoot}", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart", + "launchBrowser": { + "enabled": true, + "args": "${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" + }, + "osx": { + "command": "open" + }, + "linux": { + "command": "xdg-open" + } + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceRoot}/Views" + } + }, + { + "name": "[Production] Launch Web", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceRoot}/bin/Debug/netcoreapp2.1/Asp2017.dll", + "args": [], + "cwd": "${workspaceRoot}", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart", + "launchBrowser": { + "enabled": true, + "args": "${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" + }, + "osx": { + "command": "open" + }, + "linux": { + "command": "xdg-open" + } + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Production" + }, + "sourceFileMap": { + "/Views": "${workspaceRoot}/src/AspCoreServer/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} 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 0cb19457..b307acba 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -1,42 +1,56 @@ - + - netcoreapp1.1 + netcoreapp2.1 true + Latest false + Linux + docker-compose.dcproj - - - - - - - - + + + + + + + + + - - + + - - - - - - + + + + + + + + + + + + + + - - + + + - + %(DistFiles.Identity) PreserveNewest @@ -45,9 +59,9 @@ - + - + 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/Client/app/app.component.html b/Client/app/app.component.html deleted file mode 100644 index 12a962a5..00000000 --- a/Client/app/app.component.html +++ /dev/null @@ -1,6 +0,0 @@ -
- -
-
- -
\ No newline at end of file diff --git a/Client/app/app.component.scss b/Client/app/app.component.scss deleted file mode 100644 index 9ba6e4bf..00000000 --- a/Client/app/app.component.scss +++ /dev/null @@ -1,26 +0,0 @@ -$navbar-default-bg: #312312; -$light-orange: #ff8c00; -$navbar-default-color: $light-orange; - -/* Import Bootstrap & Fonts */ -$icon-font-path: '~bootstrap-sass/assets/fonts/bootstrap/'; -@import "http://github.com/~bootstrap-sass/assets/stylesheets/bootstrap"; - - - - -/* *** Overall APP Styling can go here *** - -------------------------------------------- - Note: This Component has ViewEncapsulation.None so the styles will bleed out - -*/ - -body { background: #f1f1f1; line-height: 18px; } -ul { padding: 10px 25px; } -ul li { padding: 5px 0; } - -h1 { border-bottom: 5px #4189C7 solid; } -h1, h2, h3 { padding: 10px 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/Client/app/app.component.ts b/Client/app/app.component.ts deleted file mode 100644 index 02b91787..00000000 --- a/Client/app/app.component.ts +++ /dev/null @@ -1,96 +0,0 @@ -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'; - -// i18n support -import { TranslateService } from '@ngx-translate/core'; -import { REQUEST } from './shared/constants/request'; - -@Component({ - selector: 'app', - 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(); - } - - 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); - }); - } - - 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/Client/app/app.module.ts b/Client/app/app.module.ts deleted file mode 100644 index 13f98f84..00000000 --- a/Client/app/app.module.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { NgModule, Inject } from '@angular/core'; -import { RouterModule } 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 'ng2-bootstrap'; - -// i18n support -import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; -import { TranslateHttpLoader } from '@ngx-translate/http-loader'; - -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 { NgxBootstrapComponent } from './containers/ngx-bootstrap-demo/ngx-bootstrap.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'); -} - -@NgModule({ - declarations: [ - AppComponent, - NavMenuComponent, - CounterComponent, - UsersComponent, - UserDetailComponent, - HomeComponent, - ChatComponent, - 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 - - // i18n support - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: (createTranslateLoader), - deps: [Http, [ORIGIN_URL]] - } - }), - - // 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: 'chat', component: ChatComponent, - // Wait until the resolve is finished before loading the Route - resolve: { connection: ConnectionResolver }, - data: { - title: 'SignalR chat example', - meta: [{ name: 'description', content: 'This is an Chat page Description!' }], - links: [ - { rel: 'canonical', href: 'http://blogs.example.com/chat/something' }, - { rel: 'alternate', hreflang: 'es', href: 'http://es.example.com/chat' } - ] - } - }, - { - 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'}, - - // All else fails - go home! - { path: '**', redirectTo: 'home' } - ]) - ], - providers: [ - LinkService, - UserService, - ConnectionResolver, - TranslateModule - ] -}) -export class AppModule { -} diff --git a/Client/app/browser-app.module.ts b/Client/app/browser-app.module.ts deleted file mode 100644 index 0150d22c..00000000 --- a/Client/app/browser-app.module.ts +++ /dev/null @@ -1,62 +0,0 @@ -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 { SignalRModule, SignalRConfiguration } from 'ng2-signalr'; - -import { ORIGIN_URL } from './shared/constants/baseurl.constants'; -import { AppModule } from './app.module'; -import { AppComponent } from './app.component'; -import { REQUEST } from './shared/constants/request'; -import { BrowserTransferStateModule } from '../modules/transfer-state/browser-transfer-state.module'; - -export function createConfig(): SignalRConfiguration { - const signalRConfig = new SignalRConfiguration(); - - signalRConfig.hubName = 'Ng2SignalRHub'; - signalRConfig.qs = { user: 'donald' }; - signalRConfig.url = 'http://ng2-signalr-backend.azurewebsites.net/'; - signalRConfig.logging = true; - - return signalRConfig; -} - -export function getOriginUrl() { - return window.location.origin; -} - -export function getRequest() { - // the Request object only lives on the server - return { cookie: document.cookie }; -} - -@NgModule({ - bootstrap: [AppComponent], - imports: [ - BrowserModule.withServerTransition({ - appId: 'my-app-id' // make sure this matches with your Server NgModule - }), - BrowserAnimationsModule, - BrowserTransferStateModule, - - // Our Common AppModule - AppModule, - - SignalRModule.forRoot(createConfig) - ], - 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 BrowserAppModule { -} diff --git a/Client/app/components/navmenu/navmenu.component.css b/Client/app/components/navmenu/navmenu.component.css deleted file mode 100644 index e15c6128..00000000 --- a/Client/app/components/navmenu/navmenu.component.css +++ /dev/null @@ -1,59 +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; -} - -@media (min-width: 768px) { - /* On small screens, convert the nav menu to a vertical sidebar */ - .main-nav { - height: 100%; - width: calc(25% - 20px); - } - .navbar { - border-radius: 0px; - border-width: 0px; - height: 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 */ - width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } -} diff --git a/Client/app/components/navmenu/navmenu.component.html b/Client/app/components/navmenu/navmenu.component.html deleted file mode 100644 index 277923a1..00000000 --- a/Client/app/components/navmenu/navmenu.component.html +++ /dev/null @@ -1,48 +0,0 @@ -<div class='main-nav'> - <div class='navbar'> - <div class='navbar-header'> - <button type='button' class='navbar-toggle' data-toggle='collapse' data-target='.navbar-collapse'> - <span class='sr-only'>Toggle navigation</span> - <span class='icon-bar'></span> - <span class='icon-bar'></span> - <span class='icon-bar'></span> - </button> - <a class='navbar-brand' [routerLink]="['/home']">Angular 4 Universal & ASP.NET Core </a> - </div> - <div class='clearfix'></div> - <div class='navbar-collapse collapse'> - <ul class='nav navbar-nav'> - <li [routerLinkActive]="['link-active']"> - <a [routerLink]="['/home']"> - <span class='glyphicon glyphicon-home'></span> {{ 'HOME' | translate }} - </a> - </li> - <li [routerLinkActive]="['link-active']"> - <a [routerLink]="['/counter']"> - <span class='glyphicon glyphicon-education'></span> {{ 'COUNTER' | translate }} - </a> - </li> - <li [routerLinkActive]="['link-active']"> - <a [routerLink]="['/users']"> - <span class='glyphicon glyphicon-user'></span> Rest API Demo - </a> - </li> - <li [routerLinkActive]="['link-active']"> - <a [routerLink]="['/ngx-bootstrap']"> - <span class='glyphicon glyphicon-th-large'></span> ngx-Bootstrap demo - </a> - </li> - <li [routerLinkActive]="['link-active']"> - <a [routerLink]="['/lazy']"> - <span class='glyphicon glyphicon-star-empty'></span> Lazy-loaded demo - </a> - </li> - <!--<li [routerLinkActive]="['link-active']"> - <a [routerLink]="['/chat']"> - <span class='glyphicon glyphicon-comment'></span> Chat - </a> - </li>--> - </ul> - </div> - </div> -</div> diff --git a/Client/app/components/navmenu/navmenu.component.ts b/Client/app/components/navmenu/navmenu.component.ts deleted file mode 100644 index 7a1691cb..00000000 --- a/Client/app/components/navmenu/navmenu.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'nav-menu', - templateUrl: './navmenu.component.html', - styleUrls: ['./navmenu.component.css'] -}) -export class NavMenuComponent { -} diff --git a/Client/app/components/user-detail/user-detail.component.html b/Client/app/components/user-detail/user-detail.component.html deleted file mode 100644 index 585bbe40..00000000 --- a/Client/app/components/user-detail/user-detail.component.html +++ /dev/null @@ -1,9 +0,0 @@ -<div *ngIf="user"> - <h2>{{user.name}} details:</h2> - <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 diff --git a/Client/app/components/user-detail/user-detail.component.ts b/Client/app/components/user-detail/user-detail.component.ts deleted file mode 100644 index 9db88355..00000000 --- a/Client/app/components/user-detail/user-detail.component.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { IUser } from '../../models/User'; -import { UserService } from '../../shared/user.service'; - -@Component({ - selector: 'user-detail', - templateUrl: './user-detail.component.html' -}) -export class UserDetailComponent { - @Input() user: IUser; - - constructor(private userService: UserService) { } - - - updateUser(user) { - this.userService.updateUser(user).subscribe(result => { - console.log('Put user result: ', result); - }, error => { - console.log(`There was an issue. ${error._body}.`); - }); - } -} diff --git a/Client/app/containers/chat/chat.component.css b/Client/app/containers/chat/chat.component.css deleted file mode 100644 index d6fefaad..00000000 --- a/Client/app/containers/chat/chat.component.css +++ /dev/null @@ -1,52 +0,0 @@ -.chat { - list-style: none; - margin: 0; - padding: 0; -} - -.chat li { - margin-bottom: 10px; - padding-bottom: 5px; - border-bottom: 1px dotted #B3A9A9; -} - - -/* -.chat li.left .chat-body { - margin-left: 60px; -} -*/ - -.chat li.right .chat-body { - margin-right: 60px; -} - -.chat li .chat-body p { - margin: 0; - color: #777777; -} - -.panel .slidedown .glyphicon, -.chat .glyphicon { - margin-right: 5px; -} - -.panel-body { - overflow-y: scroll; - height: 600px; -} - -::-webkit-scrollbar-track { - -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); - background-color: #F5F5F5; -} - -::-webkit-scrollbar { - width: 12px; - background-color: #F5F5F5; -} - -::-webkit-scrollbar-thumb { - -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3); - background-color: #555; -} \ No newline at end of file diff --git a/Client/app/containers/chat/chat.component.html b/Client/app/containers/chat/chat.component.html deleted file mode 100644 index e29d109b..00000000 --- a/Client/app/containers/chat/chat.component.html +++ /dev/null @@ -1,45 +0,0 @@ -<h2>SignalR chat example</h2> - -<div class="row"> - <div class="col-md-12"> - <div class="panel panel-primary"> - <div class="panel-heading"> - <span class="glyphicon glyphicon-comment"></span> Chat - </div> - <div class="panel-body"> - <ul class="chat"> - - <li class="left clearfix" *ngFor="let message of chatMessages"> - <!--<span chatMessage="chat-img pull-left"> - <img src="http://placehold.it/50/55C1E7/fff&text=U" alt="User Avatar" class="img-circle" /> - </span>--> - - <div class="chat-body clearfix"> - <div class="header"> - <strong class="primary-font">{{message.user}}</strong> - <small class="pull-right text-muted"><span class="glyphicon glyphicon-time"></span>x mins ago</small> - </div> - <p> - {{message.content}} - </p> - </div> - </li> - - </ul> - </div> - <div class="panel-footer"> - <div class="input-group"> - <span class="input-group-btn" style="padding-right: 10px; min-width: 150px;"> - <input #username id="btn-input" type="text" class="form-control input-sm" value="Neo" /> - </span> - <input #message id="btn-input" type="text" class="form-control input-sm" placeholder="Type your message here..." /> - <span class="input-group-btn "> - <button class="btn btn-warning btn-sm" id="btn-chat" (click)="sendMessage(username.value, message.value)"> Send</button> - </span> - </div> - </div> - </div> - </div> -</div> - -<pre>{{chatMessages | json}}</pre> \ No newline at end of file diff --git a/Client/app/containers/chat/chat.component.ts b/Client/app/containers/chat/chat.component.ts deleted file mode 100644 index ca9dc8ea..00000000 --- a/Client/app/containers/chat/chat.component.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Component, OnInit, Inject } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; - -import { SignalR, BroadcastEventListener, SignalRConnection } from 'ng2-signalr'; -import { Subscription } from 'rxjs/Subscription'; - -export class ChatMessage { - constructor(public user: string, public content: string) { } -} - -@Component({ - selector: 'chat', - templateUrl: './chat.component.html', - styleUrls: ['./chat.component.css'] -}) -export class ChatComponent implements OnInit { - - public chatMessages: ChatMessage[] = []; - - private _connection: SignalRConnection; - private _subscription: Subscription; - - constructor(route: ActivatedRoute) { - this._connection = route.snapshot.data['connection']; - } - - ngOnInit() { - const onMessageSent$ = new BroadcastEventListener<ChatMessage>('OnMessageSent'); - this._connection.listen(onMessageSent$); - this._subscription = onMessageSent$.subscribe((chatMessage: ChatMessage) => { - this.chatMessages.push(chatMessage); - console.log('chat messages', this.chatMessages); - }); - } - - // send chat message to server - sendMessage(user, message) { - console.log('send message', user, message); - this._connection.invoke('Chat', new ChatMessage(user, message)) - .catch((err: any) => console.log('Failed to invoke', err)); - } - -} diff --git a/Client/app/containers/counter/counter.component.spec.ts b/Client/app/containers/counter/counter.component.spec.ts deleted file mode 100644 index c1e54ed4..00000000 --- a/Client/app/containers/counter/counter.component.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -/// <reference path="../../../../node_modules/@types/jasmine/index.d.ts" /> -import { assert } from 'chai'; -import { CounterComponent } from './counter.component'; -import { TestBed, async, ComponentFixture } from '@angular/core/testing'; - -let fixture: ComponentFixture<CounterComponent>; - -describe('Counter component', () => { - 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 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'); - })); -}); diff --git a/Client/app/containers/counter/counter.component.ts b/Client/app/containers/counter/counter.component.ts deleted file mode 100644 index 69de17d9..00000000 --- a/Client/app/containers/counter/counter.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'counter', - templateUrl: './counter.component.html' -}) -export class CounterComponent { - public currentCount = 0; - - public incrementCounter() { - this.currentCount++; - } -} diff --git a/Client/app/containers/home/home.component.html b/Client/app/containers/home/home.component.html deleted file mode 100644 index 8a4eeb65..00000000 --- a/Client/app/containers/home/home.component.html +++ /dev/null @@ -1,76 +0,0 @@ -<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> -</blockquote> - - -<div class="row"> - <div class="col-lg-6"> - <h2>{{ 'HOME_FEATURE_LIST_TITLE' | translate }} </h2> - <ul> - <li>ASP.NET Core 1.1 :: ( Visual Studio 2017 )</li> - <li> - Angular 4.* front-end UI framework - <ul> - <li>Angular **platform-server** (Universal moved into Core here) - 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 2 - <ul> - <!--<li>TS2 aware path support</li>--> - <li>Hot Module Reloading/Replacement for an amazing development experience.</li> - <li>Tree-shaking</li> - </ul> - </li> - - <li>Bootstrap (ng2-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> - - <div class="col-lg-6"> - <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 <u>Universal</u> itself?</strong> <br>Please post an issue here: <a href="https://github.com/angular/universal">Angular Universal repo</a></li>--> - </ul> - </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> - - <button class="btn btn-default" (click)="setLanguage('no')"> - <span class="flag-icon flag-icon-no"></span> {{ 'NORWEGIAN' | translate }} - </button> - - </div> -</div> diff --git a/Client/app/containers/home/home.component.ts b/Client/app/containers/home/home.component.ts deleted file mode 100644 index ed7e6f0a..00000000 --- a/Client/app/containers/home/home.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Component, OnInit, Inject } from '@angular/core'; - -import { TranslateService } from '@ngx-translate/core'; - -@Component({ - selector: 'app-home', - templateUrl: './home.component.html' -}) -export class HomeComponent implements OnInit { - - title: string = 'Angular 4.0 Universal & ASP.NET Core advanced starter-kit'; - - // 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() { } - - public setLanguage(lang) { - this.translate.use(lang); - } -} diff --git a/Client/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts b/Client/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts deleted file mode 100644 index a8ba7dec..00000000 --- a/Client/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-bootstrap', - templateUrl: './ngx-bootstrap.component.html' -}) -export class NgxBootstrapComponent { - - public oneAtATime: boolean = true; - public items = ['Item 1', 'Item 2', 'Item 3']; - - 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}`); - } - -} \ No newline at end of file diff --git a/Client/app/containers/users/users.component.css b/Client/app/containers/users/users.component.css deleted file mode 100644 index 4e5440e1..00000000 --- a/Client/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/Client/app/containers/users/users.component.html b/Client/app/containers/users/users.component.html deleted file mode 100644 index 5bb77691..00000000 --- a/Client/app/containers/users/users.component.html +++ /dev/null @@ -1,29 +0,0 @@ -<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> -</blockquote> - -<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/Client/app/containers/users/users.component.ts b/Client/app/containers/users/users.component.ts deleted file mode 100644 index 9dd28781..00000000 --- a/Client/app/containers/users/users.component.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { - Component, OnInit, - // animation imports - trigger, state, style, transition, animate, Inject -} 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%)' })) - ]) - ]) - ] -}) -export class UsersComponent implements OnInit { - - users: IUser[]; - selectedUser: IUser; - - // 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('Get user result: ', result); - console.log('TransferHttp [GET] /api/users/allresult', result); - this.users = result as IUser[]; - }); - } - - onSelect(user: IUser): void { - this.selectedUser = user; - } - - 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}.`); - }); - } - - 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}.`); - }); - } -} diff --git a/Client/app/models/User.ts b/Client/app/models/User.ts deleted file mode 100644 index 53f9df3a..00000000 --- a/Client/app/models/User.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IUser { - id: number; - name: string; -} \ No newline at end of file diff --git a/Client/app/server-app.module.ts b/Client/app/server-app.module.ts deleted file mode 100644 index ac91990a..00000000 --- a/Client/app/server-app.module.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 { AppModule } from './app.module'; -import { AppComponent } from './app.component'; -import { ServerTransferStateModule } from '../modules/transfer-state/server-transfer-state.module'; -import { TransferState } from '../modules/transfer-state/transfer-state'; - -@NgModule({ - bootstrap: [AppComponent], - imports: [ - BrowserModule.withServerTransition({ - appId: 'my-app-id' // make sure this matches with your Browser NgModule - }), - ServerModule, - NoopAnimationsModule, - - ServerTransferStateModule, - - // Our Common AppModule - AppModule - ] -}) -export class ServerAppModule { - - constructor(private transferState: TransferState) { } - - // Gotcha (needs to be an arrow function) - ngOnBootstrap = () => { - this.transferState.inject(); - } -} diff --git a/Client/app/shared/constants/baseurl.constants.ts b/Client/app/shared/constants/baseurl.constants.ts deleted file mode 100644 index 58807bc4..00000000 --- a/Client/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/Client/app/shared/constants/request.ts b/Client/app/shared/constants/request.ts deleted file mode 100644 index 4c553d8a..00000000 --- a/Client/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/Client/app/shared/link.service.ts b/Client/app/shared/link.service.ts deleted file mode 100644 index c5a2f16d..00000000 --- a/Client/app/shared/link.service.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * -- LinkService -- [Temporary] - * @MarkPieszak - * - * Similar to Meta service but made to handle <link> creation for SEO purposes - * 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'; - -@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); - } - } - - // 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; -} & { - [prop: string]: string; - }; \ No newline at end of file diff --git a/Client/app/shared/route.resolver.ts b/Client/app/shared/route.resolver.ts deleted file mode 100644 index 6bbef56b..00000000 --- a/Client/app/shared/route.resolver.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Resolve } from '@angular/router'; - -import { SignalR, SignalRConnection } from 'ng2-signalr'; - -@Injectable() -export class ConnectionResolver implements Resolve<SignalRConnection> { - - constructor(private _signalR: SignalR) { } - - resolve() { - console.log('ConnectionResolver. Resolving...'); - return this._signalR.connect(); - } -} diff --git a/Client/app/shared/user.service.ts b/Client/app/shared/user.service.ts deleted file mode 100644 index 8bcc3bb3..00000000 --- a/Client/app/shared/user.service.ts +++ /dev/null @@ -1,42 +0,0 @@ -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 { 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) { - - } - - 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! - - // 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): Observable<IUser> { - return this.transferHttp.get(`${this.baseUrl}/api/users/` + user.id); - } - - deleteUser(user: IUser): Observable<any> { - return this.http.delete(`${this.baseUrl}/api/users/` + user.id); - } - - 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 }) - } -} diff --git a/Client/main.browser.ts b/Client/main.browser.ts deleted file mode 100644 index f1e76b9d..00000000 --- a/Client/main.browser.ts +++ /dev/null @@ -1,22 +0,0 @@ -import './polyfills/browser.polyfills'; -import { enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { BrowserAppModule } from './app/browser-app.module'; - -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(() => { - // Before restarting the app, we create a new root element and dispose the old one - const oldRootElem = document.querySelector(rootElemTagName); - const newRootElem = document.createElement(rootElemTagName); - oldRootElem.parentNode.insertBefore(newRootElem, oldRootElem); - modulePromise.then(appModule => appModule.destroy()); - }); -} else { - enableProdMode(); -} - -const modulePromise = platformBrowserDynamic().bootstrapModule(BrowserAppModule); diff --git a/Client/main.server.aot.ts b/Client/main.server.aot.ts deleted file mode 100644 index eb4f0699..00000000 --- a/Client/main.server.aot.ts +++ /dev/null @@ -1,41 +0,0 @@ -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 { ORIGIN_URL } from './app/shared/constants/baseurl.constants'; -// Grab the (Node) server-specific NgModule -import { ServerAppModuleNgFactory } from './ngfactory/app/server-app.module.ngfactory'; -// Temporary * the engine will be on npm soon (`@universal/ng-aspnetcore-engine`) -import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from './polyfills/temporary-aspnetcore-engine'; - -enableProdMode(); - -export default createServerRenderer(params => { - - // Platform-server provider configuration - const setupOptions: IEngineOptions = { - appSelector: '<app></app>', - ngModule: ServerAppModuleNgFactory, - 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' - }); - - return ({ - html: response.html, - globals: response.globals - }); - }); -}); - -/* -------- THIS FILE IS TEMPORARY and will be gone when @ngtools/webpack can handle dual files (w server) ---------- */ diff --git a/Client/main.server.ts b/Client/main.server.ts deleted file mode 100644 index e20e4f9f..00000000 --- a/Client/main.server.ts +++ /dev/null @@ -1,40 +0,0 @@ -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 { ORIGIN_URL } from './app/shared/constants/baseurl.constants'; -// Grab the (Node) server-specific NgModule -import { ServerAppModule } from './app/server-app.module'; -// Temporary * the engine will be on npm soon (`@universal/ng-aspnetcore-engine`) -import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from './polyfills/temporary-aspnetcore-engine'; - -enableProdMode(); - -export default createServerRenderer((params: BootFuncParams) => { - - // Platform-server provider configuration - const setupOptions: IEngineOptions = { - appSelector: '<app></app>', - ngModule: ServerAppModule, - 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, - globals: response.globals - }); - }); -}); diff --git a/Client/modules/transfer-http/transfer-http.module.ts b/Client/modules/transfer-http/transfer-http.module.ts deleted file mode 100644 index c2875b33..00000000 --- a/Client/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/Client/modules/transfer-http/transfer-http.ts b/Client/modules/transfer-http/transfer-http.ts deleted file mode 100644 index e8667b58..00000000 --- a/Client/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, 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, 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, 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/Client/modules/transfer-state/browser-transfer-state.module.ts b/Client/modules/transfer-state/browser-transfer-state.module.ts deleted file mode 100644 index 20e11421..00000000 --- a/Client/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/Client/modules/transfer-state/server-transfer-state.module.ts b/Client/modules/transfer-state/server-transfer-state.module.ts deleted file mode 100644 index 1a77f653..00000000 --- a/Client/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/Client/modules/transfer-state/server-transfer-state.ts b/Client/modules/transfer-state/server-transfer-state.ts deleted file mode 100644 index b2890b26..00000000 --- a/Client/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/Client/modules/transfer-state/transfer-state.ts b/Client/modules/transfer-state/transfer-state.ts deleted file mode 100644 index cc963b8f..00000000 --- a/Client/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/Client/polyfills/browser.polyfills.ts b/Client/polyfills/browser.polyfills.ts deleted file mode 100644 index fa02ffab..00000000 --- a/Client/polyfills/browser.polyfills.ts +++ /dev/null @@ -1,4 +0,0 @@ -import './polyfills.ts'; - -import 'zone.js/dist/zone'; -import 'reflect-metadata'; diff --git a/Client/polyfills/polyfills.ts b/Client/polyfills/polyfills.ts deleted file mode 100644 index fe3a6bd6..00000000 --- a/Client/polyfills/polyfills.ts +++ /dev/null @@ -1,36 +0,0 @@ - -/*************************************************************************************************** - * 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'; - -/** 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'; diff --git a/Client/polyfills/rx-imports.ts b/Client/polyfills/rx-imports.ts deleted file mode 100644 index 506f102b..00000000 --- a/Client/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/Client/polyfills/server.polyfills.ts b/Client/polyfills/server.polyfills.ts deleted file mode 100644 index 7d3dc3bf..00000000 --- a/Client/polyfills/server.polyfills.ts +++ /dev/null @@ -1,4 +0,0 @@ -import './polyfills.ts'; - -import 'reflect-metadata'; -import 'zone.js'; diff --git a/Client/polyfills/temporary-aspnetcore-engine.ts b/Client/polyfills/temporary-aspnetcore-engine.ts deleted file mode 100644 index d9b19b95..00000000 --- a/Client/polyfills/temporary-aspnetcore-engine.ts +++ /dev/null @@ -1,240 +0,0 @@ -/* ********* TEMPORARILY HERE ************** - * - will be on npm soon - - * import { ngAspnetCoreEngine } from `@ng-universal/ng-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'; - -// import { FileLoader } from './file-loader'; - -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 META = []; - const LINKS = []; - let TITLE = ''; - - let STYLES_STRING = htmlDoc.substring( - htmlDoc.indexOf('<style ng-transition'), - htmlDoc.lastIndexOf('</style>') + 8 - ); - // STYLES_STRING = STYLES_STRING.replace(/\s/g, '').replace('<styleng-transition', '<style ng-transition'); - - 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; - } - - // 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, - meta: META.join(' '), - links: LINKS.join(' ') - } - }); - - moduleRef.destroy(); - - }, (err) => { - reject(err); - }); - - }); - }); - - } catch (ex) { - 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) { - console.log('Already AoT?'); - resolve(moduleOrFactory); - return; - } else { - let moduleFactory = factoryCacheMap.get(moduleOrFactory); - - // If module factory is cached - if (moduleFactory) { - console.log('\n\n\n WE FOUND ONE!! USE IT!!\n\n\n'); - resolve(moduleFactory); - return; - } - - // Compile the module and cache it - compiler.compileModuleAsync(moduleOrFactory) - .then((factory) => { - console.log('\n\n\n\n MAP THIS THING!!!!\n\n\n '); - factoryCacheMap.set(moduleOrFactory, factory); - resolve(factory); - }, (err => { - reject(err); - })); - } - }); -} diff --git a/Client/test/jestGlobalMocks.ts b/Client/test/jestGlobalMocks.ts deleted file mode 100644 index e5e90ebc..00000000 --- a/Client/test/jestGlobalMocks.ts +++ /dev/null @@ -1,15 +0,0 @@ -const mock = () => { - let storage = {}; - return { - getItem: key => key in storage ? storage[key] : null, - setItem: (key, value) => storage[key] = value || '', - removeItem: key => delete storage[key], - clear: () => storage = {} - }; -}; - -Object.defineProperty(window, 'localStorage', {value: mock()}); -Object.defineProperty(window, 'sessionStorage', {value: mock()}); -Object.defineProperty(window, 'getComputedStyle', { - value: () => ['-webkit-appearance'] -}); diff --git a/Client/test/setupJest.ts b/Client/test/setupJest.ts deleted file mode 100644 index 1d3bd024..00000000 --- a/Client/test/setupJest.ts +++ /dev/null @@ -1,2 +0,0 @@ -import 'jest-preset-angular'; -import './jestGlobalMocks'; diff --git a/Client/tsconfig.browser.json b/Client/tsconfig.browser.json deleted file mode 100644 index d04b9e60..00000000 --- a/Client/tsconfig.browser.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig.json", - "angularCompilerOptions": { - "entryModule": "./app/browser-app.module#BrowserAppModule" - }, - "exclude": [ - "./main.server.aot.ts" - ] -} diff --git a/Client/tsconfig.server.aot.json b/Client/tsconfig.server.aot.json deleted file mode 100644 index 2407460a..00000000 --- a/Client/tsconfig.server.aot.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.server.json", - "angularCompilerOptions": { - "genDir": "ngfactory", - "entryModule": "./app/server-app.module#ServerAppModule" - }, - "exclude": [] -} diff --git a/Client/tsconfig.server.json b/Client/tsconfig.server.json deleted file mode 100644 index 6fecd055..00000000 --- a/Client/tsconfig.server.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig.json", - "angularCompilerOptions": { - "entryModule": "./app/server-app.module#ServerAppModule" - }, - "exclude": [ - "./main.server.aot.ts" - ] -} 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 new file mode 100644 index 00000000..45ca7f89 --- /dev/null +++ b/ClientApp/app/app.component.html @@ -0,0 +1,6 @@ +<div class="container-fluid"> + <app-nav-menu></app-nav-menu> + <div class="col-12 col-lg-9 body-content"> + <router-outlet></router-outlet> + </div> +</div> diff --git a/ClientApp/app/app.component.scss b/ClientApp/app/app.component.scss new file mode 100644 index 00000000..1e7eea64 --- /dev/null +++ b/ClientApp/app/app.component.scss @@ -0,0 +1,71 @@ +@import 'http://github.com/variables'; +/* *** Overall APP Styling can go here *** + -------------------------------------------- + Note: This Component has ViewEncapsulation.None so the styles will bleed out + +*/ + +body { + line-height: 18px; + padding-top: $header-height; +} + +.body-content { + margin: auto; +} + +h1 { + border-bottom: 3px theme-color('accent') solid; + font-size: 24px; +} + +h2 { + font-size: 20px; +} + +h1, +h2, +h3 { + padding: 3px 0; +} + +ul { + padding: 10px 25px; +} + +ul li { + padding: 5px 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; +} + +@include media-breakpoint-up(lg) { + body { + padding-top: 30px; + } + .body-content { + margin-left: $menu-max-width; + } + h1 { + border-bottom: 5px #4189c7 solid; + font-size: 36px; + } + h2 { + font-size: 30px; + } + h1, + h2, + h3 { + padding: 10px 0; + } +} diff --git a/ClientApp/app/app.component.ts b/ClientApp/app/app.component.ts new file mode 100644 index 00000000..415c196a --- /dev/null +++ b/ClientApp/app/app.component.ts @@ -0,0 +1,103 @@ +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 { Subscription } from 'rxjs'; +import { filter, map, mergeMap } from 'rxjs/operators'; +import { LinkService } from './shared/link.service'; + +@Component({ + 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; + 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]); + } + + 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 new file mode 100644 index 00000000..5162c3ca --- /dev/null +++ b/ClientApp/app/app.module.browser.ts @@ -0,0 +1,40 @@ +import { NgModule } from '@angular/core'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ORIGIN_URL, REQUEST } from '@nguniversal/aspnetcore-engine/tokens'; +import { PrebootModule } from 'preboot'; +import { AppComponent } from './app.component'; +import { AppModuleShared } from './app.module'; + +export function getOriginUrl() { + return window.location.origin; +} + +export function getRequest() { + // the Request object only lives on the server + return { cookie: document.cookie }; +} + +@NgModule({ + bootstrap: [AppComponent], + imports: [ + PrebootModule.withConfig({ appRoot: 'app-root' }), + BrowserAnimationsModule, + + // 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 {} diff --git a/ClientApp/app/app.module.server.ts b/ClientApp/app/app.module.server.ts new file mode 100644 index 00000000..06af629a --- /dev/null +++ b/ClientApp/app/app.module.server.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ServerModule } from '@angular/platform-server'; +import { PrebootModule } from 'preboot'; +import { AppComponent } from './app.component'; +import { AppModuleShared } from './app.module'; + +import { TransferHttpCacheModule, StateTransferInitializerModule } from '@nguniversal/common'; + +@NgModule({ + bootstrap: [AppComponent], + imports: [ + // Our Common AppModule + AppModuleShared, + + ServerModule, + PrebootModule.withConfig({ appRoot: 'app-root' }), + NoopAnimationsModule, + + 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() {} +} diff --git a/ClientApp/app/app.module.ts b/ClientApp/app/app.module.ts new file mode 100644 index 00000000..a30f46d8 --- /dev/null +++ b/ClientApp/app/app.module.ts @@ -0,0 +1,212 @@ +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 { 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 { UserDetailComponent } from './components/user-detail/user-detail.component'; +import { CounterComponent } from './containers/counter/counter.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'; + +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, + 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: [HttpClient, [ORIGIN_URL]] + } + }), + + // 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 <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: '**', + 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 {} diff --git a/ClientApp/app/components/navmenu/navmenu.component.html b/ClientApp/app/components/navmenu/navmenu.component.html new file mode 100644 index 00000000..ba9ff74e --- /dev/null +++ b/ClientApp/app/components/navmenu/navmenu.component.html @@ -0,0 +1,41 @@ +<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> +</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 new file mode 100644 index 00000000..d3dadb4d --- /dev/null +++ b/ClientApp/app/components/navmenu/navmenu.component.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-nav-menu', + templateUrl: './navmenu.component.html', + styleUrls: ['./navmenu.component.scss'] +}) +export class NavMenuComponent { + collapse: string = 'collapse'; + + collapseNavbar(): void { + if (this.collapse.length > 1) { + this.collapse = ''; + } else { + 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 new file mode 100644 index 00000000..f43b4bbb --- /dev/null +++ b/ClientApp/app/components/user-detail/user-detail.component.html @@ -0,0 +1,24 @@ +<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> --> 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 new file mode 100644 index 00000000..d14dbdc1 --- /dev/null +++ b/ClientApp/app/components/user-detail/user-detail.component.ts @@ -0,0 +1,52 @@ +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: 'app-user-detail', + styleUrls: ['./user-detail.component.scss'], + templateUrl: './user-detail.component.html' +}) +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) {} + + ngOnInit() { + this.userForm.valueChanges + .pipe( + debounceTime(400), + distinctUntilChanged() + ) + .subscribe(user => (this.user = user)); + } + ngOnChanges(changes: SimpleChanges) { + this.userForm.patchValue(this.user); + } + + 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/Client/app/containers/counter/counter.component.html b/ClientApp/app/containers/counter/counter.component.html similarity index 68% rename from Client/app/containers/counter/counter.component.html rename to ClientApp/app/containers/counter/counter.component.html index cc2bbd59..d4dcafe4 100644 --- a/Client/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 new file mode 100644 index 00000000..960f08d3 --- /dev/null +++ b/ClientApp/app/containers/counter/counter.component.spec.ts @@ -0,0 +1,29 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { CounterComponent } from './counter.component'; + +import {} from 'jasmine'; + +let fixture: ComponentFixture<CounterComponent>; + +describe('Counter component', () => { + 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 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'); + })); +}); diff --git a/ClientApp/app/containers/counter/counter.component.ts b/ClientApp/app/containers/counter/counter.component.ts new file mode 100644 index 00000000..42123687 --- /dev/null +++ b/ClientApp/app/containers/counter/counter.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-counter', + templateUrl: './counter.component.html' +}) +export class CounterComponent { + public currentCount = 0; + + public incrementCounter() { + this.currentCount++; + } +} diff --git a/ClientApp/app/containers/home/home.component.html b/ClientApp/app/containers/home/home.component.html new file mode 100644 index 00000000..c3a92f5c --- /dev/null +++ b/ClientApp/app/containers/home/home.component.html @@ -0,0 +1,86 @@ +<h1>{{ title }}</h1> + +<blockquote> + <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.1 :: ( Visual Studio 2017 )</li> + <li> + 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>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 + </li> + <li> + Webpack + <ul> + <li>Hot Module Reloading/Replacement for an amazing development experience.</li> + <li>Tree-shaking</li> + </ul> + </li> + + <li>Bootstrap (ngx-bootstrap) : Bootstrap capable of being rendered even on the server.</li> + <li>Unit testing via karma & jasmine.</li> + </ul> + + </div> + + <div class="col-lg-6"> + <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" 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-outline-secondary mr-2" (click)="setLanguage('en')"> + <span class="flag-icon flag-icon-us"></span> {{ 'ENGLISH' | translate }} + </button> + + <button class="btn btn-outline-secondary" (click)="setLanguage('no')"> + <span class="flag-icon flag-icon-no"></span> {{ 'NORWEGIAN' | translate }} + </button> + + </div> +</div> diff --git a/ClientApp/app/containers/home/home.component.ts b/ClientApp/app/containers/home/home.component.ts new file mode 100644 index 00000000..850f7b8f --- /dev/null +++ b/ClientApp/app/containers/home/home.component.ts @@ -0,0 +1,22 @@ +import { Component, OnInit } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + 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'; + + // 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() {} + + public setLanguage(lang) { + this.translate.use(lang); + } +} diff --git a/Client/app/containers/+lazy/lazy.component.ts b/ClientApp/app/containers/lazy/lazy.component.ts similarity index 78% rename from Client/app/containers/+lazy/lazy.component.ts rename to ClientApp/app/containers/lazy/lazy.component.ts index 25d6d1a4..53327733 100644 --- a/Client/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/Client/app/containers/+lazy/lazy.module.ts b/ClientApp/app/containers/lazy/lazy.module.ts similarity index 76% rename from Client/app/containers/+lazy/lazy.module.ts rename to ClientApp/app/containers/lazy/lazy.module.ts index 8d468754..aa33a605 100644 --- a/Client/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/Client/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html b/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html similarity index 54% rename from Client/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html rename to ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html index 5f7c3820..2b95c774 100644 --- a/Client/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 new file mode 100644 index 00000000..d1122dad --- /dev/null +++ b/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts @@ -0,0 +1,34 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-bootstrap', + templateUrl: './ngx-bootstrap.component.html' +}) +export class NgxBootstrapComponent { + public oneAtATime: boolean = true; + public items = ['Item 1', 'Item 2', 'Item 3']; + + 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}`); + } +} diff --git a/ClientApp/app/containers/not-found/not-found.component.html b/ClientApp/app/containers/not-found/not-found.component.html new file mode 100644 index 00000000..b0b7072d --- /dev/null +++ b/ClientApp/app/containers/not-found/not-found.component.html @@ -0,0 +1,10 @@ +<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> + </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" + width="560" height="315" frameborder="0" webkitAllowFullScreen allowFullScreen></iframe> + </div> diff --git a/ClientApp/app/containers/not-found/not-found.component.ts b/ClientApp/app/containers/not-found/not-found.component.ts new file mode 100644 index 00000000..6bd9147a --- /dev/null +++ b/ClientApp/app/containers/not-found/not-found.component.ts @@ -0,0 +1,11 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-not-found', + templateUrl: './not-found.component.html' +}) +export class NotFoundComponent implements OnInit { + constructor() {} + + ngOnInit() {} +} diff --git a/ClientApp/app/containers/users/users.component.html b/ClientApp/app/containers/users/users.component.html new file mode 100644 index 00000000..4b8a7ecc --- /dev/null +++ b/ClientApp/app/containers/users/users.component.html @@ -0,0 +1,38 @@ +<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> +</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> 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 new file mode 100644 index 00000000..b64ea0ee --- /dev/null +++ b/ClientApp/app/containers/users/users.component.ts @@ -0,0 +1,87 @@ +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: '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; + + // 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; + }); + } + + 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}.`); + } + ); + } + + 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}.`); + } + ); + } + + clearUser() { + if (this.selectedUser) { + this.selectedUser = null; + } + } +} diff --git a/ClientApp/app/models/User.ts b/ClientApp/app/models/User.ts new file mode 100644 index 00000000..f140ecb1 --- /dev/null +++ b/ClientApp/app/models/User.ts @@ -0,0 +1,4 @@ +export interface IUser { + id: number; + name: string; +} diff --git a/ClientApp/app/shared/link.service.ts b/ClientApp/app/shared/link.service.ts new file mode 100644 index 00000000..2e6754a4 --- /dev/null +++ b/ClientApp/app/shared/link.service.ts @@ -0,0 +1,95 @@ +/* + * -- LinkService -- [Temporary] + * @MarkPieszak + * + * Similar to Meta service but made to handle <link> creation for SEO purposes + * Soon there will be an overall HeadService within Angular that handles Meta/Link everything + */ + +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); + } + } + + // 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; +} & { + [prop: string]: string; +}; diff --git a/ClientApp/app/shared/user.service.ts b/ClientApp/app/shared/user.service.ts new file mode 100644 index 00000000..85249249 --- /dev/null +++ b/ClientApp/app/shared/user.service.ts @@ -0,0 +1,35 @@ +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'; + +@Injectable() +export class UserService { + private baseUrl: string; + + constructor(private http: HttpClient, private injector: Injector) { + this.baseUrl = this.injector.get(ORIGIN_URL); + } + + getUsers() { + return this.http.get<IUser[]>(`${this.baseUrl}/api/users`); + } + + getUser(user: IUser) { + return this.http.get<IUser>(`${this.baseUrl}/api/users/` + user.id); + } + + deleteUser(user: IUser) { + return this.http.delete<IUser>(`${this.baseUrl}/api/users/` + user.id); + } + + updateUser(user: IUser) { + return this.http.put<IUser>(`${this.baseUrl}/api/users/` + user.id, user); + } + + 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 new file mode 100644 index 00000000..68de6e5b --- /dev/null +++ b/ClientApp/boot.browser.ts @@ -0,0 +1,16 @@ +import './polyfills/browser.polyfills'; +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './app/app.module.browser'; + +// // Enable either Hot Module Reloading or production mode +if (module['hot']) { + module['hot'].accept(); + module['hot'].dispose(() => { + modulePromise.then(appModule => appModule.destroy()); + }); +} else { + 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 new file mode 100644 index 00000000..b6510018 --- /dev/null +++ b/ClientApp/boot.server.ts @@ -0,0 +1,39 @@ +import 'zone.js/dist/zone-node'; +import './polyfills/server.polyfills'; + +import { enableProdMode } from '@angular/core'; +import { createTransferScript, IEngineOptions, ngAspnetCoreEngine } from '@nguniversal/aspnetcore-engine'; +import { createServerRenderer } from 'aspnet-prerendering'; + +// Grab the (Node) server-specific NgModule +import { AppModule } from './app/app.module.server'; + +enableProdMode(); + +export default createServerRenderer((params) => { + + // Platform-server provider configuration + const setupOptions: IEngineOptions = { + 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) + ] + }; + + 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/polyfills/browser.polyfills.ts b/ClientApp/polyfills/browser.polyfills.ts new file mode 100644 index 00000000..9e6fe1d4 --- /dev/null +++ b/ClientApp/polyfills/browser.polyfills.ts @@ -0,0 +1,8 @@ +// Note: * The order is IMPORTANT! * + +import './polyfills.ts'; + +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 new file mode 100644 index 00000000..45b62d73 --- /dev/null +++ b/ClientApp/polyfills/polyfills.ts @@ -0,0 +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 'reflect-metadata'; + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. +/** Evergreen browsers require these. **/ +import 'core-js/es6/reflect'; +import 'core-js/es7/reflect'; \ No newline at end of file diff --git a/ClientApp/polyfills/server.polyfills.ts b/ClientApp/polyfills/server.polyfills.ts new file mode 100644 index 00000000..68867b14 --- /dev/null +++ b/ClientApp/polyfills/server.polyfills.ts @@ -0,0 +1,4 @@ +// Note: * The order is IMPORTANT! * +import './polyfills.ts'; + +import 'zone.js'; diff --git a/ClientApp/test/boot-tests.js b/ClientApp/test/boot-tests.js new file mode 100644 index 00000000..51eeb6d3 --- /dev/null +++ b/ClientApp/test/boot-tests.js @@ -0,0 +1,32 @@ +Error.stackTraceLimit = Infinity; +// Load required polyfills and testing libraries +require('core-js'); // Added for Phantomjs +require('zone.js'); +require('zone.js/dist/long-stack-trace-zone'); +require('zone.js/dist/proxy.js'); +require('zone.js/dist/sync-test'); +require('zone.js/dist/jasmine-patch'); +require('zone.js/dist/async-test'); +require('zone.js/dist/fake-async-test'); +const testing = require('@angular/core/testing'); +const testingBrowser = require('@angular/platform-browser-dynamic/testing'); + +// Prevent Karma from running prematurely +__karma__.loaded = function() {}; + +// First, initialize the Angular testing environment +testing + .getTestBed() + .initTestEnvironment( + testingBrowser.BrowserDynamicTestingModule, + testingBrowser.platformBrowserDynamicTesting() + ); + +// Then we find all the tests +const context = require.context('../', true, /\.spec\.ts$/); + +// And load the modules +context.keys().map(context); + +// Finally, start Karma to run the tests +__karma__.start(); diff --git a/ClientApp/test/karma.conf.js b/ClientApp/test/karma.conf.js new file mode 100644 index 00000000..6ab0db4b --- /dev/null +++ b/ClientApp/test/karma.conf.js @@ -0,0 +1,62 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/0.13/config/configuration-file.html + +module.exports = function(config) { + config.set({ + basePath: '.', + frameworks: ['jasmine'], + exclude: [], + files: ['../../wwwroot/dist/vendor.js', './boot-tests.js'], + preprocessors: { + './boot-tests.js': ['coverage', 'webpack', 'sourcemap'] + }, + client: { + captureConsole: false + }, + coverageReporter: { + type: 'in-memory' + }, + remapCoverageReporter: { + 'text-summary': null, + json: './coverage/coverage.json', + html: './coverage/html' + }, + reporters: ['mocha', 'coverage', 'remap-coverage'], + port: 9876, + colors: true, + logLevel: config.LOG_WARN, + autoWatch: false, + browsers: ['Chrome'], + mime: { + 'application/javascript': ['ts', 'tsx'] + }, + singleRun: true, + webpack: require('./webpack.config.test.js')({ + env: 'test' + }), + webpackMiddleware: { + noInfo: true, + stats: { + chunks: false + } + }, + // you can define custom flags + customLaunchers: { + PhantomJS_custom: { + base: 'PhantomJS', + options: { + windowName: 'test-window', + settings: { + webSecurityEnabled: false + } + }, + flags: ['--load-images=true'] + // debug: true + } + }, + phantomjsLauncher: { + // Have phantomjs exit if a ResourceError is encountered (useful if karma exits without killing phantom) + exitOnResourceError: true + } + }); +}; diff --git a/ClientApp/test/webpack.config.test.js b/ClientApp/test/webpack.config.test.js new file mode 100644 index 00000000..5cbcecc5 --- /dev/null +++ b/ClientApp/test/webpack.config.test.js @@ -0,0 +1,112 @@ +const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin'); +const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); + +const webpack = require('webpack'); +var path = require('path'); +var rootPath = path.join.bind(path, path.resolve(__dirname, '../../')); + +module.exports = function(options) { + return { + devtool: 'inline-source-map', + resolve: { + extensions: ['.ts', '.js'], + modules: [rootPath('ClientApp'), 'node_modules'] + }, + module: { + rules: [ + { + enforce: 'pre', + test: /\.js$/, + loader: 'source-map-loader', + exclude: [ + rootPath('node_modules/rxjs'), + rootPath('node_modules/@angular') + ] + }, + { + test: /\.ts$/, + use: [ + { + loader: 'awesome-typescript-loader', + query: { + sourceMap: false, + inlineSourceMap: true, + compilerOptions: { + removeComments: true + } + } + }, + 'angular2-template-loader' + ], + exclude: [/\.e2e\.ts$/] + }, + { + test: /\.css$/, + loader: ['to-string-loader', 'css-loader'] + }, + { + test: /\.scss$/, + loader: ['raw-loader', 'sass-loader'] + }, + { + test: /\.html$/, + loader: 'raw-loader' + }, + { + enforce: 'post', + test: /\.(js|ts)$/, + loader: 'istanbul-instrumenter-loader', + options: { + esModules: true + }, + include: rootPath('ClientApp'), + exclude: [/ClientApp\\test/, /\.(e2e|spec)\.ts$/, /node_modules/] + } + ] + }, + plugins: [ + new webpack.DllReferencePlugin({ + context: __dirname, + manifest: require(rootPath('wwwroot', 'dist', 'vendor-manifest.json')) + }), + new ContextReplacementPlugin( + /** + * The (\\|\/) piece accounts for path separators in *nix and Windows + */ + /angular(\\|\/)core(\\|\/)@angular/, + rootPath('ClientApp'), // location of your src + { + /** + * your Angular Async Route paths relative to this root directory + */ + } + ), + new LoaderOptionsPlugin({ + debug: false, + options: { + /** + * legacy options go here + */ + } + }) + ], + performance: { + hints: false + }, + + /** + * Include polyfills or mocks for various node stuff + * Description: Node configuration + * + * See: https://webpack.github.io/docs/configuration.html#node + */ + node: { + global: true, + process: false, + crypto: 'empty', + module: false, + clearImmediate: false, + setImmediate: false + } + }; +}; diff --git a/ClientApp/tsconfig.app.json b/ClientApp/tsconfig.app.json new file mode 100644 index 00000000..a082833e --- /dev/null +++ b/ClientApp/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "module": "es2015", + "baseUrl": "", + "sourceMap": true, + "types": ["node"] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts" + ] +} diff --git a/Client/tsconfig.spec.json b/ClientApp/tsconfig.spec.json similarity index 56% rename from Client/tsconfig.spec.json rename to ClientApp/tsconfig.spec.json index 584cb0a4..c8fc1d84 100644 --- a/Client/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/Client/typings.d.ts b/ClientApp/typings.d.ts similarity index 100% rename from Client/typings.d.ts rename to ClientApp/typings.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/LICENSE b/LICENSE new file mode 100644 index 00000000..2085d375 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015-2017 Mark Pieszak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 6e07e7c6..00000000 --- a/LICENSE.md +++ /dev/null @@ -1,11 +0,0 @@ -License - -The MIT License (MIT) - -Copyright (c) 2016-2017 Mark Pieszak - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file 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 1d73860f..f52bdf4d 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,47 @@ -# ASP.NET Core & 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)! +--- + +<br> <p align="center"> - <img src="http://github.com/docs/architecture.png" alt="ASP.NET Core Angular 2+ Starter" title="ASP.NET Core Angular 2+ 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 2+, ASP.NET Core, 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 Angular2 SEO" title="ASP.NET Core Angular2 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 Universal](https://github.com/angular/universal) and is meant to be an advanced starter -for both ASP.NET Core using Angular 4.0+, not only for the client-side, but to be rendered on the server for instant -application paints (Note: If you don't need Universal (SSR) [read here](#faq) on how to disable it). +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) @@ -29,10 +49,11 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual * [Deployment](#deployment) * [Upcoming Features](#upcoming-features) * [Application Structure](#application-structure) -* [Universal Gotchas](#universal-gotchas) -* [FAQ](#faq---also-check-out-the-faq-issues-label) +* [Gotchas](#gotchas) +* [FAQ](#faq---also-check-out-the-faq-issues-label-and-the-how-to-issues-label) * [Special Thanks](#special-thanks) * [License](#license) +* [Trilon - Consulting & Training](#trilon---angular--aspnet---consulting--training--development) --- @@ -40,16 +61,22 @@ 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 1.0 - VS2017 support now! +- ASP.NET 2.1 - VS2017 support now! - Azure delpoyment straight from VS2017 - Built in docker support through VS2017 - RestAPI (WebAPI) integration - SQL Database CRUD demo - Swagger WebAPI documentation when running in development mode - - SignalR Chat demo! (Thanks to [@hakonamatata](https://github.com/hakonamatata) - -- **Angular 4.0.0** : - - Featuring Server-side rendering (Platform-Server (basically Angular Universal, but moved into Angular Core) + - SignalR Chat demo! (Thanks to [@hakonamatata](https://github.com/hakonamatata)) + +- **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: + - `ng g c components/example-component` + - `ng g s shared/some-service` + - Featuring Server-side rendering (Platform-Server, aka: "Universal") - Faster initial paints, SEO (Search-engine optimization w Title/Meta/Link tags), social media link-previews, etc - i18n internationalization support (via/ ngx-translate) - Baked in best-practices (follows Angular style guide) @@ -57,37 +84,39 @@ 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 + - Production builds w/ AoT Compilation - **Testing frameworks** - - Unit testing with Karma/Jasmine + - Unit testing with Jest (Going back to Karma soon) - **Productivity** - Typescript 2 - Codelyzer (for Real-time static code analysis) - VSCode & Atom provide real-time analysis out of the box. - - **NOTE**: Does not fully work with Visual Studio yet. (Even with VS2017 and .NET core 1.0) -- **ASP.NET Core 1.1** +- **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 -```typescript - // Add the Module to your imports - ApplicationInsightsModule.forRoot({ - instrumentationKey: 'Your-Application-Insights-instrumentationKey' - }) -``` - + ```typescript + // Add the Module to your imports + ApplicationInsightsModule.forRoot({ + 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) ---- @@ -95,15 +124,21 @@ 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!** +- **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 1.0+ installed and/or VS2017. +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 > Note: Make sure you have the C# extension & .NET Core Debugger installed. @@ -116,7 +151,7 @@ npm install && npm run build:dev && dotnet restore # or yarn install ``` -If you're running the project from command line with `dotnet run` make sure you set your environment variables to Development (otherwise things like HMR won't work). +If you're running the project from command line with `dotnet run` make sure you set your environment variables to Development (otherwise things like HMR might not work). ```bash # on Windows: @@ -127,16 +162,9 @@ export ASPNETCORE_ENVIRONMENT=Development # Upcoming Features: -- **Fix and update Webpack build / Vendor chunking and overall compilation speed.** ( important ) -- 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) -- ~~Add Azure application insights module (or at least demo how to use it)~~ -- ~~Add i18n support~~ -- ~~DONE - Fix old README to match new project~~ -- ~~Add AoT compilation~~ -- ~~Add Bootstrap with SCSS~~ -- ~~Add REST API CRUD Demo~~ +- Clean API / structure / simplify application +- Refactor to latest RxJs pipeable syntax +- Attempt to integrate with Angular-CLI fully ---- @@ -180,14 +208,14 @@ Here we have the *usual suspects* found at the root level. - `protractor` - config files (e2e testing) - `tslint` - TypeScript code linting rules -### /Client/ - Everything Angular +### /ClientApp/ - Everything Angular > Let's take a look at how this is structured so we can make some sense of it all! -With Angular Universal, we need to split our applicatoin logic **per platform** so [if we look inside this folder](./Client), +With Angular Universal, we need to split our applicatoin logic **per platform** so [if we look inside this folder](./ClientApp), you'll see the 2 root files, that branch the entire logic for browser & server respectively. -- [**Main.Browser.ts**](./Client/main.browser.ts) - +- [**Boot.Browser.ts**](./ClientApp/boot.browser.ts) - This file starts up the entire Angular application for the Client/browser platform. Here we setup a few things, client Angular bootstrapping. @@ -195,24 +223,24 @@ Here we setup a few things, client Angular bootstrapping. You'll barely need to touch this file, but something to note, this is the file where you would import libraries that you **only** want being used in the Browser. (Just know that you'd have to provide a mock implementation for the Server when doing that). -- [**Main-Server.ts**](./Client/main.server.ts) - +- [**Boot.Server.ts**](./ClientApp/boot.server.ts) - This file is where Angular _platform-server_ *serializes* the Angular application itself on the .NET server within a very quick Node process, and renders it a string. This is what causes that initial fast paint of the entire application to the Browser, and helps us get all our _SEO_ goodness :sparkles: --- -Notice the folder structure here in `./Client/` : +Notice the folder structure here in `./ClientApp/` : ```diff -+ /Client/ ++ /ClientApp/ + /app/ App NgModule - our Root NgModule (you'll insert Components/etc here most often) AppComponent / App Routes / global css styles * Notice that we have 2 dividing NgModules: - browser-app.module & server-app.module + app.module.browser & app.module.server You'll almost always be using the common app.module, but these 2 are used to split up platform logic for situations where you need to use Dependency Injection / etc, between platforms. @@ -229,21 +257,21 @@ Note: You could use whatever folder conventions you'd like, I prefer to split up ``` When adding new features/components/etc to your application you'll be commonly adding things to the Root **NgModule** (located -in `/Client/app/app.module.ts`), but why are there **two** other NgModules in this folder? +in `/ClientApp/app/app.module.ts`), but why are there **two** other NgModules in this folder? This is because we want to split our logic **per Platform**, but notice they both share the Common NgModule named `app.module.ts`. When adding most things to your application, this is the only place you'll have to add in your new Component / Directive / Pipe / etc. You'll only occassional need to manually -add in the Platform specific things to either `browser-app.module || server-app.module`. +add in the Platform specific things to either `app.module.browser || app.module.server`. To illustrate this point with an example, you can see how we're using Dependency Injection to inject a `StorageService` that is different for the Browser & Server. ```typescript -// For the Browser (browser-app.module) +// For the Browser (app.module.browser) { provide: StorageService, useClass: BrowserStorage } -// For the Server (server-app.module) +// For the Server (app.module.server) { provide: StorageService, useClass: ServerStorage } ``` @@ -261,14 +289,14 @@ 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! +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: [TODO-add-link * You can read a more detailed explanation here](#) +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 (with Universal) +// Prerender / Serialize application var prerenderResult = await Prerenderer.RenderToString( - /* all of our parameters / options / boot-server file / customData object goes here */ + /* all of our parameters / options / boot.server file / customData object goes here */ ); ViewData["SpaHtml"] = prerenderResult.Html; @@ -288,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 (+) Universal starter + @ViewData["Title"] - AspNET.Core Angular 7.0.0 (+) starter @@ -325,14 +353,20 @@ Well now, your Client-side Angular will take over, and you'll have a fully funct ---- -# Universal "Gotchas" +# "Gotchas" -> When building Universal components in Angular 2 there are a few things to keep in mind. +- 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 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. - ``` + ```typescript import { PLATFORM_ID } from '@angular/core'; import { isPlatformBrowser, isPlatformServer } from '@angular/common'; @@ -353,46 +387,48 @@ Well now, your Client-side Angular will take over, and you'll have a fully funct - Try to *limit or* **avoid** using **`setTimeout`**. It will slow down the server-side rendering process. Make sure to remove them [`ngOnDestroy`](https://angular.io/docs/ts/latest/api/core/index/OnDestroy-class.html) in Components. - Also for RxJs timeouts, make sure to _cancel_ their stream on success, for they can slow down rendering as well. - **Don't manipulate the nativeElement directly**. Use the _Renderer2_. We do this to ensure that in any environment we're able to change our view. -``` -constructor(element: ElementRef, renderer: Renderer) { - renderer.setElementStyle(element.nativeElement, 'font-size', 'x-large'); +```typescript +constructor(element: ElementRef, renderer: Renderer2) { + this.renderer.setStyle(element.nativeElement, 'font-size', 'x-large'); } ``` - The application runs XHR requests on the server & once again on the Client-side (when the application bootstraps) - 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 ---- ---- -# FAQ - Also check out the [FAQ Issues label](https://github.com/MarkPieszak/aspnetcore-angular2-universal/issues?utf8=%E2%9C%93&q=is%3Aissue%20label%3Afaq) +# FAQ - Also check out the [!FAQ Issues label!](https://github.com/MarkPieszak/aspnetcore-angular2-universal/issues?utf8=%E2%9C%93&q=is%3Aissue%20label%3Afaq) and the [!HOW-TO Issues Label!](https://github.com/MarkPieszak/aspnetcore-angular2-universal/issues?q=is%3Aissue+label%3A%22HOW+TO+-+Guide%22) -### How can I disable Universal / SSR (Server-side rendering)? +### 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, browser-app.module & server-app.module files, just make sure your `boot-client` file points to `app.module`. +> 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`. ### How do I have code run only in the Browser? -Check the [Universal Gotchas](#universal-gotchas) on how to use `isPlatformBrowser()`. +Check the [Gotchas](#gotchas) on how to use `isPlatformBrowser()`. ### How do I Material2 with this repo? -You'll either want to remove SSR for now, or wait as support should be coming to handle Universal/platform-server rendering. +~~You'll either want to remove SSR for now, or wait as support should be coming to handle platform-server rendering.~~ +This is now possible, with the recently updated Angular Material changes. We do not have a tutorial available for this yet. -### How can I use jQuery and/or some jQuery plugins with Angular Universal? +### 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 (2+) 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' })` -Now, make sure any "plugins" etc that you have, are only included in your `main.browser.ts` file. (ie: `import 'slick-carousel';`) +Now, make sure any "plugins" etc that you have, are only included in your `boot.browser.ts` file. (ie: `import 'slick-carousel';`) In a Component you want to use jQuery, make sure to import it near the top like so: ```typescript @@ -403,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`**. ---- @@ -412,6 +448,8 @@ 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) - [@hakonamatata](https://github.com/hakonamatata) @@ -432,13 +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: [@MarkPieszak](http://twitter.com/MarkPieszak) | Medium: [@MarkPieszak](https://medium.com/@MarkPieszak) +[![Twitter Follow](https://img.shields.io/twitter/follow/MarkPieszak.svg?style=social)](https://twitter.com/MarkPieszak) ---- -# Looking for Angular Consulting / Training / support? +# Trilon - Angular & ASP.NET - Consulting | Training | Development + +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. + +

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

-[Contact me](mpieszak84@gmail.com), and let's talk about your projects needs! +## Follow Trilon online: +Twitter: [@Trilon_io](http://twitter.com/Trilon_io) diff --git a/Server/Controllers/HomeController.cs b/Server/Controllers/HomeController.cs index 3feb71ce..18c423c6 100644 --- a/Server/Controllers/HomeController.cs +++ b/Server/Controllers/HomeController.cs @@ -1,89 +1,47 @@ -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 System.Threading.Tasks; namespace AspCoreServer.Controllers { - public class HomeController : Controller - { - public async Task Index() - { - var nodeServices = Request.HttpContext.RequestServices.GetRequiredService(); - var hostEnv = Request.HttpContext.RequestServices.GetRequiredService(); - - var applicationBasePath = hostEnv.ContentRootPath; - var requestFeature = Request.HttpContext.Features.Get(); - 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 = AbstractHttpContextRequestInfo(Request); - transferData.thisCameFromDotNET = "Hi Angular it's asp.net :)"; - // Add more customData here, add it to the TransferData class - - // Prerender / Serialize application (with Universal) - var prerenderResult = await Prerenderer.RenderToString( - "/", - nodeServices, - new JavaScriptModuleExport(applicationBasePath + "/Client/dist/main-server"), - unencodedAbsoluteUrl, - unencodedPathAndQuery, - transferData, // Our simplified Request object & any other CustommData you want to send! - 30000, - Request.PathBase.ToString() - ); - - 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["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(); + 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 (); } - public IActionResult Error() - { - return View(); - } - - private IRequest AbstractHttpContextRequestInfo(HttpRequest request) - { - - IRequest requestSimplified = new IRequest(); - requestSimplified.cookies = request.Cookies; - requestSimplified.headers = request.Headers; - requestSimplified.host = request.Host; - - return requestSimplified; - } - } - - public class IRequest - { - public object cookies { get; set; } - public object headers { get; set; } - public object host { get; set; } - } - - public class TransferData - { - public dynamic request { get; set; } - - // Your data here ? - public object thisCameFromDotNET { get; set; } + [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 new file mode 100644 index 00000000..81c613b9 --- /dev/null +++ b/Server/Helpers/HttpRequestExtensions.cs @@ -0,0 +1,43 @@ +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; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.NodeServices; +using Microsoft.AspNetCore.SpaServices.Prerendering; +using Microsoft.Extensions.DependencyInjection; + +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( + "/", + 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() + ); + } +} diff --git a/Server/Models/IRequest.cs b/Server/Models/IRequest.cs new file mode 100644 index 00000000..295c88fb --- /dev/null +++ b/Server/Models/IRequest.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +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; } + } +} diff --git a/Server/Models/TransferData.cs b/Server/Models/TransferData.cs new file mode 100644 index 00000000..c144d2df --- /dev/null +++ b/Server/Models/TransferData.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Asp2017.Server.Models { + public class TransferData { + public dynamic request { 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 4e3c0510..8d978fd4 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,117 +1,109 @@ using System; using System.IO; +using AspCoreServer.Data; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Antiforgery; - +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.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; - -using Microsoft.AspNetCore.NodeServices; -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(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { - HotModuleReplacement = true - }); - - DbInitializer.Initialize(context); - - app.UseSwagger(); - - // Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint. - app.UseSwaggerUI(c => - { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); - }); - - app.MapWhen(x => !x.Request.Path.Value.StartsWith("/swagger"), 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.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 418bb1fd..d8882a29 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -1,6 +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-browser.js" asp-append-version="true"></script> + <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 8d8fb071..e749bc3b 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -1,32 +1,26 @@ -<!DOCTYPE html> +<!DOCTYPE html> <html> - <head> - <base href="http://github.com/" /> - <title>@ViewData["Title"] - - - - @Html.Raw(ViewData["Meta"]) - @Html.Raw(ViewData["Links"]) - - - - @Html.Raw(ViewData["Styles"]) - - - - @RenderBody() - - - - - - - - @Html.Raw(ViewData["TransferData"]) - - @RenderSection("scripts", required: false) - + + + + @ViewData["Title"] + + + @Html.Raw(ViewData["Meta"]) + @Html.Raw(ViewData["Links"]) + + + + @Html.Raw(ViewData["Styles"]) + + + + @RenderBody() + + + @Html.Raw(ViewData["TransferData"]) + @Html.Raw(ViewData["Scripts"]) + + @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/global.json b/global.json deleted file mode 100644 index 5c5ead2f..00000000 --- a/global.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "sdk": { "version": "1.0.0" } -} diff --git a/package.json b/package.json index 5256ae29..01bc52bc 100644 --- a/package.json +++ b/package.json @@ -1,87 +1,105 @@ { - "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": { - "test": "jest", - "test:watch": "npm run test -- --watch", - "test:ci": "npm run test -- --runInBand", + "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", + "test:ci": "npm run test -- --browsers PhantomJS_custom", + "test:ci:watch": "npm run test:ci -- --auto-watch --no-single-run", "test:coverage": "npm run test -- --coverage", - "build:dev": "webpack --progress --color", - "build:aot": "webpack --env.aot --env.client & webpack --env.aot --env.server" - }, - "jest": { - "preset": "jest-preset-angular", - "setupTestFrameworkScriptFile": "./Client/test/setupJest.ts", - "globals": { - "__TS_CONFIG__": "Client/tsconfig.spec.json", - "__TRANSFORM_HTML__": true - }, - "coveragePathIgnorePatterns": [ - "/node_modules/", - "/Client/test/.*.ts" - ], - "coverageDirectory": "coverage" + "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.0.0", - "@angular/common": "^4.0.0", - "@angular/compiler": "^4.0.0", - "@angular/compiler-cli": "^4.0.0", - "@angular/core": "^4.0.0", - "@angular/forms": "^4.0.0", - "@angular/http": "^4.0.0", - "@angular/platform-browser": "^4.0.0", - "@angular/platform-browser-dynamic": "^4.0.0", - "@angular/platform-server": "^4.0.0", - "@angular/router": "^4.0.0", - "@ngx-translate/core": "^6.0.1", - "@ngx-translate/http-loader": "0.0.3", - "@types/node": "^7.0.12", - "angular2-template-loader": "0.6.0", - "aspnet-prerendering": "2.0.3", - "aspnet-webpack": "^1.0.17", - "awesome-typescript-loader": "^3.0.0", - "bootstrap": "^3.3.7", - "bootstrap-sass": "^3.3.7", - "core-js": "^2.4.1", - "css": "^2.2.1", - "css-loader": "^0.25.0", - "event-source-polyfill": "^0.0.7", - "expose-loader": "^0.7.1", - "extract-text-webpack-plugin": "^2.0.0-rc", - "file-loader": "^0.9.0", - "html-loader": "^0.4.4", + "@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": "^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", - "ng2-bootstrap": "^1.6.1", - "ng2-signalr": "^2.0.2", - "node-sass": "^4.5.2", - "preboot": "^4.5.2", - "raw-loader": "^0.5.1", - "rimraf": "^2.6.1", - "rxjs": "^5.0.1", - "sass-loader": "^6.0.3", - "signalr": "^2.2.1", - "style-loader": "^0.13.1", + "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.2.1", - "url-loader": "^0.5.7", - "webpack": "^2.2.0", - "webpack-hot-middleware": "^2.12.2", - "webpack-merge": "^0.14.1", - "zone.js": "^0.8.9" + "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": { - "@ngtools/webpack": "^1.3.0", - "@types/chai": "^3.4.34", - "@types/jasmine": "^2.5.37", - "@types/jest": "^19.2.3", - "chai": "^3.5.0", - "codelyzer": "^3.0.0-beta.4", - "jasmine-core": "^2.5.2", - "jest": "^20.0.0", - "jest-preset-angular": "^2.0.1", - "tslint": "^4.5.1" - } + "@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.5", + "karma-sourcemap-loader": "^0.3.7", + "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/tsconfig.json b/tsconfig.json index 468cee8f..23317dc5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,10 +3,14 @@ "moduleResolution": "node", "module": "es2015", "target": "es5", + "alwaysStrict": true, "noImplicitAny": false, - "sourceMap": false, + "sourceMap": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "allowUnreachableCode": false, "lib": [ "es2016", "dom" @@ -14,6 +18,6 @@ "types": [ "node" ] }, "include": [ - "Client" + "ClientApp" ] } diff --git a/tslint.json b/tslint.json index bfbd3f5a..f318a0b8 100644 --- a/tslint.json +++ b/tslint.json @@ -1,4 +1,5 @@ { + "defaultSeverity": "warn", "rules": { "align": false, "ban": false, @@ -8,32 +9,20 @@ "check-space" ], "component-class-suffix": true, - "component-selector-name": [ + "component-selector": [ true, + "element", + "app", "kebab-case" ], - "component-selector-prefix": [ - true, - "app" - ], - "component-selector-type": [ - true, - "element" - ], "curly": false, "directive-class-suffix": true, - "directive-selector-name": [ + "directive-selector": [ true, + "attribute", + "app", "camelCase" ], - "directive-selector-prefix": [ - true, - "app" - ], - "directive-selector-type": [ - true, - "attribute" - ], "eofline": true, "forin": true, "import-destructuring-spacing": true, @@ -44,7 +33,6 @@ "interface-name": false, "jsdoc-format": true, "label-position": true, - "label-undefined": true, "max-line-length": [ true, 200 @@ -73,7 +61,6 @@ "no-construct": true, "no-constructor-vars": false, "no-debugger": true, - "no-duplicate-key": true, "no-duplicate-variable": true, "no-empty": false, "no-eval": true, @@ -88,9 +75,8 @@ "no-string-literal": false, "no-switch-case-fall-through": true, "no-trailing-whitespace": false, - "no-unreachable": true, "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, @@ -153,10 +139,6 @@ "use-life-cycle-interface": true, "use-output-property-decorator": true, "use-pipe-transform-interface": true, - "use-strict": [ - true, - "check-module" - ], "variable-name": [ true, "check-format", @@ -175,4 +157,4 @@ "rulesDirectory": [ "node_modules/codelyzer" ] -} \ No newline at end of file +} diff --git a/webpack.additions.js b/webpack.additions.js new file mode 100644 index 00000000..9c63a2f8 --- /dev/null +++ b/webpack.additions.js @@ -0,0 +1,20 @@ +/* [ Webpack Additions ] + * + * This file contains ADD-ONS we are adding on-top of the traditional JavaScriptServices repo + * We do this so that those already using JavaScriptServices can easily figure out how to combine this repo into it. + */ + +// Shared rules[] we need to add +const sharedModuleRules = [ + // sass + { + 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 76980b9c..249c4f59 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,46 +1,216 @@ -const path = require('path'); -const webpackMerge = require('webpack-merge'); -const commonPartial = require('./webpack/webpack.common'); -const clientPartial = require('./webpack/webpack.client'); -const serverPartial = require('./webpack/webpack.server'); -const prodPartial = require('./webpack/webpack.prod'); -const { getAotPlugin } = require('./webpack/webpack.aot'); +/* + * Webpack (JavaScriptServices) with a few changes & updates + * - This is to keep us inline with JSServices, and help those using that template to add things from this one + * + * Things updated or changed: + * module -> rules [] + * .ts$ test : Added 'angular2-router-loader' for lazy-loading in development + * added ...sharedModuleRules (for scss & font-awesome loaders) + */ -module.exports = function (options, webpackOptions) { - options = options || {}; - webpackOptions = webpackOptions || {}; +const path = require('path'); +const webpack = require('webpack'); +const merge = require('webpack-merge'); +const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin; +const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin; +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') + .BundleAnalyzerPlugin; +const TerserPlugin = require('terser-webpack-plugin'); - if (options.aot) { - console.log(`Running build for ${options.client ? 'client' : 'server'} with AoT Compilation`) - } - - const serverConfig = webpackMerge({}, commonPartial, serverPartial, { - entry: options.aot ? { 'main-server' : './Client/main.server.aot.ts' } : serverPartial.entry, // Temporary - plugins: [ - getAotPlugin('server', !!options.aot) - ] - }); +const { sharedModuleRules } = require('./webpack.additions'); - let clientConfig = webpackMerge({}, commonPartial, clientPartial, { - plugins: [ - getAotPlugin('client', !!options.aot) - ] - }); +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' + }, + { + 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 + ] + }, + plugins: [new CheckerPlugin()] + }; - if (webpackOptions.prod) { - clientConfig = webpackMerge({}, clientConfig, prodPartial); + // 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 + }) + ] + : [ + // new BundleAnalyzerPlugin(), + // Plugins that apply in production builds only + 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, + }, + }), + ] + ) } + }); - const configs = []; - if (!options.aot) { - configs.push(clientConfig, serverConfig); - - } else if (options.client) { - configs.push(clientConfig); - - } else if (options.server) { - configs.push(serverConfig); + // 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 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', + // 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 configs; -} + return [clientBundleConfig, serverBundleConfig]; +}; diff --git a/webpack.config.vendor.js b/webpack.config.vendor.js new file mode 100644 index 00000000..775910fb --- /dev/null +++ b/webpack.config.vendor.js @@ -0,0 +1,162 @@ +const path = require('path'); +const webpack = require('webpack'); +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', +]; +const nonTreeShakableModules = [ + // '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 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 ? [] : [ + + ]), + 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: [ + 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]; +} diff --git a/webpack/helpers.js b/webpack/helpers.js deleted file mode 100644 index 2b49ccc0..00000000 --- a/webpack/helpers.js +++ /dev/null @@ -1,9 +0,0 @@ -const { resolve } = require('path'); - -function root(path) { - return resolve(__dirname, '..', path); -} - -module.exports = { - root: root -}; diff --git a/webpack/webpack.aot.js b/webpack/webpack.aot.js deleted file mode 100644 index 5af7ca50..00000000 --- a/webpack/webpack.aot.js +++ /dev/null @@ -1,48 +0,0 @@ -const { root } = require('./helpers'); -const { AotPlugin } = require('@ngtools/webpack'); - -const tsconfigs = { - client: root('./Client/tsconfig.browser.json'), - server: root('./Client/tsconfig.server.json') -}; - -const aotTsconfigs = { - client: root('./Client/tsconfig.browser.json'), - server: root('./Client/tsconfig.server.aot.json') -}; - -/** - * Generates a AotPlugin for @ngtools/webpack - * - * @param {string} platform Should either be client or server - * @param {boolean} aot Enables/Disables AoT Compilation - * @returns - */ -function getAotPlugin(platform, aot) { - - var aotPlugin = new AotPlugin({ - tsConfigPath: aot ? aotTsconfigs[platform] : tsconfigs[platform], - skipCodeGeneration: !aot - }); - - // TEMPORARY fix for Windows 10 - will be gone when fixed - aotPlugin._compilerHost._resolve = function (path_to_resolve) { - path_1 = require("path"); - path_to_resolve = aotPlugin._compilerHost._normalizePath(path_to_resolve); - if (path_to_resolve[0] == '.') { - return aotPlugin._compilerHost._normalizePath(path_1.join(aotPlugin._compilerHost.getCurrentDirectory(), path_to_resolve)); - } - else if (path_to_resolve[0] == '/' || path_to_resolve.match(/^\w:\//)) { - return path_to_resolve; - } - else { - return aotPlugin._compilerHost._normalizePath(path_1.join(aotPlugin._compilerHost._basePath, path_to_resolve)); - } - }; - - return aotPlugin; -} - -module.exports = { - getAotPlugin: getAotPlugin -}; diff --git a/webpack/webpack.client.js b/webpack/webpack.client.js deleted file mode 100644 index 1c50c612..00000000 --- a/webpack/webpack.client.js +++ /dev/null @@ -1,20 +0,0 @@ -const { AotPlugin } = require('@ngtools/webpack'); - -const { root } = require('./helpers'); -const clientBundleOutputDir = root('./wwwroot/dist'); - -/** - * This is a client config which should be merged on top of common config - */ -module.exports = { - entry: { - 'main-browser': root('./Client/main.browser.ts') - }, - output: { - path: root('./wwwroot/dist'), - }, - target: 'web', - plugins: [ - - ] -}; diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js deleted file mode 100644 index 254ecf86..00000000 --- a/webpack/webpack.common.js +++ /dev/null @@ -1,25 +0,0 @@ -const { root } = require('./helpers'); - -/** - * This is a common webpack config which is the base for all builds - */ -module.exports = { - devtool: 'source-map', - resolve: { - extensions: ['.ts', '.js'] - }, - output: { - filename: '[name].js', - publicPath: '/dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix - }, - module: { - rules: [ - { test: /\.ts$/, loader: '@ngtools/webpack' }, - { test: /\.css$/, loader: ['to-string-loader', 'css-loader'] }, - { test: /\.html$/, loader: 'html-loader' }, - { test: /\.scss$/, loaders: ['to-string-loader', 'css-loader', 'sass-loader'] }, - { test: /\.(woff2?|ttf|eot|svg)$/, loader: 'url-loader?limit=10000' } - ] - }, - plugins: [] -}; diff --git a/webpack/webpack.prod.js b/webpack/webpack.prod.js deleted file mode 100644 index 658f1cd7..00000000 --- a/webpack/webpack.prod.js +++ /dev/null @@ -1,4 +0,0 @@ -/** - * This is a prod config to be merged with the Client config - */ -module.exports = {}; diff --git a/webpack/webpack.server.js b/webpack/webpack.server.js deleted file mode 100644 index e968400a..00000000 --- a/webpack/webpack.server.js +++ /dev/null @@ -1,22 +0,0 @@ -const { root } = require('./helpers'); -const { AotPlugin } = require('@ngtools/webpack'); - -/** - * This is a server config which should be merged on top of common config - */ -module.exports = { - devtool: 'inline-source-map', - resolve: { - extensions: ['.ts', '.js', '.json'], - // An array of directory names to be resolved to the current directory - modules: [root('Client'), root('node_modules')], - }, - entry: { - 'main-server': root('./Client/main.server.ts') - }, - output: { - libraryTarget: 'commonjs', - path: root('./Client/dist') - }, - target: 'node' -}; 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": "/" +}