diff --git a/.frontmatter/database/mediaDb.json b/.frontmatter/database/mediaDb.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/.frontmatter/database/mediaDb.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/.frontmatter/database/pinnedItemsDb.json b/.frontmatter/database/pinnedItemsDb.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/.frontmatter/database/pinnedItemsDb.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/.frontmatter/database/taxonomyDb.json b/.frontmatter/database/taxonomyDb.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/.frontmatter/database/taxonomyDb.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/BlazeKit.sln b/BlazeKit.sln
index 761cb3f..cb832c8 100644
--- a/BlazeKit.sln
+++ b/BlazeKit.sln
@@ -25,19 +25,21 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Reactivity.Tests",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{6B6EFC67-31B4-4D2E-98D5-1A731AF98B0B}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazeKit.Reactivity", "src\BlazeKit.Reactivity\BlazeKit.Reactivity.csproj", "{3CD31A61-9A86-4D79-A7A6-B6A4AD0BF7D8}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Reactivity", "src\BlazeKit.Reactivity\BlazeKit.Reactivity.csproj", "{3CD31A61-9A86-4D79-A7A6-B6A4AD0BF7D8}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazeKit.Routing.TestApp", "tests\BlazeKit.Routing.TestApp\BlazeKit.Routing.TestApp.csproj", "{196C36B5-D580-4C2D-BDFF-AFD530255F2E}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Routing.TestApp", "tests\BlazeKit.Routing.TestApp\BlazeKit.Routing.TestApp.csproj", "{196C36B5-D580-4C2D-BDFF-AFD530255F2E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{98E8084D-CE3D-429D-97B4-A7E41EDF1431}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazeKit.Static", "src\BlazeKit.Static\BlazeKit.Static.csproj", "{B28BBCD3-5A90-447E-8BAD-D78D7517AAD7}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Static", "src\BlazeKit.Static\BlazeKit.Static.csproj", "{B28BBCD3-5A90-447E-8BAD-D78D7517AAD7}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazeKit.Site.Static", "src\BlazeKit.Site.Static\BlazeKit.Site.Static.csproj", "{FB0F7358-8500-46DF-B213-3E8980F70856}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Website", "src\BlazeKit.Website\BlazeKit.Website.csproj", "{AB38EF74-D766-4A77-A7F2-EA8B74AEA495}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazeKit.Website", "src\BlazeKit.Website\BlazeKit.Website.csproj", "{AB38EF74-D766-4A77-A7F2-EA8B74AEA495}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Website.Islands", "src\BlazeKit.Website.Islands\BlazeKit.Website.Islands.csproj", "{1C224EA5-8DE8-46E4-A0BE-20AEDB8ACB1D}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazeKit.Website.Islands", "src\BlazeKit.Website.Islands\BlazeKit.Website.Islands.csproj", "{1C224EA5-8DE8-46E4-A0BE-20AEDB8ACB1D}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Web", "src\BlazeKit.Web\BlazeKit.Web.csproj", "{788D739C-CA07-4E74-9F13-F3DA1A49C3A5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazeKit.Hydration", "src\BlazeKit.Hydration\BlazeKit.Hydration.csproj", "{90DC8A52-7AD7-4C60-AEE8-5E1F11C1F4AD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -91,10 +93,6 @@ Global
{B28BBCD3-5A90-447E-8BAD-D78D7517AAD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B28BBCD3-5A90-447E-8BAD-D78D7517AAD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B28BBCD3-5A90-447E-8BAD-D78D7517AAD7}.Release|Any CPU.Build.0 = Release|Any CPU
- {FB0F7358-8500-46DF-B213-3E8980F70856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FB0F7358-8500-46DF-B213-3E8980F70856}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FB0F7358-8500-46DF-B213-3E8980F70856}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FB0F7358-8500-46DF-B213-3E8980F70856}.Release|Any CPU.Build.0 = Release|Any CPU
{AB38EF74-D766-4A77-A7F2-EA8B74AEA495}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AB38EF74-D766-4A77-A7F2-EA8B74AEA495}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB38EF74-D766-4A77-A7F2-EA8B74AEA495}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -103,6 +101,14 @@ Global
{1C224EA5-8DE8-46E4-A0BE-20AEDB8ACB1D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1C224EA5-8DE8-46E4-A0BE-20AEDB8ACB1D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1C224EA5-8DE8-46E4-A0BE-20AEDB8ACB1D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {788D739C-CA07-4E74-9F13-F3DA1A49C3A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {788D739C-CA07-4E74-9F13-F3DA1A49C3A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {788D739C-CA07-4E74-9F13-F3DA1A49C3A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {788D739C-CA07-4E74-9F13-F3DA1A49C3A5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {90DC8A52-7AD7-4C60-AEE8-5E1F11C1F4AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {90DC8A52-7AD7-4C60-AEE8-5E1F11C1F4AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {90DC8A52-7AD7-4C60-AEE8-5E1F11C1F4AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {90DC8A52-7AD7-4C60-AEE8-5E1F11C1F4AD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -113,9 +119,9 @@ Global
{4D98C97D-CFCC-4473-BFD8-B28DA3199EF6} = {952B525D-297C-4F66-85E2-967C9982771F}
{196C36B5-D580-4C2D-BDFF-AFD530255F2E} = {952B525D-297C-4F66-85E2-967C9982771F}
{B28BBCD3-5A90-447E-8BAD-D78D7517AAD7} = {98E8084D-CE3D-429D-97B4-A7E41EDF1431}
- {FB0F7358-8500-46DF-B213-3E8980F70856} = {98E8084D-CE3D-429D-97B4-A7E41EDF1431}
{AB38EF74-D766-4A77-A7F2-EA8B74AEA495} = {98E8084D-CE3D-429D-97B4-A7E41EDF1431}
{1C224EA5-8DE8-46E4-A0BE-20AEDB8ACB1D} = {98E8084D-CE3D-429D-97B4-A7E41EDF1431}
+ {788D739C-CA07-4E74-9F13-F3DA1A49C3A5} = {98E8084D-CE3D-429D-97B4-A7E41EDF1431}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8F0560BE-EE70-4DE3-8ACA-590A2B56C96A}
diff --git a/Directory.Build.props b/Directory.Build.props
index 3a4619f..b5d2c72 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,6 @@
- 0.2.0
+ 0.2.1Andreas KönigBlazeKit
diff --git a/frontmatter.json b/frontmatter.json
new file mode 100644
index 0000000..b4d0883
--- /dev/null
+++ b/frontmatter.json
@@ -0,0 +1,66 @@
+{
+ "$schema": "https://frontmatter.codes/frontmatter.schema.json",
+ "frontMatter.taxonomy.contentTypes": [
+ {
+ "name": "default",
+ "pageBundle": false,
+ "previewPath": null,
+ "fields": [
+ {
+ "title": "Title",
+ "name": "title",
+ "type": "string"
+ },
+ {
+ "title": "Description",
+ "name": "description",
+ "type": "string"
+ },
+ {
+ "title": "Publishing date",
+ "name": "date",
+ "type": "datetime",
+ "default": "{{now}}",
+ "isPublishDate": true
+ },
+ {
+ "title": "Content preview",
+ "name": "preview",
+ "type": "image"
+ },
+ {
+ "title": "Is in draft",
+ "name": "draft",
+ "type": "draft"
+ },
+ {
+ "title": "Tags",
+ "name": "tags",
+ "type": "tags"
+ },
+ {
+ "title": "Categories",
+ "name": "categories",
+ "type": "categories"
+ },
+ {
+ "title": "Author",
+ "name": "author",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "frontMatter.framework.id": "other",
+ "frontMatter.content.publicFolder": "",
+ "frontMatter.content.pageFolders": [
+ {
+ "title": "Blog",
+ "path": "[[workspace]]/src/BlazeKit.Website/Content/Blog"
+ },
+ {
+ "title": "News",
+ "path": "[[workspace]]/src/BlazeKit.Website/Content/News"
+ }
+ ]
+}
diff --git a/src/BlazeKit.Abstraction/Config/BkConfig.cs b/src/BlazeKit.Abstraction/Config/BkConfig.cs
index a28f306..b69eee8 100644
--- a/src/BlazeKit.Abstraction/Config/BkConfig.cs
+++ b/src/BlazeKit.Abstraction/Config/BkConfig.cs
@@ -13,6 +13,7 @@ public BkConfig()
public string Routes { get; set; }
public TailwindcssConfig Tailwindcss { get; set; }
+ public bool PreRender { get; set; } = false;
public static bool TryLoad(out BkConfig config)
{
diff --git a/src/BlazeKit.Abstraction/IServerLoad.cs b/src/BlazeKit.Abstraction/IServerLoad.cs
new file mode 100644
index 0000000..d000118
--- /dev/null
+++ b/src/BlazeKit.Abstraction/IServerLoad.cs
@@ -0,0 +1,10 @@
+using System.Threading.Tasks;
+
+namespace BlazeKit.Abstraction;
+
+public interface IServerLoad
+{
+ Task LoadAsync();
+
+ TResult Load();
+}
diff --git a/src/BlazeKit.CLI/AnsiConsoleExtensions.cs b/src/BlazeKit.CLI/AnsiConsoleExtensions.cs
new file mode 100644
index 0000000..7b68d7a
--- /dev/null
+++ b/src/BlazeKit.CLI/AnsiConsoleExtensions.cs
@@ -0,0 +1,12 @@
+using Spectre.Console;
+
+namespace BlazeKit.CLI;
+
+public static class AnsiConsoleExtensions
+{
+ [System.Diagnostics.Conditional("DEBUG")]
+ public static void Debug(this IAnsiConsole console, string message)
+ {
+ console.MarkupLine($"[bold yellow on blue] DEBUG: {message.EscapeMarkup()}[/]");
+ }
+}
diff --git a/src/BlazeKit.CLI/BlazeKit.CLI.csproj b/src/BlazeKit.CLI/BlazeKit.CLI.csproj
index ae87b14..f4da95b 100644
--- a/src/BlazeKit.CLI/BlazeKit.CLI.csproj
+++ b/src/BlazeKit.CLI/BlazeKit.CLI.csproj
@@ -6,6 +6,7 @@
enableenabletrue
+ 0.2.1bkit
@@ -39,4 +40,13 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/BlazeKit.CLI/Commands/Build/BuildCommand.cs b/src/BlazeKit.CLI/Commands/Build/BuildCommand.cs
index c116db4..f97f047 100644
--- a/src/BlazeKit.CLI/Commands/Build/BuildCommand.cs
+++ b/src/BlazeKit.CLI/Commands/Build/BuildCommand.cs
@@ -1,29 +1,32 @@
-using BlazeKit.CLI.Commands.New;
+using BlazeKit.Abstraction.Config;
using BlazeKit.CLI.Tasks.Utils;
+using Microsoft.AspNetCore.Components;
using Spectre.Console;
using Spectre.Console.Cli;
using System.Diagnostics;
-using System.Text.Json;
+using System.Runtime.Loader;
namespace BlazeKit.CLI.Commands.Build;
-public class BuildCommand : AsyncCommand
+public class BuildCommand : Command
{
- public override async Task ExecuteAsync(CommandContext context, RunSettings settings)
+ public override int Execute(CommandContext context, BuildSettings settings)
{
- // load blazekit.config.json file if it exists
- if (File.Exists("blazekit.config.json"))
- {
- AnsiConsole.MarkupLine("[yellow]Loading blazekit.config.json...[/]");
- var config = JsonSerializer.Deserialize(File.ReadAllText("blazekit.config.json"));
+ if (!BkConfig.TryLoad(out var bkConfig))
+ {
+ AnsiConsole.MarkupLine("[red]blazekit.config.json not found...Using default values[/]");
+ }
- config!.RootElement.TryGetProperty("tailwindcss", out var tailwindcss);
+ var hasTailwindcss = bkConfig.HasTailwindcss();
- var input = tailwindcss.GetProperty("input").GetString();
- var output = tailwindcss.GetProperty("output").GetString();
+ // load blazekit.config.json file if it exists
+ if (hasTailwindcss && string.IsNullOrEmpty(settings.Tailwindcss))
+ {
+ var input = bkConfig.Tailwindcss.Input;
+ var output = bkConfig.Tailwindcss.Output;
- settings.Tailwindcss = $"-i {input} -o {output}";
- }
+ settings.Tailwindcss = $"-i {input} -o {output} --minify";
+ }
// run dotnet watch command
AnsiConsole.MarkupLine("[yellow] Running Tailwindcss build process...[/]");
@@ -55,9 +58,16 @@ public override async Task ExecuteAsync(CommandContext context, RunSettings
}
});
- await Task.WhenAll(publishTask);
-
+ Task.WhenAll(publishTask).Wait();
+ // var tempOutput = bkConfig.PreRender ? ".blazekit/tmp/" : settings.Output;
+ var tempOutput = bkConfig.PreRender ? Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()) : settings.Output;
+ // clean the temp output directory
+ if (Directory.Exists(tempOutput))
+ {
+ AnsiConsole.MarkupLine($"[yellow]Cleaning {tempOutput}...[/]");
+ Directory.Delete(tempOutput, true);
+ }
publishTask =
Task.Run(async () =>
{
@@ -66,9 +76,11 @@ public override async Task ExecuteAsync(CommandContext context, RunSettings
"dotnet",
output =>
{
- AnsiConsole.MarkupLine($"[purple]{$"[DOTNET PUBLISH]".EscapeMarkup()} {output.EscapeMarkup()}[/]");
+ //AnsiConsole.MarkupLine($"[purple]{$"[DOTNET PUBLISH]".EscapeMarkup()}[/] {output.EscapeMarkup()}");
+ AnsiConsole.MarkupLine($"{output.EscapeMarkup()}");
},
- (string.IsNullOrEmpty(settings.Dotnet) ? "publish -c Release" : settings.Dotnet) + " -o ./blazekit/build"
+ info => info.EnvironmentVariables.Add("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "True"),
+ (string.IsNullOrEmpty(settings.Dotnet) ? "publish -c Debug" : settings.Dotnet) + $" -o {tempOutput}"
).Run();
while (!process.HasExited)
@@ -82,92 +94,72 @@ public override async Task ExecuteAsync(CommandContext context, RunSettings
await Task.Delay(500);
}
-
});
- // Wait for CTRL+C input to cancel running dotnet watch
- AnsiConsole.MarkupLine("[yellow]Press CTRL+C to stop dotnet watch...[/]");
- // Start a console read operation. Do not display the input.
- Console.TreatControlCAsInput = true;
- var cancelTask = Task.Run(async () =>
+ Task.WhenAll(publishTask).Wait();
+ // check if preprender is enabled
+ if(bkConfig.PreRender)
{
- while (true)
+ // pre-render the app (SSG output)
+ AnsiConsole.MarkupLine("[yellow] Pre-rendering app...[/]");
+ // get the name of the csproj file in the current directory
+ var csproj = new FileInfo(Directory.GetFiles(Directory.GetCurrentDirectory(), "*.csproj").FirstOrDefault());
+ // find the assembly name from the csproj file
+ var assemblyName = csproj.Name.Replace(csproj.Extension, "");
+ // get the path to the assembly
+ var assemblyPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), tempOutput, assemblyName + ".dll"));
+ // use BlazeKit.Static to pre-render the app
+ // var loadCtx = AssemblyLoadContext.Default;
+
+ System.Runtime.Loader.AssemblyLoadContext loadCtx = new AssemblyLoadContext("ssg",isCollectible:true);
+ loadCtx.Resolving += (ctx, name) =>
{
- var key = Console.ReadKey(true);
- if (key.Key == ConsoleKey.C && key.Modifiers == ConsoleModifiers.Control)
+ AnsiConsole.MarkupLine($"Resolving assembly '{name.Name}'");
+
+ var resolved = ctx.LoadFromAssemblyPath(Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), tempOutput, name.Name + ".dll")));
+ if(resolved != null)
{
- Console.WriteLine("CTRL+C pressed");
- cancel.Cancel();
- break;
+ AnsiConsole.MarkupLine($"Resolved assembly '{name.Name}'");
+ } else {
+ AnsiConsole.MarkupLine($"Failed to resolve assembly '{name.Name}'");
}
- await Task.Delay(100);
- }
- });
- await Task.WhenAll(publishTask);
- return 0;
- }
-
- private Process RunDotNetCommand(string command, IList output, IList error, Action refresh, params string[] arguments)
- {
- AnsiConsole.MarkupLine($"[yellow]{"[BlazeKit]".EscapeMarkup()} running 'dotnet {command} {string.Join(" ", arguments)}'[/]");
- // print working directory
- // AnsiConsole.MarkupLine($"[yellow]Working directory: {Directory.GetCurrentDirectory()}[/]");
- var startInfo =
- new ProcessStartInfo("dotnet", $"{command} {string.Join(" ", arguments)}")
- {
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- RedirectStandardInput = true,
- UseShellExecute = false,
- CreateNoWindow = true,
- // DOTNET_WATCH_SUPPRESS_EMOJIS=1
- WorkingDirectory = Directory.GetCurrentDirectory()
+ return resolved;
};
- startInfo.Environment.Add("DOTNET_WATCH_SUPPRESS_EMOJIS", "1");
- var process =
- Process.Start(
- startInfo
- );
- process.EnableRaisingEvents = true;
- process.OutputDataReceived += (sender, args) => { output.Add(args.Data); refresh(args.Data); };
- process.ErrorDataReceived += (sender, args) => { error.Add(args.Data); refresh(args.Data); };
-
- process.BeginOutputReadLine();
- process.BeginErrorReadLine();
-
- return process;
- }
+ AnsiConsole.MarkupLine($"Try load assembly '{assemblyPath}'");
+ var asm = loadCtx.LoadFromAssemblyPath(assemblyPath);
+ var ssg = new BlazeKit.Static.StaticSiteGenerator(settings.Output,Path.Combine(tempOutput,"wwwroot"),asm);
+ ssg.Build().Wait();
+ AnsiConsole.MarkupLine($"[green]Succesfully created static site at '{settings.Output}'[/]");
+
+ // loadCtx.Unload();
+ // delete the temp output directory
+ // TODO: throws Error: Access to the path 'BlazeKit.Abstraction.dll' is denied.
+ // Directory.Delete(tempOutput, true);
+ }
- private Process RunTailwindcss(string command, IList output, IList error, Action refresh, params string[] arguments)
- {
- AnsiConsole.MarkupLine($"[yellow]{"[BlazeKit]".EscapeMarkup()} running 'tailwindcss {command} {string.Join(" ", arguments)}'[/]");
- // print working directory
- // AnsiConsole.MarkupLine($"[yellow]Working directory: {Directory.GetCurrentDirectory()}[/]");
- var startInfo =
- new ProcessStartInfo("tailwindcss", $"{command} {string.Join(" ", arguments)}")
- {
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- RedirectStandardInput = true,
- UseShellExecute = false,
- CreateNoWindow = true,
- // DOTNET_WATCH_SUPPRESS_EMOJIS=1
- WorkingDirectory = Directory.GetCurrentDirectory()
- };
- var process =
- Process.Start(
- startInfo
- );
- process.EnableRaisingEvents = true;
- process.OutputDataReceived += (sender, args) => { output.Add(args.Data); refresh(args.Data); };
- process.ErrorDataReceived += (sender, args) => { error.Add(args.Data); refresh(args.Data); };
- process.BeginOutputReadLine();
- process.BeginErrorReadLine();
+ // // Wait for CTRL+C input to cancel running dotnet watch
+ // AnsiConsole.MarkupLine("[yellow]Press CTRL+C to stop dotnet watch...[/]");
+ // // Start a console read operation. Do not display the input.
+ // Console.TreatControlCAsInput = true;
+ // var cancelTask = Task.Run(async () =>
+ // {
+ // while (true)
+ // {
+ // var key = Console.ReadKey(true);
+ // if (key.Key == ConsoleKey.C && key.Modifiers == ConsoleModifiers.Control)
+ // {
+ // Console.WriteLine("CTRL+C pressed");
+ // cancel.Cancel();
+ // break;
+ // }
+ // await Task.Delay(100);
+ // }
+ // });
- return process;
+ return 0;
}
}
diff --git a/src/BlazeKit.CLI/Commands/Build/BuildSettings.cs b/src/BlazeKit.CLI/Commands/Build/BuildSettings.cs
new file mode 100644
index 0000000..6dbaf85
--- /dev/null
+++ b/src/BlazeKit.CLI/Commands/Build/BuildSettings.cs
@@ -0,0 +1,23 @@
+using Spectre.Console.Cli;
+using System.ComponentModel;
+
+namespace BlazeKit.CLI.Commands.Build
+{
+ public sealed class BuildSettings : Run.RunSettings
+ {
+ [CommandOption("-p|--project ")]
+ public string Project { get; set; }
+
+ [CommandOption("-t|--tailwindcss")]
+ [DefaultValue("")]
+ public string Tailwindcss { get; set; }
+
+ [CommandOption("-d|--dotnet")]
+ [DefaultValue("")]
+ public string Dotnet { get; set; }
+
+ [CommandOption("-o|--output")]
+ [DefaultValue(".blazekit/build")]
+ public string Output { get; set; }
+ }
+}
diff --git a/src/BlazeKit.CLI/Commands/Run/DevCommand.cs b/src/BlazeKit.CLI/Commands/Run/Dev/DevCommand.cs
similarity index 87%
rename from src/BlazeKit.CLI/Commands/Run/DevCommand.cs
rename to src/BlazeKit.CLI/Commands/Run/Dev/DevCommand.cs
index 20e41d9..fa0c20d 100644
--- a/src/BlazeKit.CLI/Commands/Run/DevCommand.cs
+++ b/src/BlazeKit.CLI/Commands/Run/Dev/DevCommand.cs
@@ -1,5 +1,5 @@
using BlazeKit.Abstraction.Config;
-using BlazeKit.CLI.Commands.New;
+using BlazeKit.CLI.Commands.Run;
using BlazeKit.CLI.Tasks;
using BlazeKit.CLI.Tasks.Tasks;
using BlazeKit.CLI.Tasks.Tools;
@@ -9,11 +9,11 @@
using System.Text;
using Yaapii.Atoms.List;
-namespace BlazeKit.CLI;
+namespace BlazeKit.CLI.Commands.Run;
-public class DevCommand : AsyncCommand
+public class DevCommand : AsyncCommand
{
- public override async Task ExecuteAsync(CommandContext context, RunSettings settings)
+ public override async Task ExecuteAsync(CommandContext context, DevSettings settings)
{
try
{
@@ -28,7 +28,7 @@ public override async Task ExecuteAsync(CommandContext context, RunSettings
}
var hasTailwindcss = bkConfig.HasTailwindcss();
-
+
// load blazekit.config.json file if it exists
if (hasTailwindcss && string.IsNullOrEmpty(settings.Tailwindcss))
{
@@ -54,18 +54,20 @@ public override async Task ExecuteAsync(CommandContext context, RunSettings
new TskDotNetWatch(
settings,
output => {
- AnsiConsole.MarkupLine($"[purple]{$"[DOTNET WATCH {Emoji.Known.MagnifyingGlassTiltedRight}]".EscapeMarkup()} {output.EscapeMarkup()}[/]");
+ //AnsiConsole.MarkupLine($"[purple]{$"[DOTNET WATCH {Emoji.Known.MagnifyingGlassTiltedRight}]".EscapeMarkup()} {output.EscapeMarkup()}[/]");
+ //AnsiConsole.MarkupLine($"{$"[DOTNET WATCH {Emoji.Known.MagnifyingGlassTiltedRight}]".EscapeMarkup()} {output.EscapeMarkup()}");
+ AnsiConsole.MarkupLine($"{output.EscapeMarkup()}");
},
cancel
),
new TskWhen(
() => hasTailwindcss,
new TskTailwindCss(
- settings,
output => {
AnsiConsole.MarkupLine($"[blue]{$"[TAILWINDCSS {Emoji.Known.ArtistPalette}]".EscapeMarkup()} {output.EscapeMarkup()}[/]");
},
- cancel
+ cancel,
+ string.IsNullOrEmpty(settings.Tailwindcss) ? "-i app.css -o wwwroot/css/app.css --watch" : settings.Tailwindcss
)
)
).Run();
@@ -80,7 +82,7 @@ public override async Task ExecuteAsync(CommandContext context, RunSettings
var key = Console.ReadKey(true);
if (key.Key == ConsoleKey.C && key.Modifiers == ConsoleModifiers.Control)
{
- Console.WriteLine("CTRL+C pressed");
+ AnsiConsole.WriteLine("CTRL+C pressed");
AnsiConsole.MarkupLine("[yellow]Stopping dev mode...[/]");
cancel.Cancel();
break;
@@ -117,7 +119,7 @@ private void AlternateOrClearScreen(Action action)
}
}
- private async void UseLayout(RunSettings settings, bool hasTailwindcss)
+ private async void UseLayout(DevSettings settings, bool hasTailwindcss)
{
var outputLayouts = hasTailwindcss ? ListOf.New(new Layout("Left"), new Layout("Right")) : ListOf.New(new Layout("Left"));
// Create the layout
@@ -162,7 +164,7 @@ await AnsiConsole.Live(layout).StartAsync(async ctx =>
.Update(
panel
);
-
+
ctx.Refresh();
Debug.WriteLine($"Refresh took {sw.ElapsedMilliseconds}ms");
},
@@ -171,7 +173,6 @@ await AnsiConsole.Live(layout).StartAsync(async ctx =>
new TskWhen(
() => hasTailwindcss,
new TskTailwindCss(
- settings,
output => {
tailwind.Add(output.EscapeMarkup());
@@ -192,7 +193,8 @@ await AnsiConsole.Live(layout).StartAsync(async ctx =>
ctx.Refresh();
//AnsiConsole.MarkupLine($"[blue]{$"[TAILWINDCSS {Emoji.Known.ArtistPalette}]".EscapeMarkup()} {output.EscapeMarkup()}[/]");
},
- cancel
+ cancel,
+ string.IsNullOrEmpty(settings.Tailwindcss) ? "-i app.css -o wwwroot/css/app.css --watch" : settings.Tailwindcss
)
)
).Run();
@@ -208,7 +210,7 @@ await AnsiConsole.Live(layout).StartAsync(async ctx =>
var key = Console.ReadKey(true);
if (key.Key == ConsoleKey.C && key.Modifiers == ConsoleModifiers.Control)
{
- Console.WriteLine("CTRL+C pressed");
+ AnsiConsole.WriteLine("CTRL+C pressed");
AnsiConsole.MarkupLine("[yellow]Stopping dev mode...[/]");
cancel.Cancel();
break;
diff --git a/src/BlazeKit.CLI/Commands/Run/Dev/DevSettings.cs b/src/BlazeKit.CLI/Commands/Run/Dev/DevSettings.cs
new file mode 100644
index 0000000..3ddf809
--- /dev/null
+++ b/src/BlazeKit.CLI/Commands/Run/Dev/DevSettings.cs
@@ -0,0 +1,24 @@
+using Spectre.Console.Cli;
+using System.ComponentModel;
+
+namespace BlazeKit.CLI.Commands.Run
+{
+ public sealed class DevSettings : RunSettings
+ {
+ [CommandOption("-p|--project ")]
+ public string Project { get; set; }
+
+ [CommandOption("-t|--tailwindcss")]
+ [DefaultValue("")]
+ public string Tailwindcss { get; set; }
+
+ [CommandOption("-d|--dotnet")]
+ [DefaultValue("")]
+ public string Dotnet { get; set; }
+
+
+ [CommandOption("-l|--layout-view")]
+ [DefaultValue(false)]
+ public bool UseLayoutView { get; set; }
+ }
+}
diff --git a/src/BlazeKit.CLI/Commands/Run/RunSettings.cs b/src/BlazeKit.CLI/Commands/Run/RunSettings.cs
index 1ade48f..b0975f0 100644
--- a/src/BlazeKit.CLI/Commands/Run/RunSettings.cs
+++ b/src/BlazeKit.CLI/Commands/Run/RunSettings.cs
@@ -1,24 +1,5 @@
using Spectre.Console.Cli;
-using System.ComponentModel;
+namespace BlazeKit.CLI.Commands.Run;
-namespace BlazeKit.CLI.Commands.New
-{
- public sealed class RunSettings : CommandSettings
- {
- [CommandOption("-p|--project ")]
- public string Project { get; set; }
-
- [CommandOption("-t|--tailwindcss")]
- [DefaultValue("")]
- public string Tailwindcss { get; set; }
-
- [CommandOption("-d|--dotnet")]
- [DefaultValue("")]
- public string Dotnet { get; set; }
-
-
- [CommandOption("-l|--layout-view")]
- [DefaultValue(false)]
- public bool UseLayoutView { get; set; }
- }
-}
+public abstract class RunSettings : CommandSettings
+{ }
diff --git a/src/BlazeKit.CLI/Commands/Run/Tailwind/TailwindCommand.cs b/src/BlazeKit.CLI/Commands/Run/Tailwind/TailwindCommand.cs
new file mode 100644
index 0000000..d7e7eec
--- /dev/null
+++ b/src/BlazeKit.CLI/Commands/Run/Tailwind/TailwindCommand.cs
@@ -0,0 +1,90 @@
+using BlazeKit.Abstraction.Config;
+using BlazeKit.CLI.Commands.Run;
+using BlazeKit.CLI.Tasks;
+using BlazeKit.CLI.Tasks.Tasks;
+using BlazeKit.CLI.Tasks.Tools;
+using Spectre.Console;
+using Spectre.Console.Cli;
+using System.Diagnostics;
+using System.Text;
+using Yaapii.Atoms.List;
+
+namespace BlazeKit.CLI;
+
+public class TailwindCommand : AsyncCommand
+{
+ public override async Task ExecuteAsync(CommandContext context, TailwindSettings settings)
+ {
+ try
+ {
+
+ var content = new StringBuilder();
+ var tailwindContent = new StringBuilder();
+
+
+ if (!BkConfig.TryLoad(out var bkConfig))
+ {
+ AnsiConsole.MarkupLine("[red]blazekit.config.json not found...Using default values[/]");
+ }
+
+ var hasTailwindcss = bkConfig.HasTailwindcss();
+ var twSettings = "-i app.css -o wwwroot/css/app.css --watch";
+ // load blazekit.config.json file if it exists
+ if (hasTailwindcss)
+ {
+ var input = bkConfig.Tailwindcss.Input;
+ var output = bkConfig.Tailwindcss.Output;
+
+ twSettings = $"-i {input} -o {output} --watch";
+ }
+
+ // run tailwind cli command
+ AnsiConsole.MarkupLine("[yellow]Starting Tailwind ...[/]");
+ var prefix = $"[Tailwind {Emoji.Known.ArtistPalette}]";
+ var cancel = new CancellationTokenSource();
+ var tailwind = new List();
+ var devTask =
+ new TskChain(
+ new TskWhen(
+ () => hasTailwindcss,
+ new TskTailwindCss(
+ output => {
+ AnsiConsole.MarkupLine($"{prefix} {output}".EscapeMarkup());
+ },
+ cancel,
+ twSettings
+ )
+ )
+ ).Run();
+
+ // Wait for CTRL+C input to cancel running dotnet watch
+ // Start a console read operation. Do not display the input.
+ Console.TreatControlCAsInput = true;
+ var cancelTask = Task.Run(async () =>
+ {
+ while (true)
+ {
+ var key = Console.ReadKey(true);
+ if (key.Key == ConsoleKey.C && key.Modifiers == ConsoleModifiers.Control)
+ {
+ AnsiConsole.WriteLine("CTRL+C pressed");
+ AnsiConsole.MarkupLine("[yellow]Stopping Tailwind ...[/]");
+ cancel.Cancel();
+ break;
+ }
+ await Task.Delay(100);
+ }
+ });
+ Task.WaitAll(devTask);
+
+ AnsiConsole.MarkupLine("[yellow]Stopped Tailwind...[/]");
+
+
+ }
+ catch (Exception ex)
+ {
+ AnsiConsole.WriteException(ex);
+ }
+ return 0;
+ }
+}
diff --git a/src/BlazeKit.CLI/Commands/Run/Tailwind/TailwindSettings.cs b/src/BlazeKit.CLI/Commands/Run/Tailwind/TailwindSettings.cs
new file mode 100644
index 0000000..061a3b4
--- /dev/null
+++ b/src/BlazeKit.CLI/Commands/Run/Tailwind/TailwindSettings.cs
@@ -0,0 +1,11 @@
+using Spectre.Console.Cli;
+using System.ComponentModel;
+
+namespace BlazeKit.CLI.Commands.Run
+{
+ public sealed class TailwindSettings : RunSettings
+ {
+ // [CommandOption("-p|--project ")]
+ // public string Project { get; set; }
+ }
+}
diff --git a/src/BlazeKit.CLI/IProcess.cs b/src/BlazeKit.CLI/IProcess.cs
index ccb2940..703943e 100644
--- a/src/BlazeKit.CLI/IProcess.cs
+++ b/src/BlazeKit.CLI/IProcess.cs
@@ -4,5 +4,6 @@ namespace BlazeKit;
public interface IProcess
{
+ void Input(T cmd);
Process Run();
}
diff --git a/src/BlazeKit.CLI/Program.cs b/src/BlazeKit.CLI/Program.cs
index c41b675..41ee58f 100644
--- a/src/BlazeKit.CLI/Program.cs
+++ b/src/BlazeKit.CLI/Program.cs
@@ -3,11 +3,13 @@
using BlazeKit.CLI.Commands.Add;
using BlazeKit.CLI.Commands.Build;
using BlazeKit.CLI.Commands.New;
+using BlazeKit.CLI.Commands.Run;
using Spectre.Console;
using Spectre.Console.Cli;
+using System.Diagnostics;
Console.OutputEncoding = System.Text.Encoding.UTF8;
-
+Debugger.Launch();
var app = new Spectre.Console.Cli.CommandApp();
app.Configure(config =>
@@ -16,8 +18,9 @@
config.AddCommand("new");
config.AddBranch("run", c => {
c.AddCommand("dev");
- c.AddCommand("build");
+ c.AddCommand("tailwind");
});
+ config.AddCommand("build");
config.AddBranch("add", c => {
c.AddCommand("tailwindcss");
});
diff --git a/src/BlazeKit.CLI/Tasks/Tools/TskDotNetWatch.cs b/src/BlazeKit.CLI/Tasks/Tools/TskDotNetWatch.cs
index cb67a7f..4c9b760 100644
--- a/src/BlazeKit.CLI/Tasks/Tools/TskDotNetWatch.cs
+++ b/src/BlazeKit.CLI/Tasks/Tools/TskDotNetWatch.cs
@@ -1,19 +1,28 @@
using BlazeKit.CLI.Commands.New;
using BlazeKit.CLI.Tasks.Utils;
-
+using Spectre.Console;
+using BlazeKit.CLI;
+using BlazeKit.CLI.Commands.Run;
+using System.Diagnostics;
namespace BlazeKit.CLI.Tasks.Tools
{
internal class TskDotNetWatch : TskEnveleope
{
- public TskDotNetWatch(RunSettings settings,Action output, CancellationTokenSource cancel) : base(() =>
+ public TskDotNetWatch(DevSettings settings,Action output, CancellationTokenSource cancel) : base(() =>
{
+ var watchProcess =
+ new ExecCliCommand(
+ "dotnet",
+ (msg) => output(msg),
+ info => info.EnvironmentVariables.Add("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "True"),
+ "watch --non-interactive", settings.Dotnet ?? ""
+ );
+
+
+
return
new CancableProcess(
- new ExecCliCommand(
- "dotnet", (msg) => output(msg),
- // "watch", settings.Dotnet ?? "--non-interactive"
- "watch", settings.Dotnet ?? ""
- ),
+ watchProcess,
cancel.Token
).Invoke();
})
diff --git a/src/BlazeKit.CLI/Tasks/Tools/TskTailwindCss.cs b/src/BlazeKit.CLI/Tasks/Tools/TskTailwindCss.cs
index e12092d..1dd9a70 100644
--- a/src/BlazeKit.CLI/Tasks/Tools/TskTailwindCss.cs
+++ b/src/BlazeKit.CLI/Tasks/Tools/TskTailwindCss.cs
@@ -5,13 +5,13 @@ namespace BlazeKit.CLI.Tasks.Tasks
{
internal class TskTailwindCss : TskEnveleope
{
- public TskTailwindCss(RunSettings settings, Action output, CancellationTokenSource cancel) : base(() =>
+ public TskTailwindCss(Action output, CancellationTokenSource cancel, params string[] arguments) : base(() =>
{
return
new CancableProcess(
new ExecCliCommand(
"tailwindcss", (msg) => output(msg),
- string.IsNullOrEmpty(settings.Tailwindcss) ? "-i app.css -o wwwroot/css/app.css --watch" : settings.Tailwindcss
+ arguments
),
cancel.Token
).Invoke();
diff --git a/src/BlazeKit.CLI/Tasks/Utils/ExecCliCommand.cs b/src/BlazeKit.CLI/Tasks/Utils/ExecCliCommand.cs
index 118f6c1..12cc8dd 100644
--- a/src/BlazeKit.CLI/Tasks/Utils/ExecCliCommand.cs
+++ b/src/BlazeKit.CLI/Tasks/Utils/ExecCliCommand.cs
@@ -1,4 +1,5 @@
using System.Diagnostics;
+using Spectre.Console;
using Yaapii.Atoms;
using Yaapii.Atoms.Scalar;
@@ -20,19 +21,21 @@ public ExecCliCommand(string command, Action output, Action
{
+ var args = string.Join(" ", arguments);
+ AnsiConsole.Console.Debug($"Execute {command} {args}");
var process =
new Process
{
StartInfo =
{
FileName = command,
- Arguments = string.Join(" ", arguments),
+ Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
UseShellExecute = false,
CreateNoWindow = true,
- WorkingDirectory = Directory.GetCurrentDirectory()
+ WorkingDirectory = Directory.GetCurrentDirectory(),
},
EnableRaisingEvents = true
};
@@ -55,11 +58,19 @@ public ExecCliCommand(string command, Action output, Action(T cmd)
+ {
+ AnsiConsole.Console.Debug($"Call Input with Command '{cmd}'");
+ process.Value(). StandardInput.WriteLine(cmd);
+ }
+
public Process Run()
{
return process.Value();
diff --git a/src/BlazeKit.Core/Routing/Navigating.cs b/src/BlazeKit.Core/Routing/Navigating.cs
deleted file mode 100644
index 4e3c440..0000000
--- a/src/BlazeKit.Core/Routing/Navigating.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-// using BlazeKit.Reactivity.Signals;
-// using Microsoft.AspNetCore.Components;
-// using Microsoft.JSInterop;
-
-// namespace BlazeKit.Core.Routing
-// {
-// public class Navigating : SignalEnvelope
-// {
-// private readonly IDictionary scrollPositions;
-// private readonly NavigationManager router;
-// private readonly IJSRuntime jsRuntime;
-
-// public Navigating() : base(true)
-// { }
-// public Navigating(NavigationManager router, IJSRuntime jsRuntime) : base(false)
-// {
-// scrollPositions = new Dictionary();
-// if(router != null)
-// {
-// router.RegisterLocationChangingHandler(async ctx =>
-// {
-// // strore scroll position
-// var scrollPosition = await jsRuntime.InvokeAsync("getScrollPosition");
-// //Console.WriteLine($"Storing scroll position for {router.Uri}");
-// scrollPositions[router.Uri] = scrollPosition;
-// this.Value = true;
-// });
-
-// router.LocationChanged += (sender, args) =>
-// {
-// //// check if we have a scroll position for this route
-// //if (scrollPositions.ContainsKey(args.Location))
-// //{
-// // // restore scroll position
-// // var scrollPosition = scrollPositions[args.Location];
-// // jsRuntime.InvokeVoidAsync("setScrollPosition", scrollPosition.X, scrollPosition.Y);
-// //}
-// //Console.WriteLine(args.Location);
-// this.Value = false;
-// };
-// }
-
-// this.router = router;
-// this.jsRuntime = jsRuntime;
-// }
-
-// public void ApplyScrollPosition()
-// {
-// //Console.WriteLine($"Applying scroll position for {router.Uri}");
-// // check if we have a scroll position for this route
-// if (scrollPositions.ContainsKey(router.Uri))
-// {
-// // restore scroll position
-// var scrollPosition = scrollPositions[router.Uri];
-// jsRuntime.InvokeVoidAsync("setScrollPosition", scrollPosition.X, scrollPosition.Y);
-// }
-// }
-
-// record ScrollPosition(float X, float Y);
-// }
-// }
diff --git a/src/BlazeKit.Hydration/BKitData.razor b/src/BlazeKit.Hydration/BKitData.razor
new file mode 100644
index 0000000..6256269
--- /dev/null
+++ b/src/BlazeKit.Hydration/BKitData.razor
@@ -0,0 +1,18 @@
+@code {
+ [Inject]
+ private DataHydrationContext HydrationContext { get; set; }
+
+ protected override void OnInitialized()
+ {
+ // set update callback to render the component when data has been added to the data hydration context
+ HydrationContext.OnUpdate(() =>
+ {
+ Console.WriteLine("Data has changed -> rerender");
+ InvokeAsync(this.StateHasChanged);
+ });
+ }
+}
+@if(!HydrationContext.IsEmpty())
+{
+ @(new MarkupString($""))
+}
diff --git a/src/BlazeKit.Hydration/BlazeKit.Hydration.csproj b/src/BlazeKit.Hydration/BlazeKit.Hydration.csproj
index fa71b7a..8181b85 100644
--- a/src/BlazeKit.Hydration/BlazeKit.Hydration.csproj
+++ b/src/BlazeKit.Hydration/BlazeKit.Hydration.csproj
@@ -1,9 +1,13 @@
-
+net8.0enableenable
+
+
+
+
diff --git a/src/BlazeKit.Hydration/DataHydrationContext.cs b/src/BlazeKit.Hydration/DataHydrationContext.cs
index c9d813c..6d1cbb0 100644
--- a/src/BlazeKit.Hydration/DataHydrationContext.cs
+++ b/src/BlazeKit.Hydration/DataHydrationContext.cs
@@ -4,21 +4,29 @@ namespace BlazeKit.Hydration;
public class DataHydrationContext
{
private bool initalized = false;
- public DataHydrationContext(Func> loadData = null)
+ private Action? onUpdate = null;
+ private Dictionary hydrationData;
+ private readonly Func>? loadData;
+
+
+ public DataHydrationContext(Func>? loadData = null)
{
- _hydrationData = new Dictionary();
+ hydrationData = new Dictionary();
this.loadData = loadData;
}
- private Dictionary _hydrationData;
- private readonly Func> loadData;
public void Add(string key, object value)
{
- if(_hydrationData.ContainsKey(key)) {
- _hydrationData[key] = value;
+ if(hydrationData.ContainsKey(key)) {
+ hydrationData[key] = value;
} else {
- _hydrationData.Add(key, value);
+ hydrationData.Add(key, value);
+ }
+
+ if(this.onUpdate != null)
+ {
+ this.onUpdate();
}
}
@@ -29,9 +37,19 @@ public async Task GetAsync(string key, T defaultValue)
await LoadData();
}
- if(_hydrationData.TryGetValue(key, out var tmpValue)) {
- var deserialzed = JsonSerializer.Deserialize(((JsonElement)tmpValue).GetRawText());
- result = deserialzed;
+ if(OperatingSystem.IsBrowser())
+ {
+ if (hydrationData.TryGetValue(key, out var tmpValue))
+ {
+ var deserialzed = JsonSerializer.Deserialize(((JsonElement)tmpValue).GetRawText(),new JsonSerializerOptions() { IncludeFields = true});
+ result = deserialzed;
+ }
+ } else
+ {
+ if(hydrationData.TryGetValue(key, out var tmpValue)) {
+ //var deserialzed = JsonSerializer.Deserialize(((JsonElement)tmpValue).GetRawText(),new JsonSerializerOptions() { IncludeFields = true});
+ result = (T)tmpValue;
+ }
}
return result;
}
@@ -42,7 +60,7 @@ public bool TryGet(string key, out T value)
LoadData();
}
value = default;
- if(_hydrationData.TryGetValue(key, out var tmpValue)) {
+ if(hydrationData.TryGetValue(key, out var tmpValue)) {
// check if tmpValue is of type T
if(tmpValue is T) {
value = (T)tmpValue;
@@ -57,28 +75,30 @@ public bool TryGet(string key, out T value)
}
public string Serialized() {
- return System.Text.Json.JsonSerializer.Serialize(_hydrationData);
+ return System.Text.Json.JsonSerializer.Serialize(hydrationData);
+ }
+
+
+ internal void OnUpdate(Action onUpdate)
+ {
+ this.onUpdate = onUpdate;
+ }
+
+ internal bool IsEmpty()
+ {
+ return this.hydrationData.Keys.Count == 0;
}
private async Task LoadData() {
// check if initalized if not hydrate data from dom
- Console.WriteLine("Loading page data");
var data = System.Text.Json.JsonSerializer.Deserialize>(await loadData());
foreach(var item in data) {
- if(_hydrationData.ContainsKey(item.Key)) {
- _hydrationData[item.Key] = item.Value;
+ if(hydrationData.ContainsKey(item.Key)) {
+ hydrationData[item.Key] = item.Value;
} else {
- _hydrationData.Add(item.Key, item.Value);
+ hydrationData.Add(item.Key, item.Value);
}
}
- // if(!initalized) {
- // var data = System.Text.Json.JsonSerializer.Deserialize>(await loadData());
- // foreach(var item in data) {
- // _hydrationData.Add(item.Key, item.Value);
- // }
- // initalized = true;
- // }
-
return true;
}
-}
\ No newline at end of file
+}
diff --git a/src/BlazeKit.Reactivity/BlazeKit.Reactivity.csproj b/src/BlazeKit.Reactivity/BlazeKit.Reactivity.csproj
index 591994c..35b094f 100644
--- a/src/BlazeKit.Reactivity/BlazeKit.Reactivity.csproj
+++ b/src/BlazeKit.Reactivity/BlazeKit.Reactivity.csproj
@@ -13,19 +13,23 @@
-
+
-
+
+
+
+
+
diff --git a/src/BlazeKit.Static/BlazeKit.Static.csproj b/src/BlazeKit.Static/BlazeKit.Static.csproj
index 6bcba89..21f0444 100644
--- a/src/BlazeKit.Static/BlazeKit.Static.csproj
+++ b/src/BlazeKit.Static/BlazeKit.Static.csproj
@@ -1,4 +1,4 @@
-
+net8.0
@@ -8,8 +8,11 @@
+
+
+
diff --git a/src/BlazeKit.Static/BlazorRenderer.cs b/src/BlazeKit.Static/BlazorRenderer.cs
index cf5f66d..91e23e1 100644
--- a/src/BlazeKit.Static/BlazorRenderer.cs
+++ b/src/BlazeKit.Static/BlazorRenderer.cs
@@ -2,8 +2,10 @@
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.HtmlRendering;
+using Microsoft.AspNetCore.Html;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using System.Text.Encodings.Web;
namespace BlazeKit.Static;
#pragma warning disable BL0006 // Do not use RenderTree types
@@ -12,7 +14,7 @@ public sealed class BlazorRenderer : Renderer
{
private readonly Lazy htmlRenderer;
- public BlazorRenderer(ServiceProvider serviceProvider) : this(
+ public BlazorRenderer(IServiceProvider serviceProvider) : this(
() =>
new HtmlRenderer(
serviceProvider,
@@ -22,13 +24,13 @@ public BlazorRenderer(ServiceProvider serviceProvider) : this(
)
{ }
- public BlazorRenderer(HtmlRenderer htmlRenderer,ServiceProvider serviceProvider) :this(
+ public BlazorRenderer(HtmlRenderer htmlRenderer,IServiceProvider serviceProvider) :this(
() => htmlRenderer,
serviceProvider
)
{ }
- public BlazorRenderer(Func htmlRenderer,ServiceProvider serviceProvider) : base(serviceProvider, serviceProvider.GetRequiredService())
+ public BlazorRenderer(Func htmlRenderer, IServiceProvider serviceProvider) : base(serviceProvider, serviceProvider.GetRequiredService())
{
this.htmlRenderer = new Lazy(htmlRenderer);
}
@@ -84,21 +86,90 @@ private Task RenderComponent(ParameterView parameters) where T : ICom
return htmlRenderer.Value.Dispatcher.InvokeAsync(async () =>
{
HtmlRootComponent output = await htmlRenderer.Value.RenderComponentAsync(parameters);
+ await output.QuiescenceTask;
return output.ToHtmlString();
});
}
+ //private Task RenderComponent(Type componentType, ParameterView parameters)
+ //{
+ // // Use the default dispatcher to invoke actions in the context of the
+ // // static HTML renderer and return as a string
+ // return htmlRenderer.Value.Dispatcher.InvokeAsync(async () =>
+ // {
+ // HtmlRootComponent output = await htmlRenderer.Value.RenderComponentAsync(componentType, parameters);
+ // await output.QuiescenceTask;
+
+ // return output.ToHtmlString();
+ // });
+ //}
+
private Task RenderComponent(Type componentType, ParameterView parameters)
{
// Use the default dispatcher to invoke actions in the context of the
// static HTML renderer and return as a string
return htmlRenderer.Value.Dispatcher.InvokeAsync(async () =>
{
- HtmlRootComponent output = await htmlRenderer.Value.RenderComponentAsync(componentType, parameters);
+ var output = this.htmlRenderer.Value.BeginRenderingComponent(componentType, parameters);
+ await output.QuiescenceTask;
return output.ToHtmlString();
});
}
+ ///
+ /// HTML content which can be written asynchronously to a TextWriter.
+ ///
+ public interface IHtmlAsyncContent : IHtmlContent
+ {
+ ///
+ /// Writes the content to the specified .
+ ///
+ /// The to which the content is written.
+ ValueTask WriteToAsync(TextWriter writer);
+ }
+
+ // An implementation of IHtmlContent that holds a reference to a component until we're ready to emit it as HTML to the response.
+ // We don't construct the actual HTML until we receive the call to WriteTo.
+ public class PrerenderedComponentHtmlContent : IHtmlAsyncContent
+ {
+ private readonly Dispatcher? _dispatcher;
+ private readonly HtmlRootComponent? _htmlToEmitOrNull;
+
+ public static PrerenderedComponentHtmlContent Empty { get; }
+ = new PrerenderedComponentHtmlContent(null, default);
+
+ public PrerenderedComponentHtmlContent(
+ Dispatcher? dispatcher, // If null, we're only emitting the markers
+ HtmlRootComponent? htmlToEmitOrNull)
+ {
+ _dispatcher = dispatcher;
+ _htmlToEmitOrNull = htmlToEmitOrNull;
+ }
+
+ public async ValueTask WriteToAsync(TextWriter writer)
+ {
+ if (_dispatcher is null)
+ {
+ WriteTo(writer, HtmlEncoder.Default);
+ }
+ else
+ {
+ await _dispatcher.InvokeAsync(() => WriteTo(writer, HtmlEncoder.Default));
+ }
+ }
+
+ public void WriteTo(TextWriter writer, HtmlEncoder encoder)
+ {
+ if (_htmlToEmitOrNull is { } htmlToEmit)
+ {
+ htmlToEmit.WriteHtmlTo(writer);
+ }
+ }
+
+ public Task QuiescenceTask =>
+ _htmlToEmitOrNull.HasValue ? _htmlToEmitOrNull.Value.QuiescenceTask : Task.CompletedTask;
+ }
+
}
#pragma warning restore BL0006 // Do not use RenderTree types
diff --git a/src/BlazeKit.Static/ContentCollections/ContentCollectionEnvelope.cs b/src/BlazeKit.Static/ContentCollections/ContentCollectionEnvelope.cs
new file mode 100644
index 0000000..6f137e3
--- /dev/null
+++ b/src/BlazeKit.Static/ContentCollections/ContentCollectionEnvelope.cs
@@ -0,0 +1,64 @@
+using System.Linq.Expressions;
+using BlazeKit.Static.Utils;
+namespace BlazeKit.Static.ContentCollections;
+
+///
+/// A collection of content items
+///
+public abstract class ContentCollectionEnvelope : IContentCollection
+{
+ private readonly string name;
+ private readonly Lazy> list;
+
+ ///
+ /// A collection of content items
+ ///
+ public ContentCollectionEnvelope(string collectionName, Func cast) : this(
+ collectionName,
+ Path.Combine("content", collectionName),
+ cast,
+ schema => true
+ )
+ { }
+
+ ///
+ /// A collection of content items
+ ///
+ public ContentCollectionEnvelope(string collectionName, Func cast, Func filter) : this(
+ collectionName,
+ Path.Combine("content", collectionName),
+ cast,
+ filter
+ )
+ { }
+
+ ///
+ /// A collection of content items
+ ///
+ public ContentCollectionEnvelope(string collectionName, string contentDirectory, Func cast, Func filter)
+ {
+ this.name = collectionName;
+ this.list = new Lazy>(() => {
+
+ // read all files from the content directory
+ // and parse them into BlogSchema objects
+ // use Markdig and Frontmatter extension
+ var markdownFiles = Directory.GetFiles(contentDirectory, "*.md");
+ var items = markdownFiles
+ .Select(File.ReadAllText)
+ .Select(md => {
+ var schema = cast(md);
+ schema.Content = md.Html();
+ return schema;
+ })
+ .Where(filter)
+ .ToList();
+ return items.Cast().ToList();
+ });
+ }
+ public string Name => name;
+
+ public IEnumerable Items => this.list.Value;
+
+ public abstract string Route(ISchema item);
+}
diff --git a/src/BlazeKit.Static/ContentCollections/IContentCollection.cs b/src/BlazeKit.Static/ContentCollections/IContentCollection.cs
new file mode 100644
index 0000000..b9a12f5
--- /dev/null
+++ b/src/BlazeKit.Static/ContentCollections/IContentCollection.cs
@@ -0,0 +1,9 @@
+namespace BlazeKit.Static.ContentCollections;
+
+public interface IContentCollection
+{
+ string Name { get;}
+ IEnumerable Items { get; }
+
+ string Route(ISchema item);
+}
diff --git a/src/BlazeKit.Static/ContentCollections/ISchema.cs b/src/BlazeKit.Static/ContentCollections/ISchema.cs
new file mode 100644
index 0000000..3eeffa2
--- /dev/null
+++ b/src/BlazeKit.Static/ContentCollections/ISchema.cs
@@ -0,0 +1,5 @@
+namespace BlazeKit.Static.ContentCollections;
+
+public interface ISchema {
+ string Content { get; set; }
+}
diff --git a/src/BlazeKit.Static/IStaticServiceCollection.cs b/src/BlazeKit.Static/IStaticServiceCollection.cs
new file mode 100644
index 0000000..9fb82e9
--- /dev/null
+++ b/src/BlazeKit.Static/IStaticServiceCollection.cs
@@ -0,0 +1,14 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace BlazeKit.Static;
+
+///
+/// A static service collection that is used to build a static site
+///
+public interface IStaticServiceCollection
+{
+ ///
+ /// A static service collection that is used to build a static site
+ ///
+ IServiceCollection Services();
+}
diff --git a/src/BlazeKit.Static/StaticSiteGenerator.cs b/src/BlazeKit.Static/StaticSiteGenerator.cs
index f2f0504..30388e4 100644
--- a/src/BlazeKit.Static/StaticSiteGenerator.cs
+++ b/src/BlazeKit.Static/StaticSiteGenerator.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using BlazeKit.Hydration;
+using BlazeKit.Static.ContentCollections;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
@@ -56,61 +57,176 @@ public StaticSiteGenerator(string outputDirectory, string wwwrootContent, Func>(routes);
}
- public void Build()
+ public Task Build()
{
var rootComponent = this.assembly.GetExportedTypes().FirstOrDefault(t => t.Name == "Index");
if(rootComponent == null)
{
- throw new Exception($"No root component found name 'Index' in assembly '{assembly.Location}' to build static site.");
+ throw new Exception($"No root component found with name 'Index' in assembly '{assembly.Location}' to build static site.");
}
- Build(rootComponent);
+ IServiceCollection serviceCollection = new ServiceCollection();
+ // find a type which implements the BlazeKit.Static.IStaticServiceCollection interface from the assembly
+ var staticServiceCollectionType = this.assembly.GetExportedTypes().FirstOrDefault(t => t.GetInterfaces().Contains(typeof(IStaticServiceCollection)));
+ if(staticServiceCollectionType != null)
+ {
+ // create an instance of the type
+ Console.WriteLine($"Using static service collection: {staticServiceCollectionType.FullName}");
+ var staticServiceCollection = Activator.CreateInstance(staticServiceCollectionType) as IStaticServiceCollection;
+ if(staticServiceCollection == null)
+ {
+ throw new Exception($"Failed to create instance of type '{staticServiceCollectionType.FullName}' from assembly '{this.assembly.Location}'");
+ } else {
+ serviceCollection = staticServiceCollection.Services();
+ }
+ }
+ return Build(rootComponent, serviceCollection);
}
- public async void Build(Type rootComponent)
+ public Task Build(Type rootComponent, IServiceCollection serviceCollection)
{
- // copy wwwroot
- var source = Path.Combine(this.wwwrootContent);
- var destination = Path.Combine(this.outputDirectory);
- Console.WriteLine($"Copying wwwroot from {source} to {destination}");
- CopyDirectory(source, destination);
- var serviceCollection = new ServiceCollection();
-
- var routeManager = new StaticNavigationManager();
- serviceCollection.AddLogging();
- serviceCollection.AddSingleton(new BKitHostEnvironment("Production"));
- serviceCollection.AddSingleton(routeManager);
- serviceCollection.AddSingleton(new FkJsRuntime());
- serviceCollection.AddSingleton(new FkNavigationInterception());
- serviceCollection.AddSingleton(new FkScrollToLocationHash());
- serviceCollection.AddSingleton(new StaticErrorBoundaryLogger());
- serviceCollection.AddSingleton(new DataHydrationContext());
- foreach(var route in this.routes.Value)
+ return Task.Run(async () =>
{
- try
+ // copy wwwroot
+ var source = Path.Combine(this.wwwrootContent);
+ var destination = Path.Combine(this.outputDirectory);
+ Console.WriteLine($"Copying wwwroot from {source} to {destination}");
+ CopyDirectory(source, destination);
+ // var serviceCollection = new ServiceCollection();
+
+ var routeManager = new StaticNavigationManager();
+ serviceCollection.AddLogging();
+ serviceCollection.AddSingleton(new BKitHostEnvironment("Production"));
+ serviceCollection.AddSingleton(routeManager);
+ serviceCollection.AddSingleton(new FkJsRuntime());
+ serviceCollection.AddSingleton(new FkNavigationInterception());
+ serviceCollection.AddSingleton(new FkScrollToLocationHash());
+ serviceCollection.AddSingleton(new StaticErrorBoundaryLogger());
+ //serviceCollection.AddScoped();
+ foreach (var route in this.routes.Value)
{
- Console.WriteLine($"Building route: {route}");
- routeManager.NavigateTo(route, forceLoad: true);
- var html =
- await
+ try
+ {
+ Console.WriteLine($"Building route: {route}");
+
+ // check if route has dynamic parameters
+ if(route.Contains("{"))
+ {
+ Console.WriteLine($"Route has dynamic parameters: {route}");
+ // get the route template
+ var routeTemplate = route.Split('/')[0];
+ // get all types which implememnt IContentCollection
+ var t = typeof(ISchema);
+ var contentCollectionTypes =
+ this.assembly.GetExportedTypes()
+ .Where(t => t.GetInterfaces().Contains(typeof(IContentCollection)));
+
+ // create instaces of the types
+ var contentCollections = contentCollectionTypes.Select(t => Activator.CreateInstance(t) as IContentCollection);
+ // find the content collection which matches the route template
+ var contentCollection = contentCollections.FirstOrDefault(c => c.Name.Equals(routeTemplate, StringComparison.InvariantCultureIgnoreCase));
+ // render each item of the content colltection
+ foreach(var item in contentCollection.Items)
+ {
+ var routeWithParams = contentCollection.Route(item);
+ Console.WriteLine($"Building route: {routeWithParams}");
+ Prerender(routeWithParams, routeManager, rootComponent, serviceCollection);
+ }
+
+ continue;
+ }
+ var spv = serviceCollection.BuildServiceProvider();
+ var scoped = spv.CreateScope();
+ var serviceProvider = scoped.ServiceProvider;
+
+ // get the type which from this.assembly which has a route parameter which matches the route
+ var shouldPreRender = ShouldBePreRender(route);
+ if(!shouldPreRender)
+ {
+ Console.WriteLine($"Skipping '{route}' for pre-render due to 'prerender = {false}'");
+ continue;
+ }
+
+ //var route2 = "/loadtest";
+ routeManager.NavigateTo(route, forceLoad: true);
+ //var html =
+ // await
+ // new BlazorRenderer(
+ // new HtmlRenderer(
+ // serviceCollection.BuildServiceProvider(),
+ // serviceCollection.BuildServiceProvider()
+ // .GetRequiredService()
+ // ),
+ // serviceCollection.BuildServiceProvider()
+ // )
+ // .RenderComponent(rootComponent);
+ var renderer =
new BlazorRenderer(
- new HtmlRenderer(
- serviceCollection.BuildServiceProvider(),
- serviceCollection.BuildServiceProvider()
- .GetRequiredService()
- ),
- serviceCollection.BuildServiceProvider()
- )
- .RenderComponent(rootComponent);
- var directory = Path.Combine(new List() {this.outputDirectory}.Concat(route.Split('/')).ToArray()).ToLower();
- GeneratePage(directory, html);
+ new HtmlRenderer(
+ serviceProvider,
+ serviceProvider
+ .GetRequiredService()
+ ),
+ serviceProvider
+ );
+ var html =
+ await renderer.RenderComponent(rootComponent);
+
+
+ var directory = Path.Combine(new List() {this.outputDirectory}.Concat(route.Split('/')).ToArray()).ToLower();
+ GeneratePage(directory, html);
+ }
+ catch (System.Exception ex)
+ {
+ Console.WriteLine($"Failed to build route: {route} {ex.ToString()}");
+ throw;
+ }
}
- catch (System.Exception ex)
+ return Task.CompletedTask;
+ });
+ }
+
+ private async void Prerender(string route, NavigationManager routeManager, Type rootComponent, IServiceCollection serviceCollection) {
+ routeManager.NavigateTo(route, forceLoad: true);
+ var html =
+ await
+ new BlazorRenderer(
+ new HtmlRenderer(
+ serviceCollection.BuildServiceProvider(),
+ serviceCollection.BuildServiceProvider()
+ .GetRequiredService()
+ ),
+ serviceCollection.BuildServiceProvider()
+ )
+ .RenderComponent(rootComponent);
+ var directory = Path.Combine(new List() {this.outputDirectory}.Concat(route.Split('/')).ToArray()).ToLower();
+ GeneratePage(directory, html);
+ }
+
+ private bool ShouldBePreRender(string route, bool fallback = true)
+ {
+ var result = fallback;
+ var page =
+ this.assembly
+ .GetExportedTypes()
+ .Where(t => t.GetCustomAttributes(typeof(RouteAttribute), true).Count() > 0)
+ .Where(t =>
+ {
+ var routeValue = t.GetCustomAttributes(typeof(RouteAttribute), true).FirstOrDefault() as RouteAttribute;
+
+ return routeValue.Template.Equals(route, StringComparison.InvariantCultureIgnoreCase);
+ })
+ .FirstOrDefault();
+ if (page != null)
+ {
+ var prerender = page.GetField("prerender", BindingFlags.Static | BindingFlags.NonPublic);
+ if(prerender != null)
{
- Console.WriteLine($"Failed to build route: {route} {ex.ToString()}");
- throw;
+ var shouldPreRender = (bool)prerender.GetValue(null);
+ result = shouldPreRender;
}
}
+ return result;
}
private void GeneratePage(string directory, string htmlContent)
diff --git a/src/BlazeKit.Static/Utils/MarkdownExtensions.cs b/src/BlazeKit.Static/Utils/MarkdownExtensions.cs
new file mode 100644
index 0000000..10efdea
--- /dev/null
+++ b/src/BlazeKit.Static/Utils/MarkdownExtensions.cs
@@ -0,0 +1,55 @@
+using Markdig;
+using Markdig.Extensions.Yaml;
+using Markdig.Syntax;
+using YamlDotNet.Serialization;
+
+namespace BlazeKit.Static.Utils;
+
+public static class MarkdownExtensions
+{
+ private static readonly IDeserializer YamlDeserializer =
+ new DeserializerBuilder()
+ .IgnoreUnmatchedProperties()
+ .Build();
+
+ private static readonly MarkdownPipeline Pipeline
+ = new MarkdownPipelineBuilder()
+ .UseYamlFrontMatter()
+ .Build();
+
+ public static T? GetFrontMatter(this string markdown)
+ {
+ var document = Markdown.Parse(markdown, Pipeline);
+ var block = document
+ .Descendants()
+ .FirstOrDefault();
+
+ if (block == null)
+ return default;
+
+ var yaml =
+ block
+ // this is not a mistake
+ // we have to call .Lines 2x
+ .Lines // StringLineGroup[]
+ .Lines // StringLine[]
+ .OrderByDescending(x => x.Line)
+ .Select(x => $"{x}\n")
+ .ToList()
+ .Select(x => x.Replace("---", string.Empty))
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Aggregate((s, agg) => agg + s);
+
+ // Console.WriteLine(yaml);
+ // var deserializer = new DeserializerBuilder().Build();
+ // var result = deserializer.Deserialize>(yaml);
+ // Console.WriteLine(result["title"]);
+ // Console.WriteLine(result["author"]);
+ return YamlDeserializer.Deserialize(yaml);
+ }
+
+ public static string Html(this string markdown)
+ {
+ return Markdown.ToHtml(markdown,Pipeline);
+ }
+}
diff --git a/src/BlazeKit.Web/BlazeKit.Web.csproj b/src/BlazeKit.Web/BlazeKit.Web.csproj
new file mode 100644
index 0000000..db436c5
--- /dev/null
+++ b/src/BlazeKit.Web/BlazeKit.Web.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BlazeKit.Web/Component1.razor b/src/BlazeKit.Web/Component1.razor
new file mode 100644
index 0000000..bb3e792
--- /dev/null
+++ b/src/BlazeKit.Web/Component1.razor
@@ -0,0 +1,3 @@
+
+ This component is defined in the BlazeKit.Web library.
+
diff --git a/src/BlazeKit.Web/Component1.razor.css b/src/BlazeKit.Web/Component1.razor.css
new file mode 100644
index 0000000..c6afca4
--- /dev/null
+++ b/src/BlazeKit.Web/Component1.razor.css
@@ -0,0 +1,6 @@
+.my-component {
+ border: 2px dashed red;
+ padding: 1em;
+ margin: 1em 0;
+ background-image: url('background.png');
+}
diff --git a/src/BlazeKit.Web/Components/ClientHydrateMode.cs b/src/BlazeKit.Web/Components/ClientHydrateMode.cs
new file mode 100644
index 0000000..58e051c
--- /dev/null
+++ b/src/BlazeKit.Web/Components/ClientHydrateMode.cs
@@ -0,0 +1,32 @@
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Rendering;
+
+namespace BlazeKit.Web.Components;
+
+///
+/// Determines when a interactive component should be hydrated on the client
+///
+public enum ClientHydrateMode
+{
+ ///
+ /// Hydrates the component as soon as possible
+ ///
+ Load,
+ ///
+ /// Hydrates the component when the browser is idle
+ ///
+ Idle,
+ ///
+ /// Hydrates the component when the user hovers over it
+ ///
+ Hover,
+ ///
+ /// Hydrates the component when it is visible in the viewport
+ ///
+ Visible,
+ ///
+ /// Does not hydrate the component
+ /// The component will be rendered as a static HTML element
+ ///
+ None
+}
diff --git a/src/BlazeKit.Web/Components/IslandComponent.cs b/src/BlazeKit.Web/Components/IslandComponent.cs
new file mode 100644
index 0000000..3fe7fde
--- /dev/null
+++ b/src/BlazeKit.Web/Components/IslandComponent.cs
@@ -0,0 +1,205 @@
+using BlazeKit.Abstraction;
+using BlazeKit.Hydration;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Rendering;
+
+namespace BlazeKit.Web.Components;
+
+
+public abstract class IslandComponent : IslandComponent
+{
+
+}
+
+public abstract class IslandComponent : IComponent, IHandleEvent, IReactiveComponent
+{
+ [Parameter] public ClientHydrateMode Client { get; set; } = ClientHydrateMode.None;
+
+ [Inject] required public DataHydrationContext HydrationContext { get; init; }
+ [Inject] private Microsoft.Extensions.Hosting.IHostEnvironment HostEnvironment { get; init; }
+
+ private RenderFragment renderFragment;
+ private RenderHandle renderHandle;
+ private TResult data;
+ private string dataKey;
+ private string prerenderId = Guid.NewGuid().ToString();
+ private string locationhash = Guid.NewGuid().ToString();
+
+
+ ///
+ /// The data which has been loaded with LoadAsync.
+ /// If the method is not overwritten by the derived class the
+ /// page data will be null.
+ ///
+ [CascadingParameter(Name = "PageData")]
+ public TResult PageData
+ {
+ get
+ {
+ return data;
+ }
+ set
+ {
+ data = value;
+ }
+ }
+
+ public IslandComponent(/*string dataKey*/)
+ {
+ renderFragment = BuildRenderTree;
+ this.dataKey = "pagedata";
+ }
+
+ public void Attach(RenderHandle renderHandle)
+ {
+ this.renderHandle = renderHandle;
+ }
+ public async Task SetParametersAsync(ParameterView parameters)
+ {
+ parameters.SetParameterProperties(this);
+
+ if(IsServer())
+ {
+ // render the component decorated with the wasm marker
+ renderHandle.Render(Render);
+ } else
+ {
+ // Render component without the wasm marker
+ var data = await LoadHydratedPageDataAsync(default(TResult));
+ if (data == null)
+ {
+ throw new InvalidOperationException($"Could not load page data for '{this.dataKey}'");
+ }
+ else
+ {
+ this.data = data;
+ }
+ renderHandle.Render(renderFragment);
+ }
+
+ if(OperatingSystem.IsBrowser())
+ {
+ OnMount();
+ }
+ }
+
+ ///
+ /// Renders the component to the supplied .
+ ///
+ /// A that will receive the render output.
+ protected virtual void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ // Developers can either override this method in derived classes, or can use Razor
+ // syntax to define a derived class and have the compiler generate the method.
+
+ // Other code within this class should *not* invoke BuildRenderTree directly,
+ // but instead should invoke the _renderFragment field.
+ }
+
+ ///
+ /// This method is called slient-side when the component has been mounted/rendered into the DOM
+ ///
+ protected virtual void OnMount()
+ { }
+
+
+ void Render(RenderTreeBuilder builder)
+ {
+ var needsHydration = Client != ClientHydrateMode.None;
+ if (!HostEnvironment.EnvironmentName.Equals("Development", StringComparison.InvariantCultureIgnoreCase))
+ {
+ // render marker around the balzekit div.
+ // This will remove the div when the component is hydrated on the client
+ if (needsHydration)
+ {
+ builder.AddMarkupContent(0, OpenWasmComponent());
+ }
+
+ builder.OpenElement(1, "div");
+ builder.AddAttribute(2, "blazekit-id", prerenderId);
+ builder.AddAttribute(3, "client", Client.ToString().ToLower());
+
+ builder.OpenComponent>(0);
+ builder.AddComponentParameter(1, "Value", this.PageData);
+ builder.AddComponentParameter(2, "ChildContent", renderFragment);
+ builder.CloseComponent();
+
+ //this.BuildRenderTree(builder);
+ builder.CloseElement();
+
+ if (needsHydration)
+ {
+ builder.AddMarkupContent(4, CloseWasmComponent());
+ }
+
+ }
+ else
+ {
+ // render the marker around the component. This will leave the blazekit div in place when the component is hydrated on the client
+ // we can use this div to visualize the component boundaries in the browser
+ builder.OpenElement(0, "div");
+ builder.AddAttribute(1, "blazekit-id", prerenderId);
+ builder.AddAttribute(2, "client", Client.ToString().ToLower());
+ if (needsHydration)
+ builder.AddMarkupContent(0, OpenWasmComponent());
+
+
+ builder.OpenComponent>(0);
+ builder.AddComponentParameter(1, "Value", this.PageData);
+ builder.AddComponentParameter(2, "ChildContent", (RenderFragment)((builder2) => BuildRenderTree(builder2)));
+ builder.CloseComponent();
+
+
+ if (needsHydration)
+ {
+ builder.AddMarkupContent(4, CloseWasmComponent());
+ }
+
+ builder.CloseElement();
+ }
+ }
+
+ protected Task LoadHydratedPageDataAsync(T fallback)
+ {
+ return HydrationContext.GetAsync(dataKey, fallback);
+ }
+
+ private string OpenWasmComponent()
+ {
+ var typeName = this.GetType().FullName;
+ var assemblyName = $"{this.GetType().Assembly.GetName().Name}";
+ return @$"";
+ }
+
+ private string CloseWasmComponent()
+ {
+ return @$"";
+ }
+ private bool IsServer()
+ {
+ return !OperatingSystem.IsBrowser();
+ }
+
+ public Task HandleEventAsync(EventCallbackWorkItem callback, object? arg)
+ {
+ var task = callback.InvokeAsync(arg);
+ var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
+ task.Status != TaskStatus.Canceled;
+
+ // After each event, we synchronously re-render (unless !ShouldRender())
+ // This just saves the developer the trouble of putting "StateHasChanged();"
+ // at the end of every event callback.
+ Update();
+
+ return Task.CompletedTask;
+
+ //return shouldAwaitTask ?
+ // CallStateHasChangedOnAsyncCompletion(task) :
+ // Task.CompletedTask;
+ }
+
+ public void Update()
+ {
+ renderHandle.Render(renderFragment);
+ }
+}
diff --git a/src/BlazeKit.Web/Components/IslandComponentBase.cs b/src/BlazeKit.Web/Components/IslandComponentBase.cs
new file mode 100644
index 0000000..dcad829
--- /dev/null
+++ b/src/BlazeKit.Web/Components/IslandComponentBase.cs
@@ -0,0 +1,423 @@
+using BlazeKit.Abstraction;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Rendering;
+
+namespace BlazeKit.Web.Components;
+
+//
+// Summary:
+// Optional base class for components. Alternatively, components may implement Microsoft.AspNetCore.Components.IComponent
+// directly.
+public abstract class IslandComponentBase : IComponent, IHandleEvent, IHandleAfterRender, IReactiveComponent
+{
+ [Parameter]
+ public ClientHydrateMode Client { get; set; } = ClientHydrateMode.None;
+
+ [Inject]
+ public Microsoft.Extensions.Hosting.IHostEnvironment HostEnvironment { get; set; }
+
+ private readonly RenderFragment _renderFragment;
+
+ private RenderHandle _renderHandle;
+
+ private bool _initialized;
+
+ private bool _hasNeverRendered = true;
+
+ private bool _hasPendingQueuedRender;
+
+ private bool _hasCalledOnAfterRender;
+
+ private string prerenderId = Guid.NewGuid().ToString();
+ private string locationhash = Guid.NewGuid().ToString();
+
+
+
+ //
+ // Summary:
+ // Constructs an instance of Microsoft.AspNetCore.Components.ComponentBase.
+ public IslandComponentBase()
+ {
+ _renderFragment = delegate (RenderTreeBuilder builder)
+ {
+ _hasPendingQueuedRender = false;
+ _hasNeverRendered = false;
+ // BuildRenderTree(builder);
+ if(!OperatingSystem.IsBrowser()) {
+ // render the component decorated with the wasm marker
+ Render(builder);
+ }
+ else {
+ // Render component without the wasm marker
+ BuildRenderTree(builder);
+ }
+
+ };
+ }
+
+ void Render(RenderTreeBuilder builder)
+ {
+ var needsHydration = Client != ClientHydrateMode.None;
+ if(!HostEnvironment.EnvironmentName.Equals("Development", StringComparison.InvariantCultureIgnoreCase))
+ {
+ // render marker around the balzekit div.
+ // This will remove the div when the component is hydrated on the client
+ if(needsHydration)
+ {
+ builder.AddMarkupContent(0, OpenWasmComponent());
+ }
+
+ builder.OpenElement(1, "div");
+ builder.AddAttribute(2, "blazekit-id", prerenderId);
+ builder.AddAttribute(3, "client", Client.ToString().ToLower());
+ this.BuildRenderTree(builder);
+ builder.CloseElement();
+
+ if(needsHydration)
+ {
+ builder.AddMarkupContent(4, CloseWasmComponent());
+ }
+
+ } else {
+ // render the marker around the component. This will leave the blazekit div in place when the component is hydrated on the client
+ // we can use this div to visualize the component boundaries in the browser
+ builder.OpenElement(0, "div");
+ builder.AddAttribute(1, "blazekit-id", prerenderId);
+ builder.AddAttribute(2, "client", Client.ToString().ToLower());
+ if(needsHydration)
+ builder.AddMarkupContent(0, OpenWasmComponent());
+
+ this.BuildRenderTree(builder);
+ if(needsHydration)
+ {
+ builder.AddMarkupContent(4, CloseWasmComponent());
+ }
+
+ builder.CloseElement();
+ }
+ }
+
+
+ private string OpenWasmComponent()
+ {
+ var typeName = this.GetType().FullName;
+ var assemblyName = $"{this.GetType().Assembly.GetName().Name}";
+ return @$"";
+ }
+
+ private string CloseWasmComponent()
+ {
+ return @$"";
+ }
+
+ // Summary:
+ // Renders the component to the supplied Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.
+ //
+ //
+ // Parameters:
+ // builder:
+ // A Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder that will receive
+ // the render output.
+ protected virtual void BuildRenderTree(RenderTreeBuilder builder){}
+
+ //
+ // Summary:
+ // Method invoked when the component is ready to start, having received its initial
+ // parameters from its parent in the render tree.
+ protected virtual void OnInitialized()
+ {
+ }
+
+ //
+ // Summary:
+ // Method invoked when the component is ready to start, having received its initial
+ // parameters from its parent in the render tree. Override this method if you will
+ // perform an asynchronous operation and want the component to refresh when that
+ // operation is completed.
+ //
+ // Returns:
+ // A System.Threading.Tasks.Task representing any asynchronous operation.
+ protected virtual Task OnInitializedAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ //
+ // Summary:
+ // Method invoked when the component has received parameters from its parent in
+ // the render tree, and the incoming values have been assigned to properties.
+ protected virtual void OnParametersSet()
+ {
+ }
+
+ //
+ // Summary:
+ // Method invoked when the component has received parameters from its parent in
+ // the render tree, and the incoming values have been assigned to properties.
+ //
+ // Returns:
+ // A System.Threading.Tasks.Task representing any asynchronous operation.
+ protected virtual Task OnParametersSetAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ //
+ // Summary:
+ // Notifies the component that its state has changed. When applicable, this will
+ // cause the component to be re-rendered.
+ protected void StateHasChanged()
+ {
+ if (_hasPendingQueuedRender || (!_hasNeverRendered && !ShouldRender() && !_renderHandle.IsRenderingOnMetadataUpdate))
+ {
+ return;
+ }
+
+ _hasPendingQueuedRender = true;
+ try
+ {
+ _renderHandle.Render(_renderFragment);
+ }
+ catch
+ {
+ _hasPendingQueuedRender = false;
+ throw;
+ }
+ }
+
+ //
+ // Summary:
+ // Returns a flag to indicate whether the component should render.
+ protected virtual bool ShouldRender()
+ {
+ return true;
+ }
+
+ //
+ // Summary:
+ // Method invoked after each time the component has rendered interactively and the
+ // UI has finished updating (for example, after elements have been added to the
+ // browser DOM). Any Microsoft.AspNetCore.Components.ElementReference fields will
+ // be populated by the time this runs. This method is not invoked during prerendering
+ // or server-side rendering, because those processes are not attached to any live
+ // browser DOM and are already complete before the DOM is updated.
+ //
+ // Parameters:
+ // firstRender:
+ // Set to true if this is the first time Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean)
+ // has been invoked on this component instance; otherwise false.
+ //
+ // Remarks:
+ // The Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean)
+ // and Microsoft.AspNetCore.Components.ComponentBase.OnAfterRenderAsync(System.Boolean)
+ // lifecycle methods are useful for performing interop, or interacting with values
+ // received from @ref. Use the firstRender parameter to ensure that initialization
+ // work is only performed once.
+ protected virtual void OnAfterRender(bool firstRender)
+ {
+ }
+
+ //
+ // Summary:
+ // Method invoked after each time the component has been rendered interactively
+ // and the UI has finished updating (for example, after elements have been added
+ // to the browser DOM). Any Microsoft.AspNetCore.Components.ElementReference fields
+ // will be populated by the time this runs. This method is not invoked during prerendering
+ // or server-side rendering, because those processes are not attached to any live
+ // browser DOM and are already complete before the DOM is updated. Note that the
+ // component does not automatically re-render after the completion of any returned
+ // System.Threading.Tasks.Task, because that would cause an infinite render loop.
+ //
+ //
+ // Parameters:
+ // firstRender:
+ // Set to true if this is the first time Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean)
+ // has been invoked on this component instance; otherwise false.
+ //
+ // Returns:
+ // A System.Threading.Tasks.Task representing any asynchronous operation.
+ //
+ // Remarks:
+ // The Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean)
+ // and Microsoft.AspNetCore.Components.ComponentBase.OnAfterRenderAsync(System.Boolean)
+ // lifecycle methods are useful for performing interop, or interacting with values
+ // received from @ref. Use the firstRender parameter to ensure that initialization
+ // work is only performed once.
+ protected virtual Task OnAfterRenderAsync(bool firstRender)
+ {
+ return Task.CompletedTask;
+ }
+
+ //
+ // Summary:
+ // Executes the supplied work item on the associated renderer's synchronization
+ // context.
+ //
+ // Parameters:
+ // workItem:
+ // The work item to execute.
+ protected Task InvokeAsync(Action workItem)
+ {
+ return _renderHandle.Dispatcher.InvokeAsync(workItem);
+ }
+
+ //
+ // Summary:
+ // Executes the supplied work item on the associated renderer's synchronization
+ // context.
+ //
+ // Parameters:
+ // workItem:
+ // The work item to execute.
+ protected Task InvokeAsync(Func workItem)
+ {
+ return _renderHandle.Dispatcher.InvokeAsync(workItem);
+ }
+
+ //
+ // Summary:
+ // Treats the supplied exception as being thrown by this component. This will cause
+ // the enclosing ErrorBoundary to transition into a failed state. If there is no
+ // enclosing ErrorBoundary, it will be regarded as an exception from the enclosing
+ // renderer. This is useful if an exception occurs outside the component lifecycle
+ // methods, but you wish to treat it the same as an exception from a component lifecycle
+ // method.
+ //
+ // Parameters:
+ // exception:
+ // The System.Exception that will be dispatched to the renderer.
+ //
+ // Returns:
+ // A System.Threading.Tasks.Task that will be completed when the exception has finished
+ // dispatching.
+ protected Task DispatchExceptionAsync(Exception exception)
+ {
+ return _renderHandle.DispatchExceptionAsync(exception);
+ }
+
+ void IComponent.Attach(RenderHandle renderHandle)
+ {
+ if (_renderHandle.IsInitialized)
+ {
+ throw new InvalidOperationException("The render handle is already set. Cannot initialize a ComponentBase more than once.");
+ }
+
+ _renderHandle = renderHandle;
+ }
+
+ //
+ // Summary:
+ // Sets parameters supplied by the component's parent in the render tree.
+ //
+ // Parameters:
+ // parameters:
+ // The parameters.
+ //
+ // Returns:
+ // A System.Threading.Tasks.Task that completes when the component has finished
+ // updating and rendering itself.
+ //
+ // Remarks:
+ // Parameters are passed when Microsoft.AspNetCore.Components.ComponentBase.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView)
+ // is called. It is not required that the caller supply a parameter value for all
+ // of the parameters that are logically understood by the component.
+ //
+ // The default implementation of Microsoft.AspNetCore.Components.ComponentBase.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView)
+ // will set the value of each property decorated with Microsoft.AspNetCore.Components.ParameterAttribute
+ // or Microsoft.AspNetCore.Components.CascadingParameterAttribute that has a corresponding
+ // value in the Microsoft.AspNetCore.Components.ParameterView. Parameters that do
+ // not have a corresponding value will be unchanged.
+ public virtual Task SetParametersAsync(ParameterView parameters)
+ {
+ parameters.SetParameterProperties(this);
+ if (!_initialized)
+ {
+ _initialized = true;
+ return RunInitAndSetParametersAsync();
+ }
+
+ return CallOnParametersSetAsync();
+ }
+
+ private async Task RunInitAndSetParametersAsync()
+ {
+ OnInitialized();
+ Task task = OnInitializedAsync();
+ if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled)
+ {
+ StateHasChanged();
+ try
+ {
+ await task;
+ }
+ catch
+ {
+ if (!task.IsCanceled)
+ {
+ throw;
+ }
+ }
+ }
+
+ await CallOnParametersSetAsync();
+ }
+
+ private Task CallOnParametersSetAsync()
+ {
+ OnParametersSet();
+ Task task = OnParametersSetAsync();
+ bool num = task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled;
+ StateHasChanged();
+ if (!num)
+ {
+ return Task.CompletedTask;
+ }
+
+ return CallStateHasChangedOnAsyncCompletion(task);
+ }
+
+ private async Task CallStateHasChangedOnAsyncCompletion(Task task)
+ {
+ try
+ {
+ await task;
+ }
+ catch
+ {
+ if (task.IsCanceled)
+ {
+ return;
+ }
+
+ throw;
+ }
+
+ StateHasChanged();
+ }
+
+ Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object arg)
+ {
+ Task task = callback.InvokeAsync(arg);
+ bool num = task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled;
+ StateHasChanged();
+ if (!num)
+ {
+ return Task.CompletedTask;
+ }
+
+ return CallStateHasChangedOnAsyncCompletion(task);
+ }
+
+ Task IHandleAfterRender.OnAfterRenderAsync()
+ {
+ bool firstRender = !_hasCalledOnAfterRender;
+ _hasCalledOnAfterRender = true;
+ OnAfterRender(firstRender);
+ return OnAfterRenderAsync(firstRender);
+ }
+
+ public void Update()
+ {
+ StateHasChanged();
+ }
+}
diff --git a/src/BlazeKit.Web/Components/IslandComponentBase2.cs b/src/BlazeKit.Web/Components/IslandComponentBase2.cs
new file mode 100644
index 0000000..9f87571
--- /dev/null
+++ b/src/BlazeKit.Web/Components/IslandComponentBase2.cs
@@ -0,0 +1,452 @@
+using BlazeKit.Abstraction;
+using BlazeKit.Hydration;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Rendering;
+using static System.Runtime.InteropServices.JavaScript.JSType;
+
+namespace BlazeKit.Web.Components;
+
+//
+// Summary:
+// Optional base class for components. Alternatively, components may implement Microsoft.AspNetCore.Components.IComponent
+// directly.
+public abstract class IslandComponentBase2 : IComponent, IHandleEvent, IHandleAfterRender, IReactiveComponent
+{
+ private TResult data;
+ private string dataKey = "pagedata";
+
+ [Inject] required public DataHydrationContext HydrationContext { get; init; }
+
+ [Parameter]
+ public ClientHydrateMode Client { get; set; } = ClientHydrateMode.None;
+
+ [Inject]
+ public Microsoft.Extensions.Hosting.IHostEnvironment HostEnvironment { get; set; }
+
+ private readonly RenderFragment _renderFragment;
+
+ private RenderHandle _renderHandle;
+
+ private bool _initialized;
+
+ private bool _hasNeverRendered = true;
+
+ private bool _hasPendingQueuedRender;
+
+ private bool _hasCalledOnAfterRender;
+
+ private string prerenderId = Guid.NewGuid().ToString();
+ private string locationhash = Guid.NewGuid().ToString();
+
+ ///
+ /// The data which has been loaded with LoadAsync.
+ /// If the method is not overwritten by the derived class the
+ /// page data will be null.
+ ///
+ [CascadingParameter(Name = "PageData")]
+ public TResult PageData
+ {
+ get
+ {
+ return data;
+ }
+ set
+ {
+ data = value;
+ }
+ }
+
+ //
+ // Summary:
+ // Constructs an instance of Microsoft.AspNetCore.Components.ComponentBase.
+ public IslandComponentBase2()
+ {
+ _renderFragment = delegate (RenderTreeBuilder builder)
+ {
+ _hasPendingQueuedRender = false;
+ _hasNeverRendered = false;
+ // BuildRenderTree(builder);
+ if(!OperatingSystem.IsBrowser()) {
+ // render the component decorated with the wasm marker
+ Render(builder);
+ }
+ else {
+ // Render component without the wasm marker
+ BuildRenderTree(builder);
+ }
+
+ };
+ }
+
+ void Render(RenderTreeBuilder builder)
+ {
+ var needsHydration = Client != ClientHydrateMode.None;
+ if(!HostEnvironment.EnvironmentName.Equals("Development", StringComparison.InvariantCultureIgnoreCase))
+ {
+ // render marker around the balzekit div.
+ // This will remove the div when the component is hydrated on the client
+ if(needsHydration)
+ {
+ builder.AddMarkupContent(0, OpenWasmComponent());
+ }
+
+ builder.OpenElement(1, "div");
+ builder.AddAttribute(2, "blazekit-id", prerenderId);
+ builder.AddAttribute(3, "client", Client.ToString().ToLower());
+ this.BuildRenderTree(builder);
+ builder.CloseElement();
+
+ if(needsHydration)
+ {
+ builder.AddMarkupContent(4, CloseWasmComponent());
+ }
+
+ } else {
+ // render the marker around the component. This will leave the blazekit div in place when the component is hydrated on the client
+ // we can use this div to visualize the component boundaries in the browser
+ builder.OpenElement(0, "div");
+ builder.AddAttribute(1, "blazekit-id", prerenderId);
+ builder.AddAttribute(2, "client", Client.ToString().ToLower());
+ if(needsHydration)
+ builder.AddMarkupContent(0, OpenWasmComponent());
+
+ this.BuildRenderTree(builder);
+ if(needsHydration)
+ {
+ builder.AddMarkupContent(4, CloseWasmComponent());
+ }
+
+ builder.CloseElement();
+ }
+ }
+
+
+ private string OpenWasmComponent()
+ {
+ var typeName = this.GetType().FullName;
+ var assemblyName = $"{this.GetType().Assembly.GetName().Name}";
+ return @$"";
+ }
+
+ private string CloseWasmComponent()
+ {
+ return @$"";
+ }
+
+ // Summary:
+ // Renders the component to the supplied Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.
+ //
+ //
+ // Parameters:
+ // builder:
+ // A Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder that will receive
+ // the render output.
+ protected virtual void BuildRenderTree(RenderTreeBuilder builder){}
+
+ //
+ // Summary:
+ // Method invoked when the component is ready to start, having received its initial
+ // parameters from its parent in the render tree.
+ protected virtual void OnInitialized()
+ {
+ }
+
+ //
+ // Summary:
+ // Method invoked when the component is ready to start, having received its initial
+ // parameters from its parent in the render tree. Override this method if you will
+ // perform an asynchronous operation and want the component to refresh when that
+ // operation is completed.
+ //
+ // Returns:
+ // A System.Threading.Tasks.Task representing any asynchronous operation.
+ protected virtual Task OnInitializedAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ //
+ // Summary:
+ // Method invoked when the component has received parameters from its parent in
+ // the render tree, and the incoming values have been assigned to properties.
+ protected virtual void OnParametersSet()
+ {
+ }
+
+ //
+ // Summary:
+ // Method invoked when the component has received parameters from its parent in
+ // the render tree, and the incoming values have been assigned to properties.
+ //
+ // Returns:
+ // A System.Threading.Tasks.Task representing any asynchronous operation.
+ protected virtual Task OnParametersSetAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ //
+ // Summary:
+ // Notifies the component that its state has changed. When applicable, this will
+ // cause the component to be re-rendered.
+ protected void StateHasChanged()
+ {
+ if (_hasPendingQueuedRender || (!_hasNeverRendered && !ShouldRender() && !_renderHandle.IsRenderingOnMetadataUpdate))
+ {
+ return;
+ }
+
+ _hasPendingQueuedRender = true;
+ try
+ {
+ _renderHandle.Render(_renderFragment);
+ }
+ catch
+ {
+ _hasPendingQueuedRender = false;
+ throw;
+ }
+ }
+
+ //
+ // Summary:
+ // Returns a flag to indicate whether the component should render.
+ protected virtual bool ShouldRender()
+ {
+ return true;
+ }
+
+ //
+ // Summary:
+ // Method invoked after each time the component has rendered interactively and the
+ // UI has finished updating (for example, after elements have been added to the
+ // browser DOM). Any Microsoft.AspNetCore.Components.ElementReference fields will
+ // be populated by the time this runs. This method is not invoked during prerendering
+ // or server-side rendering, because those processes are not attached to any live
+ // browser DOM and are already complete before the DOM is updated.
+ //
+ // Parameters:
+ // firstRender:
+ // Set to true if this is the first time Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean)
+ // has been invoked on this component instance; otherwise false.
+ //
+ // Remarks:
+ // The Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean)
+ // and Microsoft.AspNetCore.Components.ComponentBase.OnAfterRenderAsync(System.Boolean)
+ // lifecycle methods are useful for performing interop, or interacting with values
+ // received from @ref. Use the firstRender parameter to ensure that initialization
+ // work is only performed once.
+ protected virtual void OnAfterRender(bool firstRender)
+ {
+ }
+
+ //
+ // Summary:
+ // Method invoked after each time the component has been rendered interactively
+ // and the UI has finished updating (for example, after elements have been added
+ // to the browser DOM). Any Microsoft.AspNetCore.Components.ElementReference fields
+ // will be populated by the time this runs. This method is not invoked during prerendering
+ // or server-side rendering, because those processes are not attached to any live
+ // browser DOM and are already complete before the DOM is updated. Note that the
+ // component does not automatically re-render after the completion of any returned
+ // System.Threading.Tasks.Task, because that would cause an infinite render loop.
+ //
+ //
+ // Parameters:
+ // firstRender:
+ // Set to true if this is the first time Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean)
+ // has been invoked on this component instance; otherwise false.
+ //
+ // Returns:
+ // A System.Threading.Tasks.Task representing any asynchronous operation.
+ //
+ // Remarks:
+ // The Microsoft.AspNetCore.Components.ComponentBase.OnAfterRender(System.Boolean)
+ // and Microsoft.AspNetCore.Components.ComponentBase.OnAfterRenderAsync(System.Boolean)
+ // lifecycle methods are useful for performing interop, or interacting with values
+ // received from @ref. Use the firstRender parameter to ensure that initialization
+ // work is only performed once.
+ protected virtual Task OnAfterRenderAsync(bool firstRender)
+ {
+ return Task.CompletedTask;
+ }
+
+ //
+ // Summary:
+ // Executes the supplied work item on the associated renderer's synchronization
+ // context.
+ //
+ // Parameters:
+ // workItem:
+ // The work item to execute.
+ protected Task InvokeAsync(Action workItem)
+ {
+ return _renderHandle.Dispatcher.InvokeAsync(workItem);
+ }
+
+ //
+ // Summary:
+ // Executes the supplied work item on the associated renderer's synchronization
+ // context.
+ //
+ // Parameters:
+ // workItem:
+ // The work item to execute.
+ protected Task InvokeAsync(Func workItem)
+ {
+ return _renderHandle.Dispatcher.InvokeAsync(workItem);
+ }
+
+ //
+ // Summary:
+ // Treats the supplied exception as being thrown by this component. This will cause
+ // the enclosing ErrorBoundary to transition into a failed state. If there is no
+ // enclosing ErrorBoundary, it will be regarded as an exception from the enclosing
+ // renderer. This is useful if an exception occurs outside the component lifecycle
+ // methods, but you wish to treat it the same as an exception from a component lifecycle
+ // method.
+ //
+ // Parameters:
+ // exception:
+ // The System.Exception that will be dispatched to the renderer.
+ //
+ // Returns:
+ // A System.Threading.Tasks.Task that will be completed when the exception has finished
+ // dispatching.
+ protected Task DispatchExceptionAsync(Exception exception)
+ {
+ return _renderHandle.DispatchExceptionAsync(exception);
+ }
+
+ void IComponent.Attach(RenderHandle renderHandle)
+ {
+ if (_renderHandle.IsInitialized)
+ {
+ throw new InvalidOperationException("The render handle is already set. Cannot initialize a ComponentBase more than once.");
+ }
+
+ _renderHandle = renderHandle;
+ }
+
+ //
+ // Summary:
+ // Sets parameters supplied by the component's parent in the render tree.
+ //
+ // Parameters:
+ // parameters:
+ // The parameters.
+ //
+ // Returns:
+ // A System.Threading.Tasks.Task that completes when the component has finished
+ // updating and rendering itself.
+ //
+ // Remarks:
+ // Parameters are passed when Microsoft.AspNetCore.Components.ComponentBase.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView)
+ // is called. It is not required that the caller supply a parameter value for all
+ // of the parameters that are logically understood by the component.
+ //
+ // The default implementation of Microsoft.AspNetCore.Components.ComponentBase.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView)
+ // will set the value of each property decorated with Microsoft.AspNetCore.Components.ParameterAttribute
+ // or Microsoft.AspNetCore.Components.CascadingParameterAttribute that has a corresponding
+ // value in the Microsoft.AspNetCore.Components.ParameterView. Parameters that do
+ // not have a corresponding value will be unchanged.
+ public virtual Task SetParametersAsync(ParameterView parameters)
+ {
+ parameters.SetParameterProperties(this);
+ if (!_initialized)
+ {
+ _initialized = true;
+ return RunInitAndSetParametersAsync();
+ }
+
+ return CallOnParametersSetAsync();
+ }
+
+ private async Task RunInitAndSetParametersAsync()
+ {
+ this.data = await LoadPageDataAsync(default(TResult));
+ OnInitialized();
+ Task task = OnInitializedAsync();
+ if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled)
+ {
+ StateHasChanged();
+ try
+ {
+ await task;
+ }
+ catch
+ {
+ if (!task.IsCanceled)
+ {
+ throw;
+ }
+ }
+ }
+
+ await CallOnParametersSetAsync();
+ }
+
+ private Task CallOnParametersSetAsync()
+ {
+ OnParametersSet();
+ Task task = OnParametersSetAsync();
+ bool num = task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled;
+ StateHasChanged();
+ if (!num)
+ {
+ return Task.CompletedTask;
+ }
+
+ return CallStateHasChangedOnAsyncCompletion(task);
+ }
+
+ private async Task CallStateHasChangedOnAsyncCompletion(Task task)
+ {
+ try
+ {
+ await task;
+ }
+ catch
+ {
+ if (task.IsCanceled)
+ {
+ return;
+ }
+
+ throw;
+ }
+
+ StateHasChanged();
+ }
+
+ protected Task LoadPageDataAsync(T fallback)
+ {
+ return HydrationContext.GetAsync(dataKey, fallback);
+ }
+
+ Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object arg)
+ {
+ Task task = callback.InvokeAsync(arg);
+ bool num = task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled;
+ StateHasChanged();
+ if (!num)
+ {
+ return Task.CompletedTask;
+ }
+
+ return CallStateHasChangedOnAsyncCompletion(task);
+ }
+
+ Task IHandleAfterRender.OnAfterRenderAsync()
+ {
+ bool firstRender = !_hasCalledOnAfterRender;
+ _hasCalledOnAfterRender = true;
+ OnAfterRender(firstRender);
+ return OnAfterRenderAsync(firstRender);
+ }
+
+ public void Update()
+ {
+ StateHasChanged();
+ }
+}
diff --git a/src/BlazeKit.Web/Components/PageComponentBase.cs b/src/BlazeKit.Web/Components/PageComponentBase.cs
new file mode 100644
index 0000000..0224367
--- /dev/null
+++ b/src/BlazeKit.Web/Components/PageComponentBase.cs
@@ -0,0 +1,102 @@
+using BlazeKit.Hydration;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Rendering;
+using System.Diagnostics.CodeAnalysis;
+
+namespace BlazeKit.Web.Components;
+public abstract class PageComponentBase : IComponent where TResult : PageDataBase
+{
+ [Inject] required public DataHydrationContext HydrationContext { get; init; }
+
+ private RenderFragment renderFragment;
+ private readonly string dataKey;
+ private RenderHandle renderHandle;
+ private TResult data;
+
+
+ ///
+ /// The data which has been loaded with LoadAsync.
+ /// If the method is not overwritten by the derived class the
+ /// page data will be null.
+ ///
+ public TResult PageData
+ {
+ get
+ {
+ return data;
+ }
+ set
+ {
+ data = value;
+ }
+ }
+ public PageComponentBase()
+ {
+ renderFragment = BuildRenderTree;
+ this.data = default(TResult);
+ this.dataKey = "pagedata";
+ }
+
+ public void Attach(RenderHandle renderHandle)
+ {
+ this.renderHandle = renderHandle;
+ }
+ public async Task SetParametersAsync(ParameterView parameters)
+ {
+ parameters.SetParameterProperties(this);
+
+ if(IsServer())
+ {
+ var data = await this.ServerLoadAsync();
+ if(data != null)
+ {
+ this.data = data;
+ // we add the data to the data hydration context to access it on client side
+ HydrationContext.Add(this.dataKey, data);
+ }
+ }
+
+ renderHandle.Render(Render);
+ }
+
+ ///
+ /// Renders the component to the supplied .
+ ///
+ /// A that will receive the render output.
+ protected virtual void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ // Developers can either override this method in derived classes, or can use Razor
+ // syntax to define a derived class and have the compiler generate the method.
+
+ // Other code within this class should *not* invoke BuildRenderTree directly,
+ // but instead should invoke the _renderFragment field.
+ }
+
+ protected virtual Task ServerLoadAsync()
+ {
+ return Task.FromResult(default(TResult));
+ }
+
+
+ void Render(RenderTreeBuilder builder)
+ {
+ // decorate the ChildContent with a CascadingValue contianing the PageData
+ builder.OpenComponent>(0);
+ builder.AddComponentParameter(1, "Value", this.PageData);
+ builder.AddComponentParameter(1, "Name", "PageData");
+ builder.AddComponentParameter(2, "ChildContent", renderFragment);
+ builder.CloseComponent();
+ }
+
+ protected Task LoadPageDataAsync(T fallback)
+ {
+ return HydrationContext.GetAsync(this.dataKey, fallback);
+ }
+
+ private bool IsServer()
+ {
+ return !OperatingSystem.IsBrowser();
+ }
+
+
+}
diff --git a/src/BlazeKit.Website/Components/WebAssemblyLoadProgress.razor b/src/BlazeKit.Web/Components/WebAssemblyLoadProgress.razor
similarity index 100%
rename from src/BlazeKit.Website/Components/WebAssemblyLoadProgress.razor
rename to src/BlazeKit.Web/Components/WebAssemblyLoadProgress.razor
diff --git a/src/BlazeKit.Web/ExampleJsInterop.cs b/src/BlazeKit.Web/ExampleJsInterop.cs
new file mode 100644
index 0000000..00fdaa9
--- /dev/null
+++ b/src/BlazeKit.Web/ExampleJsInterop.cs
@@ -0,0 +1,36 @@
+using Microsoft.JSInterop;
+
+namespace BlazeKit.Web;
+
+// This class provides an example of how JavaScript functionality can be wrapped
+// in a .NET class for easy consumption. The associated JavaScript module is
+// loaded on demand when first needed.
+//
+// This class can be registered as scoped DI service and then injected into Blazor
+// components for use.
+
+public class ExampleJsInterop : IAsyncDisposable
+{
+ private readonly Lazy> moduleTask;
+
+ public ExampleJsInterop(IJSRuntime jsRuntime)
+ {
+ moduleTask = new (() => jsRuntime.InvokeAsync(
+ "import", "./_content/BlazeKit.Web/exampleJsInterop.js").AsTask());
+ }
+
+ public async ValueTask Prompt(string message)
+ {
+ var module = await moduleTask.Value;
+ return await module.InvokeAsync("showPrompt", message);
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (moduleTask.IsValueCreated)
+ {
+ var module = await moduleTask.Value;
+ await module.DisposeAsync();
+ }
+ }
+}
diff --git a/src/BlazeKit.Web/PageDataBase.cs b/src/BlazeKit.Web/PageDataBase.cs
new file mode 100644
index 0000000..f2f1b90
--- /dev/null
+++ b/src/BlazeKit.Web/PageDataBase.cs
@@ -0,0 +1,19 @@
+using System.Text.Json.Serialization;
+
+namespace BlazeKit.Web;
+
+public class PageDataBase
+{
+ public PageDataBase()
+ {
+
+ }
+ [JsonIgnore]
+ public string Key => "pagedata";
+
+ public static PageDataBase Empty()
+ {
+ return new PageDataBase();
+ }
+
+}
diff --git a/src/BlazeKit.Website/Islands/Components/Web/CssClasses.cs b/src/BlazeKit.Web/Utils/CssClass.cs
similarity index 72%
rename from src/BlazeKit.Website/Islands/Components/Web/CssClasses.cs
rename to src/BlazeKit.Web/Utils/CssClass.cs
index ed7b389..a1b6c7a 100644
--- a/src/BlazeKit.Website/Islands/Components/Web/CssClasses.cs
+++ b/src/BlazeKit.Web/Utils/CssClass.cs
@@ -1,18 +1,18 @@
-namespace BlazeKit.Web;
-public sealed class ConditionalClasses
+namespace BlazeKit.Web.Utils;
+public sealed class CssClass
{
private IDictionary> classes;
- public ConditionalClasses()
+ public CssClass()
{
classes = new Dictionary>();
}
- public ConditionalClasses AddIf(string className, bool condition)
+ public CssClass AddIf(string className, bool condition)
{
return this.AddIf(className,() => condition);
}
- public ConditionalClasses AddIf(string className, Func condition)
+ public CssClass AddIf(string className, Func condition)
{
if(this.classes.ContainsKey(className))
{
diff --git a/src/BlazeKit.Web/Utils/OperatingSystemExtensions.cs b/src/BlazeKit.Web/Utils/OperatingSystemExtensions.cs
new file mode 100644
index 0000000..1b13cd3
--- /dev/null
+++ b/src/BlazeKit.Web/Utils/OperatingSystemExtensions.cs
@@ -0,0 +1,10 @@
+namespace BlazeKit.Web.Utils
+{
+ public static class OperatingSystemExtensions
+ {
+ public static bool IsServer(this OperatingSystem operatingSystem)
+ {
+ return OperatingSystem.IsBrowser() == false;
+ }
+ }
+}
diff --git a/src/BlazeKit.Web/_Imports.razor b/src/BlazeKit.Web/_Imports.razor
new file mode 100644
index 0000000..7728512
--- /dev/null
+++ b/src/BlazeKit.Web/_Imports.razor
@@ -0,0 +1 @@
+@using Microsoft.AspNetCore.Components.Web
diff --git a/src/BlazeKit.Web/wwwroot/background.png b/src/BlazeKit.Web/wwwroot/background.png
new file mode 100644
index 0000000..e15a3bd
Binary files /dev/null and b/src/BlazeKit.Web/wwwroot/background.png differ
diff --git a/src/BlazeKit.Web/wwwroot/exampleJsInterop.js b/src/BlazeKit.Web/wwwroot/exampleJsInterop.js
new file mode 100644
index 0000000..ea8d76a
--- /dev/null
+++ b/src/BlazeKit.Web/wwwroot/exampleJsInterop.js
@@ -0,0 +1,6 @@
+// This is a JavaScript module that is loaded on demand. It can export any number of
+// functions, and may import other JavaScript modules if required.
+
+export function showPrompt(message) {
+ return prompt(message, 'Type anything here');
+}
diff --git a/src/BlazeKit.Website.Islands/BKitHostingEnvironment.cs b/src/BlazeKit.Website.Islands/BKitHostingEnvironment.cs
new file mode 100644
index 0000000..2f9fb71
--- /dev/null
+++ b/src/BlazeKit.Website.Islands/BKitHostingEnvironment.cs
@@ -0,0 +1,16 @@
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Hosting;
+
+namespace BlazeKit.Web;
+
+public class BKitHostEnvironment : IHostEnvironment
+{
+ public BKitHostEnvironment(string environmentName)
+ {
+ EnvironmentName = environmentName;
+ }
+ public string ApplicationName { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+ public IFileProvider ContentRootFileProvider { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+ public string ContentRootPath { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+ public string EnvironmentName { get;set;}
+}
diff --git a/src/BlazeKit.Website.Islands/BlazeKit.Website.Islands.csproj b/src/BlazeKit.Website.Islands/BlazeKit.Website.Islands.csproj
index 37027fa..b441af1 100644
--- a/src/BlazeKit.Website.Islands/BlazeKit.Website.Islands.csproj
+++ b/src/BlazeKit.Website.Islands/BlazeKit.Website.Islands.csproj
@@ -4,7 +4,6 @@
net8.0enableenable
- BlazeKit.Website.ClientDefaulttrue.blazekit
@@ -13,16 +12,11 @@
+
-
-
- Components\%(RecursiveDir)/%(FileName)%(Extension)
-
-
- Components\%(RecursiveDir)/%(FileName)%(Extension)
-
-
+
+
diff --git a/src/BlazeKit.Website.Islands/ClientLoadMode.cs b/src/BlazeKit.Website.Islands/ClientLoadMode.cs
deleted file mode 100644
index 6788b3d..0000000
--- a/src/BlazeKit.Website.Islands/ClientLoadMode.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Microsoft.AspNetCore.Components;
-using Microsoft.AspNetCore.Components.Rendering;
-
-namespace BlazeKit.Islands;
-
- public enum ClientLoadMode
-{
- Idle,
- Hover,
- Visible,
- None
-}
diff --git a/src/BlazeKit.Website/Islands/Components/Counter.razor b/src/BlazeKit.Website.Islands/Components/Counter.razor
similarity index 96%
rename from src/BlazeKit.Website/Islands/Components/Counter.razor
rename to src/BlazeKit.Website.Islands/Components/Counter.razor
index a60bc95..0e4c343 100644
--- a/src/BlazeKit.Website/Islands/Components/Counter.razor
+++ b/src/BlazeKit.Website.Islands/Components/Counter.razor
@@ -1,5 +1,6 @@
-@using BlazeKit.Web;
-@inherits ReactiveComponentEnvelope
+
+@using BlazeKit.Web.Utils
+@inherits BlazeKit.Web.Components.IslandComponentBase
@code
{
@@ -102,7 +103,7 @@
Counter: @counter.Value
Doubled Counter: @doubled.Value
Triggered Is Even-Side Effects (counter.Value % 2 == 0): @booms