mirror of
https://github.com/jcreek/jcreek.github.io.git
synced 2026-07-13 11:03:49 +00:00
chore(*): Move files into root
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
---
|
||||
tags:
|
||||
- web development
|
||||
- dotnet
|
||||
- asp
|
||||
- c#
|
||||
- vscode
|
||||
- devcontainer
|
||||
- remote-container
|
||||
- iac
|
||||
- postgres
|
||||
- smtp
|
||||
- dev environment
|
||||
---
|
||||
|
||||
# Setting up remote containers for IAC dev environments in dotnet 6 with postgres and SMTP
|
||||
|
||||
_2022-04-20_
|
||||
|
||||

|
||||
|
||||
In this post I will go through how to set up remote containers for a dotnet 6 API project using PostgreSQL and an SMTP server, so that any new devs picking up the project only have to clone down the git repo and open it in Visual Studio code to get started debugging it.
|
||||
|
||||
No dependencies need to be installed onto developer machines for each project they work on, other than:
|
||||
|
||||
- Docker
|
||||
- Visual Studio Code
|
||||
|
||||
Yes, that's all they need. They don't even need to install any extensions - the only one they need will be prompted the first time they open the project in vscode. Any extensions they need for the project will be loaded in a vscode server instance within the docker container, and then their local vscode will allow access to the vscode server as if they're using it natively, right down to providing terminal access within the container.
|
||||
|
||||
## How to set up the project
|
||||
|
||||
You will need to ensure that you've created a `.gitattributes` file in the root folder if you don't have one already, and specified `* text=auto eol=lf` to default all the line endings to LF, if you're on Windows and find that swapping between the host and the container changes the line endings in all your files. Alternatively, if you're already using Windows line endings, use `crlf` instead of `lf`.
|
||||
|
||||
In the root of the repo create a folder called `.devcontainer`. Within this you need to create three files:
|
||||
|
||||
1. `devcontainer.json`
|
||||
2. `docker-compose.yml`
|
||||
3. `Dockerfile`
|
||||
|
||||
### `devcontainer.json`
|
||||
|
||||
This is customised from the default Microsoft file. You can change the name, the vscode settings (although if you're working with newer dotnet projects I'd recommend leaving `omnisharp.useModernNet` set to `true`), add any extensions you want to use in vscode for this project (find the ID for an extension by installing it in your local vscode, then right clicking it and clicking `Copy Extension ID` - I've included some of my favourites for aspdotnet projects but the only one you *need* is `ms-dotnettools.csharp`), the post-create commands and the remote user.
|
||||
|
||||
There is an issue where Omnisharp, the underlying technology in the C# vscode plugin doesn't work properly unless `dotnet restore` is run at the point of setting up the dev container, so do not remove that command.
|
||||
|
||||
```json linenums="1"
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.231.6/containers/dotnet-postgres
|
||||
{
|
||||
"name": "Dotnet 6 API, Postgres & SMTP",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspace",
|
||||
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"omnisharp.useModernNet": true //Use the build of OmniSharp that runs on the .NET 6 SDK. This build requires that the .NET 6 SDK be installed and does not use Visual Studio MSBuild tools or Mono. It only supports newer SDK-style projects that are buildable with dotnet build. Unity projects and other Full Framework projects are not supported.
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"formulahendry.auto-rename-tag",
|
||||
"ms-dotnettools.csharp",
|
||||
"EditorConfig.EditorConfig",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"xabikos.JavaScriptSnippets",
|
||||
"PKief.material-icon-theme",
|
||||
"eg2.vscode-npm-script",
|
||||
"christian-kohler.path-intellisense",
|
||||
"esbenp.prettier-vscode",
|
||||
"gencer.html-slim-scss-css-class-completion"
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [5000, 5001],
|
||||
|
||||
// [Optional] To reuse of your local HTTPS dev cert:
|
||||
//
|
||||
// 1. Export it locally using this command:
|
||||
// * Windows PowerShell:
|
||||
// dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere"
|
||||
// * macOS/Linux terminal:
|
||||
// dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere"
|
||||
//
|
||||
// 2. Uncomment these 'remoteEnv' lines:
|
||||
// "remoteEnv": {
|
||||
// "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere",
|
||||
// "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx",
|
||||
// },
|
||||
//
|
||||
// 3. Next, copy your certificate into the container:
|
||||
// 1. Start the container
|
||||
// 2. Drag ~/.aspnet/https/aspnetapp.pfx into the root of the file explorer
|
||||
// 3. Open a terminal in VS Code and run "mkdir -p /home/vscode/.aspnet/https && mv aspnetapp.pfx /home/vscode/.aspnet/https"
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "dotnet restore", // had to use otherwise omnisharp wouldn't work properly, so no intellisense in vscode
|
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode"
|
||||
}
|
||||
```
|
||||
|
||||
### `docker-compose.yml`
|
||||
|
||||
The docker compose file sets up the three components of this example project:
|
||||
|
||||
1. The dotnet 6 API
|
||||
2. The PostgreSQL database
|
||||
3. The SMTP server
|
||||
|
||||
Take note of the name of the SMTP server container as this will be needed later, as will the environment variables for the database. These should be changed from `postgres`.
|
||||
|
||||
```yml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
# Update 'VARIANT' to pick a version of .NET: 3.1, 5.0, 6.0
|
||||
VARIANT: "6.0"
|
||||
# Optional version of Node.js
|
||||
NODE_VERSION: "lts/*"
|
||||
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
|
||||
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
||||
network_mode: service:db
|
||||
|
||||
# Uncomment the next line to use a non-root user for all processes.
|
||||
# user: vscode
|
||||
|
||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
db:
|
||||
image: postgres:14.1
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: postgres
|
||||
|
||||
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
mail:
|
||||
image: bytemark/smtp
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
|
||||
```
|
||||
|
||||
### `Dockerfile`
|
||||
|
||||
This comes directly from Microsoft, I've made no modifications to it.
|
||||
|
||||
```dockerfile
|
||||
# [Choice] .NET version: 6.0, 5.0, 3.1, 6.0-bullseye, 5.0-bullseye, 3.1-bullseye, 6.0-focal, 5.0-focal, 3.1-focal
|
||||
ARG VARIANT="6.0"
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT}
|
||||
|
||||
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
|
||||
ARG NODE_VERSION="none"
|
||||
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
# [Optional] Uncomment this line to install global node packages.
|
||||
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
|
||||
```
|
||||
|
||||
### `appsettings.json` and `appsettings.Development.json`
|
||||
|
||||
To get the SMTP working, use these keys. If you're doing anything more fancy with `bytemark/smtp` than just the default functionality you may need to change the SMTP port.
|
||||
|
||||
```json linenums="1"
|
||||
"SmtpHost": "mail",
|
||||
"SmtpPort": 25,
|
||||
```
|
||||
|
||||
To connect to the Postgres database set your connection string to `"User ID=postgres;Password=postgres;Host=db;Port=5432;Database=postgres;Pooling=true;"`, substituting in your chosen username, password and database name.
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
tags:
|
||||
- web development
|
||||
- dotnet
|
||||
- c#
|
||||
- pattern matching
|
||||
---
|
||||
|
||||
# C# 8 Pattern Matching
|
||||
|
||||
_2022-02-15_
|
||||
|
||||
This serves as introductory documentation to the pattern matching features introduced in C# 8, which I think are particularly useful.
|
||||
|
||||
## Deconstructors & Positional Patterns
|
||||
|
||||
Adding a deconstructor to your class can be done as below. It must be named `Deconstruct` and be a `public void`. Any values we want deconstructed can be set as `out` parameters, then populated. Here all properties are being deconstructed, but a subset could be deconstructed if desired.
|
||||
|
||||
```csharp linenums="1"
|
||||
class Student
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Year { get; set; }
|
||||
public Teacher FormTutor { get; set; }
|
||||
|
||||
public void Deconstruct (out string name, out int year, out Teacher formTutor)
|
||||
{
|
||||
name = Name;
|
||||
year = Year;
|
||||
formTutor = FormTutor;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To use positional parameters, I'll also set up a second model.
|
||||
|
||||
```csharp linenums="1"
|
||||
class Teacher
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Subject { get; set; }
|
||||
|
||||
public void Deconstruct (out string name, out string subject)
|
||||
{
|
||||
name = Name;
|
||||
subject = Subject;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here is an example positional pattern. The discards (`_`) are used to 'match all'.
|
||||
|
||||
```csharp linenums="1"
|
||||
public static class YourClassHere
|
||||
{
|
||||
public static bool IsInYear9English(Student student)
|
||||
{
|
||||
return student is Student(
|
||||
_,
|
||||
9,
|
||||
Teacher (_, "English")
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
While an if statement would probably be easier to read and maintain in this example, positional patterns enable recursion to be used, which can be very useful.
|
||||
|
||||
## Property Patterns
|
||||
|
||||
I've updated the Student model to add a School property.
|
||||
|
||||
```csharp linenums="1"
|
||||
class Student
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Year { get; set; }
|
||||
public string School { get; set; }
|
||||
public Teacher FormTutor { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
I prefer using property patterns to positional patterns as they are much more readable. Here's an example method to check whether a student attends a particular school and has a form tutor who teaches Maths.
|
||||
|
||||
```csharp linenums="1"
|
||||
public static class YourClassHere
|
||||
{
|
||||
public static bool IsStudentInGreenAcademyWithMathsFormTutor(Student student)
|
||||
{
|
||||
return student is {
|
||||
School: "Green Academy",
|
||||
FormTutor: {
|
||||
Subject: "Maths"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This can be made more generic to accept an `object` rather than a `Student`, and check that object is a Student.
|
||||
|
||||
```csharp linenums="1"
|
||||
public static class YourClassHere
|
||||
{
|
||||
public static bool IsStudentInGreenAcademyWithMathsFormTutor(object obj)
|
||||
{
|
||||
return obj is Student student &&
|
||||
student is {
|
||||
School: "Green Academy",
|
||||
FormTutor: {
|
||||
Subject: "Maths"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Switch Expressions
|
||||
|
||||
These can be used in place of standard switch cases. I'm using a discard (`_`) for catching the default case for unmatched patterns.
|
||||
|
||||
```csharp linenums="1"
|
||||
public static class YourClassHere
|
||||
{
|
||||
public static string DisplayPersonInfo (object person)
|
||||
{
|
||||
string result = person switch
|
||||
{
|
||||
Student student => $"Student at {student.School} in Year {student.Year}",
|
||||
Teacher teacher => $"Teacher of {teacher.Subject}",
|
||||
_ => "Person is not a student or a teacher"
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The syntax can make it easier to read, and it can be a lot more powerful. You can also define recursive switch patterns.
|
||||
|
||||
```csharp linenums="1"
|
||||
public static class YourClassHere
|
||||
{
|
||||
public static string DisplayPersonInfo (object person)
|
||||
{
|
||||
string result = person switch
|
||||
{
|
||||
Student student => student switch
|
||||
{
|
||||
_ when student.Year < 10 and student.Year > 6 => "Student in Key Stage 3",
|
||||
_ when student.Year >= 10 and student.Year <= 13> => "Student in Key Stage 4",
|
||||
_ => $"Student in Year {student.Year}"
|
||||
},
|
||||
Teacher teacher => $"Teacher of {teacher.Subject}",
|
||||
_ => "Person is not a student or a teacher"
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tuple Patterns (with Switch Expressions)
|
||||
|
||||
You can also use Switch Expressions with Tuples to write even more useful code. For example, you could be creating a game with crafting, combining two items to make another.
|
||||
|
||||
```csharp linenums="1"
|
||||
public static class YourClassHere
|
||||
{
|
||||
public static CraftingMaterial GetCraftingMaterial (CraftingMaterial item1, CraftingMaterial item2)
|
||||
{
|
||||
return (item1, item2) switch
|
||||
{
|
||||
// Match the items in both positions
|
||||
(CraftingMaterial.MountainFlowers, CraftingMaterial.SoulGems) => CraftingMaterial.DwemerMetal,
|
||||
(CraftingMaterial.SoulGems, CraftingMaterial.MountainFlowers) => CraftingMaterial.DwemerMetal,
|
||||
|
||||
(CraftingMaterial.Ore, CraftingMaterial.DwemerMetal) => CraftingMaterial.Ingots,
|
||||
(CraftingMaterial.DwemerMetal, CraftingMaterial.Ore) => CraftingMaterial.Ingots,
|
||||
|
||||
// Handle both items being the same (discard for both, to match any)
|
||||
(_, _) when item1 == item2 => item1,
|
||||
|
||||
// Default case (with discard)
|
||||
_ => CraftingMaterial.Unknown
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is lovely and easy to read, as well as very powerful.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
layout: default
|
||||
title: Web Development
|
||||
nav_order: 3
|
||||
has_children: true
|
||||
---
|
||||
@@ -0,0 +1,346 @@
|
||||
---
|
||||
tags:
|
||||
- web development
|
||||
- dotnet
|
||||
- asp
|
||||
- c#
|
||||
- serilog
|
||||
- jsnlog
|
||||
---
|
||||
|
||||
# Adding JSNLog to ASP .Net 6 with Serilog
|
||||
|
||||
_2022-01-18_
|
||||
|
||||
In this post I will go through the steps required to get an optimal install of JSNLog in ASP dotnet 6 projects using Serilog. JSNLog is a fantastic tool for logging uncaught javascript exceptions to the backend's logging system, and can be used for a variety of other things. This config includes batching of messages to reduce calls made to the server, as well as buffering, where helpful debugging logs are only sent if there's a fatal log that's also being sent, to make it easier to diagnose bugs without unnecessary diagnostic logs appearing at all times.
|
||||
|
||||
First you need to install `JSNLog` and `Destructurama.JsonNet` via NuGet. After that, make the following changes to these files.
|
||||
|
||||
## _ViewImports.cshtml
|
||||
|
||||
Add `@addTagHelper "*, jsnlog"` to enable the tag helper.
|
||||
|
||||
## Startup.cs
|
||||
|
||||
```csharp linenums="1"
|
||||
using Destructurama;
|
||||
using JSNLog;
|
||||
|
||||
...
|
||||
public Startup(IConfiguration configuration, IWebHostEnvironment environment)
|
||||
{
|
||||
...
|
||||
Log.Logger = SerilogLogger.CreateSerilogLogger(serilogConfiguration).Destructure.JsonNetTypes().CreateLogger();
|
||||
...
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
public void Configure(
|
||||
...,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
...
|
||||
loggerFactory.AddSerilog();
|
||||
|
||||
JsnlogConfiguration jsnlogConfiguration = new JsnlogConfiguration
|
||||
{
|
||||
ajaxAppenders = new List<AjaxAppender> {
|
||||
new AjaxAppender {
|
||||
name = "appender1",
|
||||
storeInBufferLevel = "TRACE", // Log messages with severity smaller than TRACE are ignored
|
||||
level = "WARN", // Log messages with severity equal or greater than TRACE and lower than WARN are stored in the internal buffer, but not sent to the server
|
||||
// Log messages with severity equal or greater than WARN and lower than FATAL are sent to the server on their own
|
||||
sendWithBufferLevel = "FATAL", // Log messages with severity equal or greater than FATAL are sent to the server, along with all messages stored in the internal buffer
|
||||
bufferSize = 20, // Stores the last up to 20 debug messages in browser memory,
|
||||
batchSize = 20,
|
||||
batchTimeout = 2000, // Logs are guaranteed to be sent within this period (in ms), even if the batch size has not been reached yet
|
||||
maxBatchSize = 50 // When the server is unreachable and log messages are being stored until it is reachable again, this is the maximum number of messages that will be stored. Cannot be smaller than batchSize
|
||||
}
|
||||
},
|
||||
loggers = new List<Logger> { // Get the loggers to use the new appender
|
||||
new Logger {
|
||||
appenders = "appender1"
|
||||
}
|
||||
},
|
||||
insertJsnlogInHtmlResponses = false, // There's an outstanding bug setting this to true so the workaround is using the jl-javascript-logger-definitions tag helper in _Layout.cshtml, via the reference in _ViewImports.cshtml
|
||||
productionLibraryPath = null // We're using a fallback from the CDN with hashing, so do not use this
|
||||
};
|
||||
|
||||
// Must be before UseStaticFiles and UseAuthorization
|
||||
app.UseJSNLog(new CustomLoggingAdapter(loggerFactory), jsnlogConfiguration);
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## _Layout.cshtml
|
||||
|
||||
This one has some interesting configuration.
|
||||
|
||||
Firstly, using local fallbacks if the CDN versions of the scripts are unavailable, or do not match their sha384 hashes (i.e. have been hacked or changed).
|
||||
|
||||
Secondly, it injects some additional information into the logs: client IP address, user ID (your model may differ from my example), and the user agent.
|
||||
|
||||
Thirdly, it sets up logging any uncaught JS exceptions, and any exceptions inside promises where no rejection method is provided.
|
||||
|
||||
Fourthly, using stacktrace.js it handles getting the relevant stacktrace using sourcemaps.
|
||||
|
||||
```html linenums="1"
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsnlog/2.30.0/jsnlog.min.js"
|
||||
asp-fallback-src="~/jsnlog.min.js"
|
||||
asp-fallback-test="window.JL"
|
||||
crossorigin="anonymous"
|
||||
integrity="sha384-ANmgu3V8Mc5/Usd/GeIS0xu0spgLKTIqkSMQAgVvV5C2SRp/rICkLLw5XG/u6BQ9">
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/stacktrace.js/2.0.2/stacktrace.min.js"
|
||||
asp-fallback-src="~/stacktrace.min.js"
|
||||
asp-fallback-test="window.StackTrace"
|
||||
crossorigin="anonymous"
|
||||
integrity="sha384-4PjQM+vlPbdcaPnFOyBIOfqz90Hvhp+QHb3rBMOy78OaxDHw9mnmzjUSNqJkn+W5">
|
||||
</script>
|
||||
|
||||
<jl-javascript-logger-definitions />
|
||||
|
||||
<script>
|
||||
async function fetchClientIp() {
|
||||
try {
|
||||
let response = await fetch('https://api.ipify.org?format=json');
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
JL().fatalException("Failed to get client IP for logging", error);
|
||||
}
|
||||
}
|
||||
fetchClientIp().then(data => {
|
||||
const userDetailsForLogging = {
|
||||
'clientIp': data.ip,
|
||||
'userId': '@Model.UserId',
|
||||
'userAgent': navigator.userAgent
|
||||
}
|
||||
@* Log uncaught JavaScript errors to the serverside log *@
|
||||
if (window) {
|
||||
window.onerror = function (errorMsg, url, lineNumber, column, errorObj) {
|
||||
var callback = function(stackframes) {
|
||||
var stringifiedStack = stackframes.map(function(sf) {
|
||||
return sf.toString();
|
||||
}).join('\n');
|
||||
const msgObj = {
|
||||
'msg': 'Uncaught exception',
|
||||
'sourceMapStack': stringifiedStack,
|
||||
'user': userDetailsForLogging
|
||||
};
|
||||
JL('serverLog').fatalException({
|
||||
msg: JSON.stringify(msgObj),
|
||||
errorMsg: errorMsg,
|
||||
url: url,
|
||||
lineNumber: lineNumber,
|
||||
column: column
|
||||
}, errorObj);
|
||||
};
|
||||
var errback = function(err) { console.log(err.message); };
|
||||
StackTrace.fromError(errorObj).then(callback).catch(errback);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
@* Log uncaught JavaScript exceptions inside promises where no rejection method is provided *@
|
||||
if (typeof window !== 'undefined') {
|
||||
window.onunhandledrejection = function (event) {
|
||||
var callback = function(stackframes) {
|
||||
var stringifiedStack = stackframes.map(function(sf) {
|
||||
return sf.toString();
|
||||
}).join('\n');
|
||||
|
||||
const msgObj = {
|
||||
'msg': 'Unhandled promise rejection',
|
||||
'sourceMapStack': stringifiedStack,
|
||||
'user': userDetailsForLogging
|
||||
};
|
||||
JL("onerrorLogger").fatalException({
|
||||
msg: JSON.stringify(msgObj),
|
||||
errorMsg: event.reason ? event.reason.message : null
|
||||
}, event.reason);
|
||||
};
|
||||
var errback = function(err) { console.log(err.message); };
|
||||
StackTrace.fromError(errorObj).then(callback).catch(errback);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## CustomLoggingAdapter.cs
|
||||
|
||||
```csharp linenums="1"
|
||||
using JSNLog;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text;
|
||||
|
||||
namespace YourProjectHere
|
||||
{
|
||||
/// <summary>
|
||||
/// This adapter is required to get JavaScript objects logged by JSNLog to appear correctly in Serilog. Regretably it is required
|
||||
/// because of a defficiency in JSNLog that hasn't been fixed in at least 5 years at the time of writing, and depends on slightly
|
||||
/// customised versions of classes and methods taken from that library, included here in this one file for tidiness.
|
||||
/// </summary>
|
||||
public class CustomLoggingAdapter : ILoggingAdapter
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
public CustomLoggingAdapter(ILoggerFactory loggerFactory)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
public void Log(FinalLogData finalLogData)
|
||||
{
|
||||
ILogger logger = _loggerFactory.CreateLogger(finalLogData.FinalLogger);
|
||||
Object message = LogMessageHelpers.DeserializeIfPossible(finalLogData.FinalMessage);
|
||||
switch (finalLogData.FinalLevel)
|
||||
{
|
||||
case Level.TRACE: logger.LogTrace("{@logMessage}", message); break;
|
||||
case Level.DEBUG: logger.LogDebug("{@logMessage}", message); break;
|
||||
case Level.INFO: logger.LogInformation("{@logMessage}", message); break;
|
||||
case Level.WARN: logger.LogWarning("{@logMessage}", message); break;
|
||||
case Level.ERROR: logger.LogError("{@logMessage}", message); break;
|
||||
case Level.FATAL: logger.LogCritical("{@logMessage}", message); break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
internal class LogMessageHelpers
|
||||
{
|
||||
public static T DeserializeJson<T>(string json)
|
||||
{
|
||||
T result = JsonConvert.DeserializeObject<T>(json);
|
||||
return result;
|
||||
}
|
||||
public static bool IsPotentialJson(string msg)
|
||||
{
|
||||
string trimmedMsg = msg.Trim();
|
||||
return (trimmedMsg.StartsWith("{") && trimmedMsg.EndsWith("}"));
|
||||
}
|
||||
/// <summary>
|
||||
/// Tries to deserialize msg.
|
||||
/// If that works, returns the resulting object.
|
||||
/// Otherwise returns msg itself (which is a string).
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <returns></returns>
|
||||
public static Object DeserializeIfPossible(string msg)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsPotentialJson(msg))
|
||||
{
|
||||
Object result = DeserializeJson<Object>(msg);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns true if the msg contains a valid JSON string.
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsJsonString(string msg)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsPotentialJson(msg))
|
||||
{
|
||||
// Try to deserialise the msg. If that does not throw an exception,
|
||||
// decide that msg is a good JSON string.
|
||||
DeserializeJson<Dictionary<string, Object>>(msg);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/// <summary>
|
||||
/// Takes a log message and finds out if it contains a valid JSON string.
|
||||
/// If so, returns it unchanged.
|
||||
///
|
||||
/// Otherwise, surrounds the string with quotes (") and escapes the string for JavaScript.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string EnsureValidJson(string msg)
|
||||
{
|
||||
if (IsJsonString(msg))
|
||||
{
|
||||
return msg;
|
||||
}
|
||||
return JavaScriptStringEncode(msg, true);
|
||||
}
|
||||
public static string JavaScriptStringEncode(string value, bool addDoubleQuotes)
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
return System.Web.HttpUtility.JavaScriptStringEncode(value, addDoubleQuotes);
|
||||
#else
|
||||
// copied from https://github.com/mono/mono/blob/master/mcs/class/System.Web/System.Web/HttpUtility.cs
|
||||
if (String.IsNullOrEmpty(value))
|
||||
return addDoubleQuotes ? "\"\"" : String.Empty;
|
||||
int len = value.Length;
|
||||
bool needEncode = false;
|
||||
char c;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
c = value[i];
|
||||
if (c >= 0 && c <= 31 || c == 34 || c == 39 || c == 60 || c == 62 || c == 92)
|
||||
{
|
||||
needEncode = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!needEncode)
|
||||
return addDoubleQuotes ? "\"" + value + "\"" : value;
|
||||
var sb = new StringBuilder();
|
||||
if (addDoubleQuotes)
|
||||
sb.Append('"');
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
c = value[i];
|
||||
if (c >= 0 && c <= 7 || c == 11 || c >= 14 && c <= 31 || c == 39 || c == 60 || c == 62)
|
||||
sb.AppendFormat("\\u{0:x4}", (int)c);
|
||||
else switch ((int)c)
|
||||
{
|
||||
case 8:
|
||||
sb.Append("\\b");
|
||||
break;
|
||||
case 9:
|
||||
sb.Append("\\t");
|
||||
break;
|
||||
case 10:
|
||||
sb.Append("\\n");
|
||||
break;
|
||||
case 12:
|
||||
sb.Append("\\f");
|
||||
break;
|
||||
case 13:
|
||||
sb.Append("\\r");
|
||||
break;
|
||||
case 34:
|
||||
sb.Append("\\\"");
|
||||
break;
|
||||
case 92:
|
||||
sb.Append("\\\\");
|
||||
break;
|
||||
default:
|
||||
sb.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (addDoubleQuotes)
|
||||
sb.Append('"');
|
||||
return sb.ToString();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,336 @@
|
||||
---
|
||||
tags:
|
||||
- web development
|
||||
- nodejs
|
||||
- typescript
|
||||
- eslint
|
||||
- webpack
|
||||
- babel
|
||||
- sourcemaps
|
||||
- scss
|
||||
- vscode
|
||||
- devcontainer
|
||||
- docker
|
||||
---
|
||||
|
||||
# How to set up a Node.js Typescript project well
|
||||
|
||||
_2022-06-15_
|
||||
|
||||
This post covers how to set up a project with Node.js and Typescript using eslint, webpack, babel, source maps, SCSS and vscode devcontainers using Docker.
|
||||
|
||||
This project makes use of eslint with airbnb styling and typescript support.
|
||||
|
||||
All linting errors can be automatically fixed (usually) by running `npm run lint -- --fix` before you commit your changes.
|
||||
|
||||
## Getting it running locally for development
|
||||
|
||||
### Devcontainer + vscode
|
||||
|
||||
If you have `Visual Studio Code` and `Docker` installed then you can open this project using the `Remote - Containers` extension (ms-vscode-remote.remote-containers). Click the button at the bottom left of vscode, then 'Reopen in container'. You can now run the application using the vscode debugger.
|
||||
|
||||
### Standard local install
|
||||
|
||||
Running `npm run dev` will build the development version in the `dist` folder and enable hot reloading. It will also open the application in a new browser tab automatically.
|
||||
|
||||
Running `npm run build` will create the production assets in the `dist` folder.
|
||||
|
||||
## Devcontainer
|
||||
|
||||
In the `.devcontainer` folder you'll need three files.
|
||||
|
||||
### `base.Dockerfile`
|
||||
|
||||
```dockerfile
|
||||
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
|
||||
ARG VARIANT=16-bullseye
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
|
||||
|
||||
# Install tslint, typescript. eslint is installed by javascript image
|
||||
ARG NODE_MODULES="tslint-to-eslint-config typescript"
|
||||
COPY library-scripts/meta.env /usr/local/etc/vscode-dev-containers
|
||||
RUN su node -c "umask 0002 && npm install -g ${NODE_MODULES}" \
|
||||
&& npm cache clean --force > /dev/null 2>&1
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
# [Optional] Uncomment if you want to install an additional version of node using nvm
|
||||
# ARG EXTRA_NODE_VERSION=10
|
||||
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
||||
|
||||
```
|
||||
|
||||
### `devcontainer.json`
|
||||
|
||||
```json linenums="1"
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/typescript-node
|
||||
{
|
||||
"name": "Node.js & TypeScript",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
// Update 'VARIANT' to pick a Node version: 18, 16, 14.
|
||||
// Append -bullseye or -buster to pin to an OS version.
|
||||
// Use -bullseye variants on local on arm64/Apple Silicon.
|
||||
"args": {
|
||||
"VARIANT": "16-bullseye"
|
||||
}
|
||||
},
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [3000],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "npm install",
|
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "node"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### `Dockerfile`
|
||||
|
||||
One important thing to note in this file is the installation of `inotify-tools` to the underlying Debian OS, as this is required for webpack to be able to poll and do hot reloading.
|
||||
|
||||
```dockerfile
|
||||
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
|
||||
ARG VARIANT=16-bullseye
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install --no-install-recommends inotify-tools
|
||||
|
||||
# [Optional] Uncomment if you want to install an additional version of node using nvm
|
||||
# ARG EXTRA_NODE_VERSION=10
|
||||
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
||||
|
||||
# [Optional] Uncomment if you want to install more global node packages
|
||||
# RUN su node -c "npm install -g <your-package-list -here>"
|
||||
|
||||
```
|
||||
|
||||
## Visual Studio Code Debugging
|
||||
|
||||
Within the `.vscode` folder you'll need to create this `launch.json` file:
|
||||
|
||||
```json linenums="1"
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch via NPM",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"dev"
|
||||
],
|
||||
"runtimeExecutable": "npm",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "pwa-node",
|
||||
"stopOnEntry": true,
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Eslint
|
||||
|
||||
Create the following files:
|
||||
|
||||
### `.eslintignore`
|
||||
|
||||
Assuming you're using git, this will be the same as your `.gitignore` file.
|
||||
|
||||
```text
|
||||
dist
|
||||
node_modules
|
||||
```
|
||||
|
||||
### `.eslintrc.json`
|
||||
|
||||
```json linenums="1"
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"airbnb-base",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
"js": "never",
|
||||
"jsx": "never",
|
||||
"ts": "never",
|
||||
"tsx": "never"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Webpack with SCSS and TS support, plus hot reloading
|
||||
|
||||
The structure for this application is to have a `src` folder that contains: all `.html` and `.ts`; `assets` folder for images; `styles` folder for `.scss` files. The files will be compiled to a `dist` folder for deploying to a web server.
|
||||
|
||||
Create the following files:
|
||||
|
||||
### `custom.d.ts`
|
||||
|
||||
```ts linenums="1"
|
||||
declare module '*.svg' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
```
|
||||
|
||||
### `tsconfig.json`
|
||||
|
||||
```json linenums="1"
|
||||
{
|
||||
"compilerOptions": {
|
||||
"preserveConstEnums": true
|
||||
},
|
||||
"include": ["src/**/*", "src/custom.d.ts"],
|
||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||
}
|
||||
```
|
||||
|
||||
### `webpack.config.js`
|
||||
|
||||
```js linenums="1"
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: {
|
||||
bundle: path.resolve(__dirname, 'src/index.ts'),
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: '[name].[contenthash].js',
|
||||
clean: true,
|
||||
assetModuleFilename: '[name].[contenthash][ext]',
|
||||
},
|
||||
devtool: 'source-map',
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.resolve(__dirname, 'dist'),
|
||||
},
|
||||
port: 3000,
|
||||
open: true,
|
||||
hot: true,
|
||||
compress: true,
|
||||
historyApiFallback: true,
|
||||
},
|
||||
watchOptions: { poll: true },
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
// Use these loaders for any matching scss file types
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader',
|
||||
'sass-loader',
|
||||
],
|
||||
},
|
||||
{
|
||||
// Add backwards compatibility
|
||||
test: /\.(js|ts)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env', '@babel/preset-typescript'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Add support for images
|
||||
test: /\.(png|svg|jpg|jpeg|gif)$/i,
|
||||
type: 'asset/resource',
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
// Specify the order in which to resolve files by their extension
|
||||
extensions: ['*', '.js', '.ts'],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### `package.json`
|
||||
|
||||
```json linenums="1"
|
||||
{
|
||||
"name": "your app name",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"dev": "webpack serve",
|
||||
"lint": "eslint --ignore-path .eslintignore --ext .js,.ts ."
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": ""
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.5",
|
||||
"@babel/preset-env": "^7.18.2",
|
||||
"@babel/preset-typescript": "^7.17.12",
|
||||
"@typescript-eslint/eslint-plugin": "^5.28.0",
|
||||
"@typescript-eslint/parser": "^5.28.0",
|
||||
"babel-loader": "^8.2.5",
|
||||
"css-loader": "^6.7.1",
|
||||
"eslint": "^8.17.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"sass": "^1.52.3",
|
||||
"sass-loader": "^13.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"typescript": "^4.7.3",
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.9.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
tags:
|
||||
- web development
|
||||
- dotnet
|
||||
- c#
|
||||
- vue
|
||||
- asp
|
||||
---
|
||||
|
||||
# Creating a reactive SPA simply within an ASP.Net Core web app with Vue.js
|
||||
|
||||
_2021-07-12_
|
||||
|
||||

|
||||
|
||||
In this post, I will cover how you can quickly and easily use Vue.js to make a reactive 'SPA' within ASP.Net Core. This is a slightly dirty way to do things, but for rapid prototyping or a simple project it does the job just fine without any unnecessary complications. By 'SPA' I mean a Single Page Application, except without any navigation as that's handled by the underlying ASP.Net Core application, as is the API called by Vue.js methods.
|
||||
|
||||
In the `<head>` tags of your Razor page/view, you'll need to include a reference to Vue.js, for example via a CDN.
|
||||
|
||||
`<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>`
|
||||
|
||||
At the bottom of your Razor page/view, you'll need to include a `<script>` tag to hold your Vue instance definition. In Vue.js this includes:
|
||||
|
||||
- the element to have the Vue instance applied to
|
||||
- the data object including any default properties
|
||||
- computed properties (these act as if part of the data object but reactively update like functions)
|
||||
- methods (functions that can interact with the data object)
|
||||
|
||||
In this example, it's a Vue instance for creating invoices and updating stock levels. The above gif shows some of the functionality achieved with this, including dynamically adding classes, and looping through elements in an array with markup for each.
|
||||
|
||||
```html linenums="1"
|
||||
<script>
|
||||
const createInvoice = new Vue({
|
||||
el: '#create-invoice',
|
||||
data() {
|
||||
return {
|
||||
errorMessage: '',
|
||||
customer: {
|
||||
Name: '',
|
||||
Address1: '',
|
||||
Address2: '',
|
||||
Address3: '',
|
||||
PostCode: ''
|
||||
},
|
||||
invoiceNumber: new Date().valueOf(),
|
||||
products: [],
|
||||
successMessage: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
calculatedVat() {
|
||||
// take total and get 20% of it
|
||||
return this.total * 0.2;
|
||||
},
|
||||
subTotal() {
|
||||
// Take total and get 80% of it
|
||||
return this.total * 0.8;
|
||||
},
|
||||
total() {
|
||||
// For each product, add total price
|
||||
let total = 0;
|
||||
this.products.forEach(product => {
|
||||
total += product.totalPrice;
|
||||
});
|
||||
|
||||
return total - this.promoDiscount;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
calculateItemTotal(product) {
|
||||
const totalPrice = product.unitPrice * product.quantity;
|
||||
Vue.set(product, 'totalPrice', totalPrice)
|
||||
},
|
||||
getDuplicateBarcodeClass(product) {
|
||||
const matchingBarcodes = this.products.filter(obj => {
|
||||
return obj.barcode === product.barcode;
|
||||
})
|
||||
|
||||
if (matchingBarcodes.length > 1) {
|
||||
return 'alert-danger';
|
||||
}
|
||||
},
|
||||
removeProduct(product) {
|
||||
const index = this.products.findIndex(
|
||||
(x) => x.barcode === product.barcode
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
this.products.splice(index, 1);
|
||||
}
|
||||
},
|
||||
updateStock() {
|
||||
// Check the user really wants to update the stock and start a new invoice
|
||||
const userResponse = confirm("Are you sure you wish to finish this invoice and update stock levels? You will not be able to print it again.");
|
||||
if (userResponse) {
|
||||
const data = {
|
||||
invoiceNumber: String(this.invoiceNumber),
|
||||
products: this.products,
|
||||
};
|
||||
|
||||
// Update the stock & log the invoice
|
||||
fetch(`Invoices/UpdateStockAndLogInvoice/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.statusCode === 404) {
|
||||
Vue.set(this, "errorMessage", data.message);
|
||||
} else {
|
||||
Vue.set(this, 'successMessage', data.message);
|
||||
|
||||
// If no errors, clear the products list to be ready for the next invoice
|
||||
Vue.set(this, "products", []);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
Vue.set(this, "errorMessage", err);
|
||||
});
|
||||
}
|
||||
},
|
||||
validateNumber: (event) => {
|
||||
// Only accept integers, so prevent anything but digits 0-9
|
||||
let keyCode = event.keyCode;
|
||||
if (keyCode < 48 || keyCode > 57) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
viewAndPrintInvoice() {
|
||||
if (this.isDuplicateBarcode) {
|
||||
alert("There is a duplicate barcode - look for red barcodes and remove duplicates");
|
||||
}
|
||||
else {
|
||||
window.print();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
To hook this into the DOM, just add the element identifier to an element, for example `<div id="create-invoice">` will bind a div to this Vue instance.
|
||||
|
||||
Within that, you are free to use a variety of tools, like `v-if`, interpolation and `v-on` event handlers. In this example, if there's any text in the `successMessage` property of the Vue data object, this alert div will be displayed, interpolating the message, and clearing it (thus hiding the alert) when the close button is clicked.
|
||||
|
||||
```html linenums="1"
|
||||
<div v-if="successMessage" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle-fill"></i>
|
||||
<strong>Success:</strong> {{ successMessage }}
|
||||
<button type="button" class="close" aria-label="Close" v-on:click="successMessage = ''">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, a `v-for` is used to loop through all the products in the array, producing a `<tr>` for each of them, with an appropriate class bound from a method taking that product as a parameter. `v-if` and `v-else` statements enable conditionally showing one (or none) of three different pooltips. The product's `name` property is interpolated and shown, as is the `unitPrice` property, which is nicely formatted for readability.
|
||||
|
||||
The `barcode` field is mapped to a `v-model`, which means as it changes it directly alters the value in the Vue data object, and as that changes this updates reactively. It also makes use of `v-on:blur` to run a method that makes an API call to retrieve product data, conditionally sets the disabled property once a barcode has been entered, and binds a class based on the output of a method taking the product as a parameter.
|
||||
|
||||
The `quantity` field also uses a `v-model`, but specifically forces it to be a number (no need for Typescript here) runs a method on keypress (the `@@` syntax is purely due to Razor escaping, normally you'd only need one) and on change.
|
||||
|
||||
```html linenums="1"
|
||||
<tr v-for="product in products" v-bind:class="getProductRowClass(product)">
|
||||
<td>
|
||||
<div class="tooltip" v-if="product.barcode.length > 0 && product.stockLevel - product.quantity < 0">
|
||||
<i class="bi bi-x-circle-fill text-danger"></i>
|
||||
<span class="tooltiptext">You will need to order more stock to fulfill this order</span>
|
||||
</div>
|
||||
<div class="tooltip" v-else-if="product.barcode.length > 0 && product.stockLevel - product.quantity == 0">
|
||||
<i class="bi bi-exclamation-triangle-fill text-warning"></i>
|
||||
<span class="tooltiptext">You will run out of stock</span>
|
||||
</div>
|
||||
<div class="tooltip" v-else-if="product.barcode.length > 0 && product.stockLevel - product.quantity <= 10">
|
||||
<i class="bi bi-info-circle-fill text-info"></i>
|
||||
<span class="tooltiptext">Stock will be low after this order</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<label>{{product.name}}</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="barcode" v-model="product.barcode" v-on:blur="getProductInformation(product)" :disabled="product.barcode.length > 0" v-bind:class="getDuplicateBarcodeClass(product)">
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" step="1" name="quantity" v-model.number="product.quantity" @@keypress="validateNumber" v-on:change="calculateItemTotal(product)">
|
||||
</td>
|
||||
<td>
|
||||
<label>£{{parseFloat(product.unitPrice).toFixed(2)}}</label>
|
||||
</td>
|
||||
</tr>
|
||||
```
|
||||
|
||||
This is a very simple, quick and dirty example, but hopefully it gives you an idea of the kind of things you can quickly and easily achieve using Vue.js in this way. While this example uses ASP.Net Core, you could easily plug this into any other website, so long as it's using HTML, CSS and JavaScript, and there is an API to make requests to.
|
||||
Reference in New Issue
Block a user